diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 454f62c93..e36b12c6a 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -347,6 +347,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order bbgo.Notify("Spot Position", s.SpotPosition) bbgo.Notify("Futures Position", s.FuturesPosition) bbgo.Notify("Neutral Position", s.NeutralPosition) + bbgo.Notify("State", s.State.PositionState) // sync funding fee txns if !s.ProfitStats.LastFundingFeeTime.IsZero() { @@ -356,6 +357,20 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order // TEST CODE: // s.syncFundingFeeRecords(ctx, time.Now().Add(-3*24*time.Hour)) + switch s.State.PositionState { + case PositionOpening: + // transfer all base assets from the spot account into the spot account + if err := s.transferIn(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, fixedpoint.Zero); err != nil { + log.WithError(err).Errorf("futures asset transfer in error") + } + + case PositionClosing, PositionClosed: + // transfer all base assets from the futures account back to the spot account + if err := s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, fixedpoint.Zero); err != nil { + log.WithError(err).Errorf("futures asset transfer out error") + } + } + s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { // we act differently on the spot account @@ -405,8 +420,11 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order switch s.getPositionState() { case PositionClosing: + // de-leverage and get the collateral base quantity for transfer + quantity := trade.Quantity.Div(s.Leverage) + if err := backoff.RetryGeneral(ctx, func() error { - return s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, trade.Quantity) + return s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, quantity) }); err != nil { log.WithError(err).Errorf("spot-to-futures transfer in retry failed") return @@ -635,13 +653,39 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) { return } + spotBase := s.SpotPosition.GetBase() + if !s.spotMarket.IsDustQuantity(spotBase, s.SpotPosition.AverageCost) { + if balance, ok := s.futuresSession.Account.Balance(s.futuresMarket.BaseCurrency); ok && balance.Available.Sign() > 0 { + if err := backoff.RetryGeneral(ctx, func() error { + return s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, balance.Available) + }); err != nil { + log.WithError(err).Errorf("spot-to-futures transfer in retry failed") + } + } + } + if futuresBase.Compare(fixedpoint.Zero) < 0 { orderPrice := ticker.Buy orderQuantity := futuresBase.Abs() orderQuantity = fixedpoint.Max(orderQuantity, s.minQuantity) orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) + if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { - log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) + submitOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Price: orderPrice, + Market: s.futuresMarket, + + // quantity: Cannot be sent with closePosition=true(Close-All) + // reduceOnly: Cannot be sent with closePosition=true + ClosePosition: true, + } + + if _, err := s.futuresOrderExecutor.SubmitOrders(ctx, submitOrder); err != nil { + log.WithError(err).Errorf("can not submit futures order with close position: %+v", submitOrder) + } return } @@ -685,7 +729,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } - log.Infof("position comparision: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) + log.Infof("syncFuturesPosition: position comparision: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) if futuresBase.Sign() > 0 { // unexpected error @@ -722,7 +766,8 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { } // if - futures position < max futures position, increase it - if futuresBase.Neg().Compare(maxFuturesBasePosition) >= 0 { + // posDiff := futuresBase.Abs().Sub(maxFuturesBasePosition) + if futuresBase.Abs().Compare(maxFuturesBasePosition) >= 0 { s.setPositionState(PositionReady) bbgo.Notify("Position Ready") @@ -745,12 +790,16 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { log.Infof("position diff quantity: %s", diffQuantity.String()) - orderQuantity := fixedpoint.Max(diffQuantity, s.minQuantity) + orderQuantity := diffQuantity + orderQuantity = fixedpoint.Max(diffQuantity, s.minQuantity) orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) - if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { - log.Warnf("unexpected dust quantity, skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) - return - } + + /* + if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Warnf("unexpected dust quantity, skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) + return + } + */ submitOrder := types.SubmitOrder{ Symbol: s.Symbol, @@ -792,7 +841,7 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { return } - log.Infof("spot/futures positions: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) + log.Infof("syncSpotPosition: spot/futures positions: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) if futuresBase.Sign() > 0 { // unexpected error @@ -800,7 +849,7 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { return } - _ = s.futuresOrderExecutor.GracefulCancel(ctx) + _ = s.spotOrderExecutor.GracefulCancel(ctx) ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { @@ -831,27 +880,34 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { orderPrice := ticker.Sell orderQuantity := diffQuantity - if b, ok := s.spotSession.Account.Balance(s.spotMarket.BaseCurrency); ok { - orderQuantity = fixedpoint.Min(b.Available, orderQuantity) - } - - // avoid increase the order size - if s.spotMarket.IsDustQuantity(orderQuantity, orderPrice) { - log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.spotMarket) + b, ok := s.spotSession.Account.Balance(s.spotMarket.BaseCurrency) + if !ok { + log.Warnf("%s balance not found, can not sync spot position", s.spotMarket.BaseCurrency) return } - createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + log.Infof("spot balance: %+v", b) + + orderQuantity = fixedpoint.Min(b.Available, orderQuantity) + + // avoid increase the order size + if s.spotMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Infof("skip spot order with dust quantity %s, market=%+v balance=%+v", orderQuantity.String(), s.spotMarket, b) + return + } + + submitOrder := types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimitMaker, Quantity: orderQuantity, Price: orderPrice, - Market: s.futuresMarket, - }) + Market: s.spotMarket, + } + createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, submitOrder) if err != nil { - log.WithError(err).Errorf("can not submit spot order") + log.WithError(err).Errorf("can not submit spot order: %+v", submitOrder) return } @@ -1092,6 +1148,13 @@ func (s *Strategy) checkAndRestorePositionRisks(ctx context.Context) error { return err } + log.Infof("fetched futures position risks: %+v", positionRisks) + + if len(positionRisks) == 0 { + s.FuturesPosition.Reset() + return nil + } + for _, positionRisk := range positionRisks { if positionRisk.Symbol != s.Symbol { continue diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 3de3540aa..afd5c8a0a 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -41,31 +41,33 @@ func (s *Strategy) resetTransfer(ctx context.Context, ex FuturesTransfer, asset return nil } -func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset string, tradeQuantity fixedpoint.Value) error { +func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset string, quantity fixedpoint.Value) error { // if transfer done if s.State.TotalBaseTransfer.IsZero() { return nil } - // de-leverage and get the collateral base quantity for transfer - quantity := tradeQuantity.Div(s.Leverage) - balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) if err != nil { - log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), asset, s.State.PendingBaseTransfer.String()) + log.Infof("balance query error, adding to pending base transfer: %s %s + %s", quantity.String(), asset, s.State.PendingBaseTransfer.String()) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return err } b, ok := balances[asset] if !ok { - log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), asset, s.State.PendingBaseTransfer.String()) + log.Infof("balance not found, adding to pending base transfer: %s %s + %s", quantity.String(), asset, s.State.PendingBaseTransfer.String()) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return fmt.Errorf("%s balance not found", asset) } + log.Infof("found futures balance: %+v", b) + // add the previous pending base transfer and the current trade quantity - amount := s.State.PendingBaseTransfer.Add(quantity) + amount := b.MaxWithdrawAmount + if !quantity.IsZero() { + amount = s.State.PendingBaseTransfer.Add(quantity) + } // try to transfer more if we enough balance amount = fixedpoint.Min(amount, b.MaxWithdrawAmount) @@ -75,7 +77,7 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset st // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here if amount.IsZero() { - log.Infof("adding to pending base transfer: %s %s + %s ", quantity.String(), asset, s.State.PendingBaseTransfer.String()) + log.Infof("zero amount, adding to pending base transfer: %s %s + %s ", quantity.String(), asset, s.State.PendingBaseTransfer.String()) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } @@ -111,17 +113,19 @@ func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, asset str } // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.Compare(quantity) < 0 { + if !quantity.IsZero() && b.Available.Compare(quantity) < 0 { log.Infof("adding to pending base transfer: %s %s", quantity, asset) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } - amount := s.State.PendingBaseTransfer.Add(quantity) + amount := b.Available + if !quantity.IsZero() { + amount = s.State.PendingBaseTransfer.Add(quantity) + } pos := s.SpotPosition.GetBase().Abs() rest := pos.Sub(s.State.TotalBaseTransfer) - if rest.Sign() < 0 { return nil }