Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/strategy/xfundingv2/round.go
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,7 @@ func handleOrderUpdate(twapWorker *TWAPWorker, update types.Order) {
activeOrder := twapWorker.ActiveOrder()
if activeOrder != nil && activeOrder.OrderID == update.OrderID {
activeOrder.Update(update)
twapWorker.logger.Infof("[handleOrderUpdate] active order updated: %s", activeOrder)
}
twapWorker.Executor().UpdateOrder(update)
}
Expand Down
64 changes: 41 additions & 23 deletions pkg/strategy/xfundingv2/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ type Strategy struct {
// followerTWAPWorkerConfig is derived from TWAPWorkerConfig (the leader config)
// with taker orders, so the follower TWAP worker can hedge the leader's fills
// reliably by crossing the spread. Set in Defaults.
followerTWAPWorkerConfig TWAPWorkerConfig
UseFollowerTWAPWorkerConfig bool `json:"useFollowerTWAPWorkerConfig"`
followerTWAPWorkerConfig TWAPWorkerConfig

// Market selection criteria
MarketSelectionConfig *MarketSelectionConfig `json:"marketSelection,omitempty"`
Expand Down Expand Up @@ -346,10 +347,12 @@ func (s *Strategy) Initialize() error {
s.futuresMarkPrices = make(map[string]fixedpoint.Value)
s.spotLastPrices = make(map[string]fixedpoint.Value)

// The follower TWAP worker always crosses the spread with taker orders so it can
// hedge the leader's fills reliably; the leader keeps the configured order type.
s.followerTWAPWorkerConfig = s.TWAPWorkerConfig
s.followerTWAPWorkerConfig.OrderType = TWAPOrderTypeTaker
// If follower twap enabled, the follower TWAP worker always crosses the spread with
// taker orders so it can hedge the leader's fills reliably; the leader keeps the configured order type.
if s.UseFollowerTWAPWorkerConfig {
s.followerTWAPWorkerConfig.OrderType = TWAPOrderTypeTaker
}

return nil
}
Expand Down Expand Up @@ -1025,7 +1028,7 @@ func (s *Strategy) tick(ctx context.Context, tickTime time.Time) {
s.logger.Warnf("round %s position deviation: %+v", roundSymbol, posDeviation)
// the round is originally not halted but the deviation is too large -> we need to halt the round
round.Halt(tickTime)
bbgo.Notify("💥 Round %s halted due to large hedge deviation. Manual intervention is required: spot filled %s, futures filled %s (deviation: %s@%s)",
bbgo.Notify("💥 Round %s halted due to large hedge deviation. Might need manual intervention: spot filled %s, futures filled %s (deviation: %s@%s)",
roundSymbol,
posDeviation.SpotFilled, posDeviation.FuturesFilled,
posDeviation.DeviatedQuantity,
Expand Down Expand Up @@ -1061,7 +1064,7 @@ func (s *Strategy) tick(ctx context.Context, tickTime time.Time) {
)
if found && limiter.AllowN(tickTime, 1) {
// send notification for rounds that have been halted for a while.
bbgo.Notify("💥 Round %s halted for %s (since %s). Manual intervention is required",
bbgo.Notify("💥 Round %s halted for %s (since %s). Might need manual intervention",
roundSymbol,
elapsed.String(),
haltedAt.Format(time.RFC3339),
Expand Down Expand Up @@ -1090,21 +1093,19 @@ func (s *Strategy) tick(ctx context.Context, tickTime time.Time) {
notification := round.NewNotification(spotPrice, futuresPrice)
switch currentState {
case RoundReady:
bbgo.Notify("🟢 Round entered ready state: %s",
round.SpotSymbol(),
args := []any{
notification,
)
}
if s.slackEvtID != "" {
args = append(args, newInteractiveCloseRound(round, s.slackEvtID, spotPrice, futuresPrice))
}
bbgo.Notify("🟢 Round entered ready state: "+round.SpotSymbol(), args...)
case RoundClosing:
bbgo.Notify("🟡 Round is closing: %s",
round.SpotSymbol(),
notification,
)
bbgo.Notify("🟡 Round is closing: "+round.SpotSymbol(), notification)
case RoundClosed:
// no need to attach a notification here for closed round
// since there will be other notifications for it
bbgo.Notify("🔴 Round is closed: %s",
round.String(),
)
bbgo.Notify("🔴 Round is closed: " + round.String())

// enque closed active rounds
s.logger.Infof("move round to closed queue: %s", round)
Expand Down Expand Up @@ -1249,10 +1250,6 @@ func (s *Strategy) transitOpeningOrReadyRoundToClosing(round *ArbitrageRound, in
)
round.SetClosing(currentTime, s.TWAPWorkerConfig.ClosingDuration, futuresPrice)
return
} else {
bbgo.Notify("⚠️ Round funding rate flipped %s -> %s (%s)",
round.TriggeredFundingRate(), index.LastFundingRate, round.SpotSymbol(),
)
}

// the round is still within the min holding time, keep holding
Expand Down Expand Up @@ -1328,6 +1325,7 @@ func (s *Strategy) transitOpeningOrReadyRoundToClosing(round *ArbitrageRound, in
if round.State() != RoundReady {
return
}
// from here, the round is ready

if lastAnnualizedFundingRate.Abs().Compare(s.MinExitRate) <= 0 && !withinMinHoldingTime {
// check hard min exit rate
Expand Down Expand Up @@ -1357,10 +1355,26 @@ func (s *Strategy) transitOpeningOrReadyRoundToClosing(round *ArbitrageRound, in
}
}

// nothing critical happened
var args []any = []any{
round.TriggeredFundingRate().String(), index.LastFundingRate.String(), round.SpotSymbol(),
}
// if it's within the minimum holding time, add an interactive close round notification
if withinMinHoldingTime && s.slackEvtID != "" {
spotPrice, futuresPrice, _ := s.getLastPrices(
round.SpotSymbol(),
round.FuturesSymbol(),
)
args = append(args, newInteractiveCloseRound(round, s.slackEvtID, spotPrice, futuresPrice))
}
bbgo.Notify("⚠️ Round funding rate flipped %s -> %s (%s)",
args...,
)

if s.allowLog(currentTime) {
s.logger.Infof(
"[transitOpeningOrReadyRound %s] round stays %s, current funding rate %s: %s",
currentTime.Format(time.RFC3339), round.State(), index.LastFundingRate, round,
currentTime.Format(time.RFC3339), round.State().String(), index.LastFundingRate.String(), round.String(),
)
}
}
Expand All @@ -1369,7 +1383,7 @@ func (s *Strategy) transitClosingRound(round *ArbitrageRound, currentTime time.T
// the round is expired and is in closing state, keep it closing until it's closed
if s.allowLog(currentTime) {
s.logger.Infof("[transitClosingRound %s] round is closing: %s",
currentTime.Format(time.RFC3339), round)
currentTime.Format(time.RFC3339), round.String())
}
}

Expand Down Expand Up @@ -1912,11 +1926,15 @@ func (s *Strategy) notifyStats() {
// left unchanged, so the round still appears in the "Active Rounds" batch.
if s.slackEvtID != "" && round.State() == RoundReady {
bbgo.Notify(
" ", // dummy text for fixing slack notification error
newInteractiveCloseRound(round, s.slackEvtID, spotPrice, futuresPrice),
round.NewNotification(spotPrice, futuresPrice),
)
} else {
bbgo.Notify(round.NewNotification(spotPrice, futuresPrice))
bbgo.Notify(
" ", // dummy text for fixing slack notification error
round.NewNotification(spotPrice, futuresPrice),
)
}

if s.roundInsertService != nil {
Expand Down
32 changes: 21 additions & 11 deletions pkg/strategy/xfundingv2/twap.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ func (w *TWAPWorker) Tick(currentTime time.Time, orderBook types.OrderBook) erro
if w.syncState.CurrentIntervalEnd.After(w.syncState.EndAt) {
w.syncState.CurrentIntervalEnd = w.syncState.EndAt
}
w.logger.Infof("[TWAP Tick] interval updated: start=%s, end=%s", w.syncState.CurrentIntervalStart, w.syncState.CurrentIntervalEnd)
}
if currentTime.After(w.syncState.EndAt) {
w.syncState.State = TWAPWorkerStateDone
Expand Down Expand Up @@ -459,17 +460,17 @@ func (w *TWAPWorker) Tick(currentTime time.Time, orderBook types.OrderBook) erro
// from here, active order is not nil

// we are within current interval and we have a better price
if w.shouldUpdateActiveOrder(orderBook) && currentTime.Before(w.syncState.CurrentIntervalEnd) {
if currentTime.Before(w.syncState.CurrentIntervalEnd) && w.shouldUpdateActiveOrder(orderBook) {
// throttle order updates to avoid excessive cancel-and-replace
if !w.syncState.LastCheckTime.IsZero() && currentTime.Sub(w.syncState.LastCheckTime) < w.syncState.Config.CheckInterval.Duration() {
return nil
}
w.syncState.LastCheckTime = currentTime
w.logger.Debugf("try to update active order: %s", w.activeOrder)
w.logger.Infof("try to update active order: %s", w.activeOrder)

// the remaining quantity of the active order is dust, no need to update
// the remaining quantity of the active order is not dust, checked in shouldUpdateActiveOrder()
remaining := w.activeOrder.GetRemainingQuantity()
w.logger.Debugf("active order remaining quantity: %s", remaining)
w.logger.Infof("active order remaining quantity: %s", remaining)

if err := w.syncState.TWAPExecutor.CancelOrder(w.ctx, *w.activeOrder); err != nil {
w.logger.WithError(err).Warnf("[TWAP tick] failed to cancel active order: %s", w.activeOrder)
Expand Down Expand Up @@ -504,7 +505,7 @@ func (w *TWAPWorker) Tick(currentTime time.Time, orderBook types.OrderBook) erro
return nil
}

w.logger.Debugf("current interval ended, placing next slice order: %s > %s", currentTime, w.syncState.CurrentIntervalEnd)
w.logger.Infof("current interval ended, placing next slice order: %s > %s", currentTime, w.syncState.CurrentIntervalEnd)
// currentTime is after current interval end, time to place the next slice order
if oldActiveOrder := w.syncAndResetActiveOrder(); oldActiveOrder != nil && !oldActiveOrder.GetRemainingQuantity().IsZero() {
// cancel the old active order before placing the next slice order
Expand All @@ -520,6 +521,7 @@ func (w *TWAPWorker) Tick(currentTime time.Time, orderBook types.OrderBook) erro
w.logger.Infof("slice quantity is dust, skip creating new active order: %s@%s", sliceQty, midPrice)
return nil
}
w.logger.Infof("placing next slice order (%s): %s", w.syncState.Symbol, sliceQty)
createdOrder, err := w.syncState.TWAPExecutor.PlaceOrder(
sliceQty,
orderSide(remaining),
Expand Down Expand Up @@ -625,9 +627,18 @@ func (w *TWAPWorker) shouldUpdateActiveOrder(orderBook types.OrderBook) bool {
return false
}

// taker orders are IOC, if it's filled, do not update
remaining := w.activeOrder.GetRemainingQuantity()
// the current active order is filled -> do not update
if w.activeOrder.Status == types.OrderStatusFilled || remaining.IsZero() {
w.logger.Infof("[TWAP shouldUpdateOrder] active order is filled, should not update: %s", w.activeOrder)
return false
}
// from here on, the active order is not filled

// taker order are IOC and it's not filled -> always update
if w.syncState.Config.OrderType == TWAPOrderTypeTaker {
return w.activeOrder.Status != types.OrderStatusFilled
w.logger.Infof("[TWAP shouldUpdateOrder] taker order is not filled, should update: %s", w.activeOrder)
return true
}

newPrice, err := w.syncState.TWAPExecutor.GetPrice(w.activeOrder.Side, orderBook)
Expand All @@ -636,15 +647,14 @@ func (w *TWAPWorker) shouldUpdateActiveOrder(orderBook types.OrderBook) bool {
return false
}

remaining := w.activeOrder.GetRemainingQuantity()
if w.Market().IsDustQuantity(remaining, newPrice) {
w.logger.Debugf("[TWAP shouldUpdateOrder] remaining quantity of active order is dust, should not update order: %s@%s", remaining, newPrice)
w.logger.Infof("[TWAP shouldUpdateOrder] remaining quantity of active order is dust, should not update order: %s@%s", remaining, newPrice)
return false
}

// remaining is not dust but canceled
if w.activeOrder.Status == types.OrderStatusCanceled {
w.logger.Debugf("[TWAP shouldUpdateOrder] non-dust active order is canceled: %s", w.activeOrder)
w.logger.Infof("[TWAP shouldUpdateOrder] non-dust active order is canceled: %s", w.activeOrder)
return true
}

Expand All @@ -655,7 +665,7 @@ func (w *TWAPWorker) shouldUpdateActiveOrder(orderBook types.OrderBook) bool {
case types.SideTypeSell:
newPriceBtter = newPrice.Compare(w.activeOrder.Price) > 0
}
w.logger.Debugf("[TWAP shouldUpdateOrder] order update check: current price=%s, new price=%s, better=%t",
w.logger.Infof("[TWAP shouldUpdateOrder] order update check: current price=%s, new price=%s, better=%t",
w.activeOrder.Price.String(), newPrice.String(), newPriceBtter)
return newPriceBtter
}
Expand Down
Loading