From 108fb6138a97aeac1e062ffde94c3af40b37752c Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 28 Aug 2024 14:48:38 +0800 Subject: [PATCH 1/4] xmaker: check hedge balance only when it's spot account --- pkg/strategy/xmaker/strategy.go | 70 +++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index c20738ae9..95c957c00 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -63,6 +63,9 @@ type Strategy struct { BollBandMargin fixedpoint.Value `json:"bollBandMargin"` BollBandMarginFactor fixedpoint.Value `json:"bollBandMarginFactor"` + // MinMarginLevel is the minimum margin level to trigger the hedge + MinMarginLevel fixedpoint.Value `json:"minMarginLevel"` + StopHedgeQuoteBalance fixedpoint.Value `json:"stopHedgeQuoteBalance"` StopHedgeBaseBalance fixedpoint.Value `json:"stopHedgeBaseBalance"` @@ -650,6 +653,36 @@ func (s *Strategy) updateQuote(ctx context.Context) { _ = createdOrders } +func (s *Strategy) adjustHedgeQuantityWithAvailableBalance(side types.SideType, quantity, lastPrice fixedpoint.Value) fixedpoint.Value { + // adjust quantity according to the balances + account := s.sourceSession.GetAccount() + + switch side { + + case types.SideTypeBuy: + // check quote quantity + if quote, ok := account.Balance(s.sourceMarket.QuoteCurrency); ok { + if quote.Available.Compare(s.sourceMarket.MinNotional) < 0 { + // adjust price to higher 0.1%, so that we can ensure that the order can be executed + availableQuote := s.sourceMarket.TruncateQuoteQuantity(quote.Available) + quantity = bbgo.AdjustQuantityByMaxAmount(quantity, lastPrice, availableQuote) + + } + } + + case types.SideTypeSell: + // check quote quantity + if base, ok := account.Balance(s.sourceMarket.BaseCurrency); ok { + if base.Available.Compare(quantity) < 0 { + quantity = base.Available + } + } + } + + // truncate the quantity to the supported precision + return s.sourceMarket.TruncateQuantity(quantity) +} + func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { side := types.SideTypeBuy if pos.IsZero() { @@ -677,36 +710,27 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { } } - notional := quantity.Mul(lastPrice) - - // adjust quantity according to the balances - account := s.sourceSession.GetAccount() - switch side { - - case types.SideTypeBuy: - // check quote quantity - if quote, ok := account.Balance(s.sourceMarket.QuoteCurrency); ok { - if quote.Available.Compare(notional) < 0 { - // adjust price to higher 0.1%, so that we can ensure that the order can be executed - quantity = bbgo.AdjustQuantityByMaxAmount(quantity, lastPrice.Mul(lastPriceModifier), quote.Available) - quantity = s.sourceMarket.TruncateQuantity(quantity) - } + if s.sourceSession.Margin { + // check the margin level + account, err := s.sourceSession.UpdateAccount(ctx) + if err != nil { + log.WithError(err).Errorf("unable to update account") + return } - case types.SideTypeSell: - // check quote quantity - if base, ok := account.Balance(s.sourceMarket.BaseCurrency); ok { - if base.Available.Compare(quantity) < 0 { - quantity = base.Available - } + if !s.MinMarginLevel.IsZero() && !account.MarginLevel.IsZero() && account.MarginLevel.Compare(s.MinMarginLevel) < 0 { + log.Errorf("margin level %f is too low (< %f), skip hedge", account.MarginLevel.Float64(), s.MinMarginLevel.Float64()) + return } + } else { + quantity = s.adjustHedgeQuantityWithAvailableBalance(side, quantity, lastPrice) } // truncate quantity for the supported precision quantity = s.sourceMarket.TruncateQuantity(quantity) if s.sourceMarket.IsDustQuantity(quantity, lastPrice) { - log.Warnf("skip dust quantity: %s", quantity.String()) + log.Warnf("skip dust quantity: %s @ price %f", quantity.String(), lastPrice.Float64()) return } @@ -821,6 +845,10 @@ func (s *Strategy) Defaults() error { s.NumLayers = 1 } + if s.MinMarginLevel.IsZero() { + s.MinMarginLevel = fixedpoint.NewFromFloat(3.0) + } + if s.BidMargin.IsZero() { if !s.Margin.IsZero() { s.BidMargin = s.Margin From 1d6282a10ba0cbb517c3f83684264a3921857c5b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 28 Aug 2024 16:07:11 +0800 Subject: [PATCH 2/4] xmaker: add account updater and handle margin account to add more flexibility --- pkg/strategy/xmaker/strategy.go | 151 +++++++++++++++++++++++--------- 1 file changed, 112 insertions(+), 39 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 95c957c00..a47682c4b 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -129,6 +129,8 @@ type Strategy struct { askPriceHeartBeat, bidPriceHeartBeat *types.PriceHeartBeat + accountValueCalculator *bbgo.AccountValueCalculator + lastPrice fixedpoint.Value groupID uint32 @@ -193,7 +195,6 @@ func aggregatePrice(pvs types.PriceVolumeSlice, requiredQuantity fixedpoint.Valu func (s *Strategy) Initialize() error { s.bidPriceHeartBeat = types.NewPriceHeartBeat(priceUpdateTimeout) s.askPriceHeartBeat = types.NewPriceHeartBeat(priceUpdateTimeout) - s.logger = logrus.WithFields(logrus.Fields{ "symbol": s.Symbol, "strategy": ID, @@ -272,7 +273,7 @@ func (s *Strategy) applyBollingerMargin( // so that the original bid margin can be multiplied by 1.x bollMargin := s.BollBandMargin.Mul(ratio).Mul(factor) - s.logger.Infof("%s bollband uptrend adjusting bid margin %f (askMargin) + %f (bollMargin) = %f (finalAskMargin)", + s.logger.Infof("%s bollband uptrend adjusting ask margin %f (askMargin) + %f (bollMargin) = %f (finalAskMargin)", s.Symbol, quote.AskMargin.Float64(), bollMargin.Float64(), @@ -369,44 +370,90 @@ func (s *Strategy) updateQuote(ctx context.Context) { } } - hedgeBalances := s.sourceSession.GetAccount().Balances() + // if + // 1) the source session is a margin session + // 2) the min margin level is configured + // 3) the hedge account's margin level is lower than the min margin level + hedgeAccount := s.sourceSession.GetAccount() + hedgeBalances := hedgeAccount.Balances() hedgeQuota := &bbgo.QuotaTransaction{} - if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok { - // to make bid orders, we need enough base asset in the foreign exchange, - // if the base asset balance is not enough for selling - if s.StopHedgeBaseBalance.Sign() > 0 { - minAvailable := s.StopHedgeBaseBalance.Add(s.sourceMarket.MinQuantity) - if b.Available.Compare(minAvailable) > 0 { - hedgeQuota.BaseAsset.Add(b.Available.Sub(minAvailable)) + + if s.sourceSession.Margin && + !s.MinMarginLevel.IsZero() && + !hedgeAccount.MarginLevel.IsZero() { + + if hedgeAccount.MarginLevel.Compare(s.MinMarginLevel) < 0 { + if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok { + quoteDebt := quote.Debt() + if quoteDebt.Sign() > 0 { + hedgeQuota.BaseAsset.Add(quoteDebt.Div(bestBid.Price)) + } + } + + if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok { + baseDebt := base.Debt() + if baseDebt.Sign() > 0 { + hedgeQuota.QuoteAsset.Add(baseDebt.Mul(bestAsk.Price)) + } + } + } 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.BaseAsset.Add(netQuote.Mul(creditBufferRatio).Div(bestBid.Price)) + } + } + + if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok { + netBase := base.Net() + if netBase.Sign() > 0 { + hedgeQuota.QuoteAsset.Add(netBase.Mul(creditBufferRatio).Mul(bestAsk.Price)) + } + } + // 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, + // if the base asset balance is not enough for selling + if s.StopHedgeBaseBalance.Sign() > 0 { + minAvailable := s.StopHedgeBaseBalance.Add(s.sourceMarket.MinQuantity) + 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()) + 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()) 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()) - disableMakerBid = true } - } - if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok { - // to make ask orders, we need enough quote asset in the foreign exchange, - // if the quote asset balance is not enough for buying - if s.StopHedgeQuoteBalance.Sign() > 0 { - minAvailable := s.StopHedgeQuoteBalance.Add(s.sourceMarket.MinNotional) - if b.Available.Compare(minAvailable) > 0 { - hedgeQuota.QuoteAsset.Add(b.Available.Sub(minAvailable)) + if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok { + // to make ask orders, we need enough quote asset in the foreign exchange, + // if the quote asset balance is not enough for buying + if s.StopHedgeQuoteBalance.Sign() > 0 { + minAvailable := s.StopHedgeQuoteBalance.Add(s.sourceMarket.MinNotional) + 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()) + 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()) 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()) - disableMakerAsk = true } + } // if max exposure position is configured, we should not: @@ -653,10 +700,9 @@ func (s *Strategy) updateQuote(ctx context.Context) { _ = createdOrders } -func (s *Strategy) adjustHedgeQuantityWithAvailableBalance(side types.SideType, quantity, lastPrice fixedpoint.Value) fixedpoint.Value { - // adjust quantity according to the balances - account := s.sourceSession.GetAccount() - +func (s *Strategy) adjustHedgeQuantityWithAvailableBalance( + account *types.Account, side types.SideType, quantity, lastPrice fixedpoint.Value, +) fixedpoint.Value { switch side { case types.SideTypeBuy: @@ -710,20 +756,15 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { } } + account := s.sourceSession.GetAccount() if s.sourceSession.Margin { // check the margin level - account, err := s.sourceSession.UpdateAccount(ctx) - if err != nil { - log.WithError(err).Errorf("unable to update account") - return - } - if !s.MinMarginLevel.IsZero() && !account.MarginLevel.IsZero() && account.MarginLevel.Compare(s.MinMarginLevel) < 0 { log.Errorf("margin level %f is too low (< %f), skip hedge", account.MarginLevel.Float64(), s.MinMarginLevel.Float64()) return } } else { - quantity = s.adjustHedgeQuantityWithAvailableBalance(side, quantity, lastPrice) + quantity = s.adjustHedgeQuantityWithAvailableBalance(account, side, quantity, lastPrice) } // truncate quantity for the supported precision @@ -920,6 +961,35 @@ func (s *Strategy) quoteWorker(ctx context.Context) { } } +func (s *Strategy) accountUpdater(ctx context.Context) { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + if _, err := s.sourceSession.UpdateAccount(ctx); err != nil { + log.WithError(err).Errorf("unable to update account") + } + + if err := s.accountValueCalculator.UpdatePrices(ctx); err != nil { + log.WithError(err).Errorf("unable to update account value with prices") + return + } + + netValue, err := s.accountValueCalculator.NetValue(ctx) + if err != nil { + log.WithError(err).Errorf("unable to update account") + return + } + + s.logger.Infof("hedge session net value ~= %f USD", netValue.Float64()) + } + } +} + func (s *Strategy) hedgeWorker(ctx context.Context) { ticker := time.NewTicker(util.MillisecondsJitter(s.HedgeInterval.Duration(), 200)) defer ticker.Stop() @@ -1008,6 +1078,8 @@ func (s *Strategy) CrossRun( return fmt.Errorf("maker session market %s is not defined", s.Symbol) } + s.accountValueCalculator = bbgo.NewAccountValueCalculator(s.sourceSession, s.sourceMarket.QuoteCurrency) + indicators := s.sourceSession.Indicators(s.Symbol) s.boll = indicators.BOLL(types.IntervalWindow{ @@ -1164,6 +1236,7 @@ func (s *Strategy) CrossRun( go s.tradeRecover(ctx) } + go s.accountUpdater(ctx) go s.hedgeWorker(ctx) go s.quoteWorker(ctx) From 77b7b29739a4940538a90a70a7519974953b82fe Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 28 Aug 2024 16:37:39 +0800 Subject: [PATCH 3/4] xmaker: adjust credit buffer algo --- pkg/strategy/xmaker/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index a47682c4b..e2e08956f 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -402,14 +402,14 @@ func (s *Strategy) updateQuote(ctx context.Context) { if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok { netQuote := quote.Net() if netQuote.Sign() > 0 { - hedgeQuota.BaseAsset.Add(netQuote.Mul(creditBufferRatio).Div(bestBid.Price)) + hedgeQuota.QuoteAsset.Add(netQuote.Mul(creditBufferRatio)) } } if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok { netBase := base.Net() if netBase.Sign() > 0 { - hedgeQuota.QuoteAsset.Add(netBase.Mul(creditBufferRatio).Mul(bestAsk.Price)) + hedgeQuota.BaseAsset.Add(netBase.Mul(creditBufferRatio)) } } // netValueInUsd, err := s.accountValueCalculator.NetValue(ctx) From d36bbe5fb558e458b3d8d5fb144e92070a2d1b4a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 28 Aug 2024 16:41:40 +0800 Subject: [PATCH 4/4] xmaker: adjust accountUpdater's ticker to 3 min --- pkg/strategy/xmaker/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index e2e08956f..89ec6e22f 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -962,7 +962,7 @@ func (s *Strategy) quoteWorker(ctx context.Context) { } func (s *Strategy) accountUpdater(ctx context.Context) { - ticker := time.NewTicker(1 * time.Minute) + ticker := time.NewTicker(3 * time.Minute) defer ticker.Stop() for { select {