Skip to content

Commit

Permalink
xfunding: implement close position transfer
Browse files Browse the repository at this point in the history
  • Loading branch information
c9s committed Mar 23, 2023
1 parent aba8039 commit 3624dd0
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 77 deletions.
136 changes: 77 additions & 59 deletions pkg/strategy/xfunding/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -495,7 +512,6 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) {
Quantity: orderQuantity,
Price: orderPrice,
Market: s.futuresMarket,
// TimeInForce: types.TimeInForceGTC,
})

if err != nil {
Expand Down Expand Up @@ -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 {

log.Infof("funding rate %s is higher than the High threshold %s, start opening position...",
fundingRate.Percentage(), s.ShortFundingRate.High.Percentage())
if s.ShortFundingRate == nil {
return changed
}

s.positionAction = PositionOpening
s.positionType = types.PositionShort
if fundingRate.Compare(s.ShortFundingRate.High) >= 0 {

// 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 {
log.Infof("funding rate %s is higher than the High threshold %s, start opening position...",
fundingRate.Percentage(), s.ShortFundingRate.High.Percentage())

log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...",
fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage())
s.positionAction = PositionOpening
s.positionType = types.PositionShort

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
}
// 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 = PositionClosing
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.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
Expand Down
39 changes: 22 additions & 17 deletions pkg/strategy/xfunding/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 {
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down

0 comments on commit 3624dd0

Please sign in to comment.