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)