xfunding: implement close position transfer

This commit is contained in:
c9s 2023-03-23 22:54:42 +08:00
parent aba80398d9
commit 3624dd0338
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
3 changed files with 100 additions and 77 deletions

View File

@ -273,45 +273,60 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
//
// when closing a position, we place orders on the futures account first, then the spot account
// we need to close the position according to its base quantity instead of quote quantity
if s.positionType == types.PositionShort {
switch s.positionAction {
case PositionOpening:
if trade.Side != types.SideTypeBuy {
log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade)
return
}
s.mu.Lock()
defer s.mu.Unlock()
s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity)
if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 {
s.positionAction = PositionNoOp
}
// 1) if we have trade, try to query the balance and transfer the balance to the futures wallet account
// TODO: handle missing trades here. If the process crashed during the transfer, how to recover?
if err := backoff.RetryGeneric(ctx, func() error {
return s.transferIn(ctx, binanceSpot, trade)
}); err != nil {
log.WithError(err).Errorf("spot-to-futures transfer in retry failed")
return
}
// 2) transferred successfully, sync futures position
// compare spot position and futures position, increase the position size until they are the same size
case PositionClosing:
if trade.Side != types.SideTypeSell {
log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade)
return
}
if s.positionType != types.PositionShort {
return
}
switch s.positionAction {
case PositionOpening:
if trade.Side != types.SideTypeBuy {
log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade)
return
}
s.mu.Lock()
defer s.mu.Unlock()
s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity)
if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 {
s.positionAction = PositionNoOp
}
// if we have trade, try to query the balance and transfer the balance to the futures wallet account
// TODO: handle missing trades here. If the process crashed during the transfer, how to recover?
if err := backoff.RetryGeneral(ctx, func() error {
return s.transferIn(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade)
}); err != nil {
log.WithError(err).Errorf("spot-to-futures transfer in retry failed")
return
}
case PositionClosing:
if trade.Side != types.SideTypeSell {
log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade)
return
}
}
})
s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition)
s.futuresOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
if s.positionType != types.PositionShort {
return
}
switch s.positionAction {
case PositionClosing:
if err := backoff.RetryGeneral(ctx, func() error {
return s.transferOut(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade)
}); err != nil {
log.WithError(err).Errorf("spot-to-futures transfer in retry failed")
return
}
}
})
s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) {
// s.queryAndDetectPremiumIndex(ctx, binanceFutures)
@ -421,6 +436,8 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) {
}
// syncFuturesPosition syncs the futures position with the given spot position
// when the spot is transferred successfully, sync futures position
// compare spot position and futures position, increase the position size until they are the same size
func (s *Strategy) syncFuturesPosition(ctx context.Context) {
if s.positionType != types.PositionShort {
return
@ -495,7 +512,6 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) {
Quantity: orderQuantity,
Price: orderPrice,
Market: s.futuresMarket,
// TimeInForce: types.TimeInForceGTC,
})
if err != nil {
@ -574,38 +590,40 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed
log.Infof("last %s funding rate: %s", s.Symbol, fundingRate.Percentage())
if s.ShortFundingRate != nil {
if fundingRate.Compare(s.ShortFundingRate.High) >= 0 {
if s.ShortFundingRate == nil {
return changed
}
log.Infof("funding rate %s is higher than the High threshold %s, start opening position...",
fundingRate.Percentage(), s.ShortFundingRate.High.Percentage())
if fundingRate.Compare(s.ShortFundingRate.High) >= 0 {
s.positionAction = PositionOpening
s.positionType = types.PositionShort
log.Infof("funding rate %s is higher than the High threshold %s, start opening position...",
fundingRate.Percentage(), s.ShortFundingRate.High.Percentage())
// reset the transfer stats
s.State.PositionStartTime = premiumIndex.Time
s.State.PendingBaseTransfer = fixedpoint.Zero
s.State.TotalBaseTransfer = fixedpoint.Zero
changed = true
} else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 {
s.positionAction = PositionOpening
s.positionType = types.PositionShort
log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...",
fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage())
// reset the transfer stats
s.State.PositionStartTime = premiumIndex.Time
s.State.PendingBaseTransfer = fixedpoint.Zero
s.State.TotalBaseTransfer = fixedpoint.Zero
changed = true
} else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 {
holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime)
if holdingPeriod < time.Duration(s.MinHoldingPeriod) {
log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod)
return
}
log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...",
fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage())
s.positionAction = PositionClosing
// reset the transfer stats
s.State.PendingBaseTransfer = fixedpoint.Zero
s.State.TotalBaseTransfer = fixedpoint.Zero
changed = true
holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime)
if holdingPeriod < time.Duration(s.MinHoldingPeriod) {
log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod)
return
}
s.positionAction = PositionClosing
// reset the transfer stats
s.State.PendingBaseTransfer = fixedpoint.Zero
s.State.TotalBaseTransfer = fixedpoint.Zero
changed = true
}
return changed

View File

@ -13,15 +13,13 @@ type FuturesTransfer interface {
QueryAccountBalances(ctx context.Context) (types.BalanceMap, error)
}
func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, trade types.Trade) error {
currency := s.spotMarket.BaseCurrency
func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, currency string, trade types.Trade) error {
// base asset needs BUY trades
if trade.Side == types.SideTypeBuy {
return nil
}
balances, err := ex.QueryAccountBalances(ctx)
balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx)
if err != nil {
return err
}
@ -31,16 +29,23 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, trade ty
return fmt.Errorf("%s balance not found", currency)
}
quantity := trade.Quantity
if s.Leverage.Compare(fixedpoint.One) > 0 {
// de-leverage and get the collateral base quantity for transfer
quantity = quantity.Div(s.Leverage)
}
// 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(trade.Quantity) < 0 {
log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency)
s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity)
if b.Available.IsZero() || b.Available.Compare(quantity) < 0 {
log.Infof("adding to pending base transfer: %s %s", quantity, currency)
s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity)
return nil
}
amount := s.State.PendingBaseTransfer.Add(trade.Quantity)
amount := s.State.PendingBaseTransfer.Add(quantity)
pos := s.SpotPosition.GetBase()
pos := s.FuturesPosition.GetBase().Abs().Div(s.Leverage)
rest := pos.Sub(s.State.TotalBaseTransfer)
if rest.Sign() < 0 {
@ -62,15 +67,14 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, trade ty
return nil
}
func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, trade types.Trade) error {
currency := s.spotMarket.BaseCurrency
func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, currency string, trade types.Trade) error {
// base asset needs BUY trades
if trade.Side == types.SideTypeSell {
return nil
}
balances, err := ex.QueryAccountBalances(ctx)
balances, err := s.spotSession.Exchange.QueryAccountBalances(ctx)
if err != nil {
return err
}
@ -81,15 +85,16 @@ func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, trade typ
}
// 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(trade.Quantity) < 0 {
log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency)
s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity)
quantity := trade.Quantity
if b.Available.Compare(quantity) < 0 {
log.Infof("adding to pending base transfer: %s %s", quantity, currency)
s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity)
return nil
}
amount := s.State.PendingBaseTransfer.Add(trade.Quantity)
amount := s.State.PendingBaseTransfer.Add(quantity)
pos := s.SpotPosition.GetBase()
pos := s.SpotPosition.GetBase().Abs()
rest := pos.Sub(s.State.TotalBaseTransfer)
if rest.Sign() < 0 {

View File

@ -8,7 +8,7 @@ import (
var MaxRetries uint64 = 101
func RetryGeneric(ctx context.Context, op backoff.Operation) (err error) {
func RetryGeneral(ctx context.Context, op backoff.Operation) (err error) {
err = backoff.Retry(op, backoff.WithContext(
backoff.WithMaxRetries(
backoff.NewExponentialBackOff(),