Compare commits

...

11 Commits

Author SHA1 Message Date
c9s
f12ba1adb9
bbgo: add comments to the quota methods
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
2024-09-02 22:18:13 +08:00
c9s
294e529a98
xmaker: add more logs 2024-09-02 16:08:51 +08:00
c9s
f30aca1b5a
xmaker: update position metrics when restored 2024-09-02 15:51:31 +08:00
c9s
f9b9832fff
add more logs 2024-09-02 15:51:31 +08:00
c9s
2bf1072977
Merge pull request #1725 from c9s/c9s/xmaker/stb-improvements
IMPROVE: [xmaker] improve hedge margin account leverage calculation
2024-09-02 15:29:53 +08:00
c9s
4d1c357c3d
xmaker: reuse makerMarket field 2024-09-01 17:55:00 +08:00
c9s
a4833524cf
xmaker: add more logs 2024-09-01 16:41:16 +08:00
c9s
ed073264f1
xmaker: add MaxHedgeAccountLeverage option 2024-09-01 15:42:36 +08:00
c9s
ad6056834e
xmaker: separate maximumValueInUsd in a new var 2024-09-01 01:34:25 +08:00
c9s
8b1306a6a6
xmaker: calculate maximum leveraged account value 2024-09-01 01:31:50 +08:00
c9s
d85da78e17
xmaker: improve hedge account credit calculation 2024-09-01 00:58:50 +08:00
3 changed files with 96 additions and 30 deletions

View File

@ -12,12 +12,16 @@ type Quota struct {
Locked fixedpoint.Value
}
// Add adds the fund to the available quota
func (q *Quota) Add(fund fixedpoint.Value) {
q.mu.Lock()
q.Available = q.Available.Add(fund)
q.mu.Unlock()
}
// Lock locks the fund from the available quota
// returns true if the fund is locked successfully
// returns false if the fund is not enough
func (q *Quota) Lock(fund fixedpoint.Value) bool {
if fund.Compare(q.Available) > 0 {
return false
@ -31,12 +35,15 @@ func (q *Quota) Lock(fund fixedpoint.Value) bool {
return true
}
// Commit commits the locked fund
func (q *Quota) Commit() {
q.mu.Lock()
q.Locked = fixedpoint.Zero
q.mu.Unlock()
}
// Rollback rolls back the locked fund
// this will move the locked fund to the available quota
func (q *Quota) Rollback() {
q.mu.Lock()
q.Available = q.Available.Add(q.Locked)
@ -44,12 +51,14 @@ func (q *Quota) Rollback() {
q.mu.Unlock()
}
// QuotaTransaction is a transactional quota manager
type QuotaTransaction struct {
mu sync.Mutex
BaseAsset Quota
QuoteAsset Quota
}
// Commit commits the transaction
func (m *QuotaTransaction) Commit() bool {
m.mu.Lock()
m.BaseAsset.Commit()
@ -58,6 +67,7 @@ func (m *QuotaTransaction) Commit() bool {
return true
}
// Rollback rolls back the transaction
func (m *QuotaTransaction) Rollback() bool {
m.mu.Lock()
m.BaseAsset.Rollback()

View File

@ -119,6 +119,8 @@ type Strategy struct {
// MaxExposurePosition defines the unhedged quantity of stop
MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"`
MaxHedgeAccountLeverage fixedpoint.Value `json:"maxHedgeAccountLeverage"`
DisableHedge bool `json:"disableHedge"`
NotifyTrade bool `json:"notifyTrade"`
@ -424,10 +426,10 @@ func (s *Strategy) updateQuote(ctx context.Context) {
if s.CircuitBreaker != nil {
now := time.Now()
if reason, halted := s.CircuitBreaker.IsHalted(now); halted {
s.logger.Warnf("[arbWorker] strategy is halted, reason: %s", reason)
s.logger.Warnf("strategy %s is halted, reason: %s", ID, reason)
if s.circuitBreakerAlertLimiter.AllowN(now, 1) {
bbgo.Notify("Strategy is halted, reason: %s", reason)
bbgo.Notify("Strategy %s is halted, reason: %s", ID, reason)
}
return
@ -436,6 +438,7 @@ func (s *Strategy) updateQuote(ctx context.Context) {
bestBid, bestAsk, hasPrice := s.book.BestBidAndAsk()
if !hasPrice {
s.logger.Warnf("no valid price, skip quoting")
return
}
@ -472,14 +475,21 @@ func (s *Strategy) updateQuote(ctx context.Context) {
// check maker's balance quota
// we load the balances from the account while we're generating the orders,
// the balance may have a chance to be deducted by other strategies or manual orders submitted by the user
makerBalances := s.makerSession.GetAccount().Balances()
makerBalances := s.makerSession.GetAccount().Balances().NotZero()
s.logger.Infof("maker balances: %+v", makerBalances)
makerQuota := &bbgo.QuotaTransaction{}
if b, ok := makerBalances[s.makerMarket.BaseCurrency]; ok {
if b.Available.Compare(s.makerMarket.MinQuantity) > 0 {
makerQuota.BaseAsset.Add(b.Available)
} else {
if s.makerMarket.IsDustQuantity(b.Available, s.lastPrice) {
disableMakerAsk = true
s.logger.Infof("%s maker ask disabled: insufficient base balance %s", s.Symbol, b.String())
} else {
makerQuota.BaseAsset.Add(b.Available)
}
} else {
disableMakerAsk = true
s.logger.Infof("%s maker ask disabled: base balance %s not found", s.Symbol, b.String())
}
if b, ok := makerBalances[s.makerMarket.QuoteCurrency]; ok {
@ -487,9 +497,15 @@ func (s *Strategy) updateQuote(ctx context.Context) {
makerQuota.QuoteAsset.Add(b.Available)
} else {
disableMakerBid = true
s.logger.Infof("%s maker bid disabled: insufficient quote balance %s", s.Symbol, b.String())
}
} else {
disableMakerBid = true
s.logger.Infof("%s maker bid disabled: quote balance %s not found", s.Symbol, b.String())
}
s.logger.Infof("maker quota: %+v", makerQuota)
// if
// 1) the source session is a margin session
// 2) the min margin level is configured
@ -503,6 +519,11 @@ func (s *Strategy) updateQuote(ctx context.Context) {
!hedgeAccount.MarginLevel.IsZero() {
if hedgeAccount.MarginLevel.Compare(s.MinMarginLevel) < 0 {
s.logger.Infof("hedge account margin level %s is less then the min margin level %s, calculating the borrowed positions",
hedgeAccount.MarginLevel.String(),
s.MinMarginLevel.String())
// TODO: should consider base asset debt as well.
if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok {
quoteDebt := quote.Debt()
if quoteDebt.Sign() > 0 {
@ -517,24 +538,46 @@ func (s *Strategy) updateQuote(ctx context.Context) {
}
}
} else {
// credit buffer
creditBufferRatio := fixedpoint.NewFromFloat(1.2)
if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok {
netQuote := quote.Net()
if netQuote.Sign() > 0 {
hedgeQuota.QuoteAsset.Add(netQuote.Mul(creditBufferRatio))
}
}
s.logger.Infof("hedge account margin level %s is greater than the min margin level %s, calculating the net value",
hedgeAccount.MarginLevel.String(),
s.MinMarginLevel.String())
if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok {
netBase := base.Net()
if netBase.Sign() > 0 {
hedgeQuota.BaseAsset.Add(netBase.Mul(creditBufferRatio))
netValueInUsd, calcErr := s.accountValueCalculator.NetValue(ctx)
if calcErr != nil {
s.logger.WithError(calcErr).Errorf("unable to calculate the net value")
} else {
// calculate credit buffer
s.logger.Infof("hedge account net value in usd: %f", netValueInUsd.Float64())
maximumValueInUsd := netValueInUsd.Mul(s.MaxHedgeAccountLeverage)
s.logger.Infof("hedge account maximum leveraged value in usd: %f (%f x)", maximumValueInUsd.Float64(), s.MaxHedgeAccountLeverage.Float64())
if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok {
debt := quote.Debt()
quota := maximumValueInUsd.Sub(debt)
s.logger.Infof("hedge account quote balance: %s, debt: %s, quota: %s",
quote.String(),
debt.String(),
quota.String())
hedgeQuota.QuoteAsset.Add(quota)
}
if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok {
debt := base.Debt()
quota := maximumValueInUsd.Div(bestAsk.Price).Sub(debt)
s.logger.Infof("hedge account base balance: %s, debt: %s, quota: %s",
base.String(),
debt.String(),
quota.String())
hedgeQuota.BaseAsset.Add(quota)
}
}
// netValueInUsd, err := s.accountValueCalculator.NetValue(ctx)
}
} else {
if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok {
// to make bid orders, we need enough base asset in the foreign exchange,
@ -544,13 +587,13 @@ func (s *Strategy) updateQuote(ctx context.Context) {
if b.Available.Compare(minAvailable) > 0 {
hedgeQuota.BaseAsset.Add(b.Available.Sub(minAvailable))
} else {
s.logger.Warnf("%s maker bid disabled: insufficient base balance %s", s.Symbol, b.String())
s.logger.Warnf("%s maker bid disabled: insufficient hedge base balance %s", s.Symbol, b.String())
disableMakerBid = true
}
} else if b.Available.Compare(s.sourceMarket.MinQuantity) > 0 {
hedgeQuota.BaseAsset.Add(b.Available)
} else {
s.logger.Warnf("%s maker bid disabled: insufficient base balance %s", s.Symbol, b.String())
s.logger.Warnf("%s maker bid disabled: insufficient hedge base balance %s", s.Symbol, b.String())
disableMakerBid = true
}
}
@ -563,17 +606,16 @@ func (s *Strategy) updateQuote(ctx context.Context) {
if b.Available.Compare(minAvailable) > 0 {
hedgeQuota.QuoteAsset.Add(b.Available.Sub(minAvailable))
} else {
s.logger.Warnf("%s maker ask disabled: insufficient quote balance %s", s.Symbol, b.String())
s.logger.Warnf("%s maker ask disabled: insufficient hedge quote balance %s", s.Symbol, b.String())
disableMakerAsk = true
}
} else if b.Available.Compare(s.sourceMarket.MinNotional) > 0 {
hedgeQuota.QuoteAsset.Add(b.Available)
} else {
s.logger.Warnf("%s maker ask disabled: insufficient quote balance %s", s.Symbol, b.String())
s.logger.Warnf("%s maker ask disabled: insufficient hedge quote balance %s", s.Symbol, b.String())
disableMakerAsk = true
}
}
}
// if max exposure position is configured, we should not:
@ -811,6 +853,7 @@ func (s *Strategy) updateQuote(ctx context.Context) {
createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.makerSession.Exchange, orderCreateCallback, formattedOrders...)
if err != nil {
log.WithError(err).Errorf("unable to place maker orders: %+v", formattedOrders)
return
}
openOrderBidExposureInUsdMetrics.With(s.metricsLabels).Set(bidExposureInUsd.Float64())
@ -1009,6 +1052,10 @@ func (s *Strategy) Defaults() error {
s.MinMarginLevel = fixedpoint.NewFromFloat(3.0)
}
if s.MaxHedgeAccountLeverage.IsZero() {
s.MaxHedgeAccountLeverage = fixedpoint.NewFromFloat(1.2)
}
if s.BidMargin.IsZero() {
if !s.Margin.IsZero() {
s.BidMargin = s.Margin
@ -1207,7 +1254,7 @@ func (s *Strategy) CrossRun(
// restore state
s.groupID = util.FNV32(instanceID)
log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID)
s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID)
configLabels := prometheus.Labels{"strategy_id": s.InstanceID(), "strategy_type": ID, "symbol": s.Symbol}
configNumOfLayersMetrics.With(configLabels).Set(float64(s.NumLayers))
@ -1219,8 +1266,12 @@ func (s *Strategy) CrossRun(
s.Position = types.NewPositionFromMarket(s.makerMarket)
s.Position.Strategy = ID
s.Position.StrategyInstanceID = instanceID
} else {
s.Position.Strategy = ID
s.Position.StrategyInstanceID = instanceID
}
s.Position.UpdateMetrics()
bbgo.Notify("xmaker: %s position is restored", s.Symbol, s.Position)
if s.ProfitStats == nil {
@ -1267,9 +1318,8 @@ func (s *Strategy) CrossRun(
return errors.New("tradesSince time can not be zero")
}
makerMarket, _ := makerSession.Market(s.Symbol)
position := types.NewPositionFromMarket(makerMarket)
profitStats := types.NewProfitStats(makerMarket)
position := types.NewPositionFromMarket(s.makerMarket)
profitStats := types.NewProfitStats(s.makerMarket)
fixer := common.NewProfitFixer()
// fixer.ConverterManager = s.ConverterManager
@ -1284,7 +1334,7 @@ func (s *Strategy) CrossRun(
fixer.AddExchange(sourceSession.Name, ss)
}
if err2 := fixer.Fix(ctx, makerMarket.Symbol,
if err2 := fixer.Fix(ctx, s.makerMarket.Symbol,
s.ProfitFixerConfig.TradesSince.Time(),
time.Now(),
profitStats,

View File

@ -656,6 +656,12 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp
return fixedpoint.Zero, fixedpoint.Zero, false
}
func (p *Position) UpdateMetrics() {
p.Lock()
p.updateMetrics()
p.Unlock()
}
func (p *Position) updateMetrics() {
// update the position metrics only if the position defines the strategy ID
if p.StrategyInstanceID == "" || p.Strategy == "" {