From 3177b4a694dfef2b4c8f075f14b481e4ff5a01d0 Mon Sep 17 00:00:00 2001 From: dboyliao <6830390+dboyliao@users.noreply.github.com> Date: Sat, 12 Sep 2026 09:14:25 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20feat(xfundingv2):=20add=20optio?= =?UTF-8?q?n=20to=20enable=20follower=20TWAP=20worker=20and=20improve=20no?= =?UTF-8?q?tification=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/strategy/xfundingv2/strategy.go | 54 ++++++++++++++++++----------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/pkg/strategy/xfundingv2/strategy.go b/pkg/strategy/xfundingv2/strategy.go index 2dc0840d34..fa0f771592 100644 --- a/pkg/strategy/xfundingv2/strategy.go +++ b/pkg/strategy/xfundingv2/strategy.go @@ -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"` @@ -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 } @@ -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, @@ -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), @@ -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) @@ -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 @@ -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 @@ -1357,6 +1355,22 @@ func (s *Strategy) transitOpeningOrReadyRoundToClosing(round *ArbitrageRound, in } } + // nothing critical happened + var args []any = []any{ + round.TriggeredFundingRate(), index.LastFundingRate, 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", From daf28a9e620a9f668932ef5c9753c920465eafa0 Mon Sep 17 00:00:00 2001 From: dboyliao <6830390+dboyliao@users.noreply.github.com> Date: Sat, 12 Sep 2026 09:26:39 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=94=A7=20fix(xfundingv2):=20add=20dum?= =?UTF-8?q?my=20text=20to=20slack=20notifications=20to=20resolve=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/strategy/xfundingv2/strategy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xfundingv2/strategy.go b/pkg/strategy/xfundingv2/strategy.go index fa0f771592..99a5cc9f34 100644 --- a/pkg/strategy/xfundingv2/strategy.go +++ b/pkg/strategy/xfundingv2/strategy.go @@ -1926,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 { From 1f6824be831ea2a979c64614f22fe9fb1ccb6e29 Mon Sep 17 00:00:00 2001 From: dboyliao <6830390+dboyliao@users.noreply.github.com> Date: Sat, 12 Sep 2026 09:41:10 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=94=A7=20fix(xfundingv2):=20ensure=20?= =?UTF-8?q?active=20order=20updates=20for=20non-filled=20taker=20orders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/strategy/xfundingv2/twap.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/xfundingv2/twap.go b/pkg/strategy/xfundingv2/twap.go index 4c1ad42241..674888ef6c 100644 --- a/pkg/strategy/xfundingv2/twap.go +++ b/pkg/strategy/xfundingv2/twap.go @@ -625,9 +625,15 @@ func (w *TWAPWorker) shouldUpdateActiveOrder(orderBook types.OrderBook) bool { return false } - // taker orders are IOC, if it's filled, do not update + // the current active order is filled -> do not update + if w.activeOrder.Status == types.OrderStatusFilled { + 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 + return true } newPrice, err := w.syncState.TWAPExecutor.GetPrice(w.activeOrder.Side, orderBook) From bd6fcae249289d4b004d643245c636e5a657c898 Mon Sep 17 00:00:00 2001 From: dboyliao <6830390+dboyliao@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:10:17 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=94=84=20refactor(xfundingv2):=20enha?= =?UTF-8?q?nce=20logging=20in=20TWAPWorker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/strategy/xfundingv2/round.go | 1 + pkg/strategy/xfundingv2/strategy.go | 6 +++--- pkg/strategy/xfundingv2/twap.go | 24 ++++++++++++++---------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/pkg/strategy/xfundingv2/round.go b/pkg/strategy/xfundingv2/round.go index f1b93e63ba..2d8c854f19 100644 --- a/pkg/strategy/xfundingv2/round.go +++ b/pkg/strategy/xfundingv2/round.go @@ -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) } diff --git a/pkg/strategy/xfundingv2/strategy.go b/pkg/strategy/xfundingv2/strategy.go index 99a5cc9f34..943946f1f6 100644 --- a/pkg/strategy/xfundingv2/strategy.go +++ b/pkg/strategy/xfundingv2/strategy.go @@ -1357,7 +1357,7 @@ func (s *Strategy) transitOpeningOrReadyRoundToClosing(round *ArbitrageRound, in // nothing critical happened var args []any = []any{ - round.TriggeredFundingRate(), index.LastFundingRate, round.SpotSymbol(), + 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 != "" { @@ -1374,7 +1374,7 @@ func (s *Strategy) transitOpeningOrReadyRoundToClosing(round *ArbitrageRound, in 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(), ) } } @@ -1383,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()) } } diff --git a/pkg/strategy/xfundingv2/twap.go b/pkg/strategy/xfundingv2/twap.go index 674888ef6c..a93d204be8 100644 --- a/pkg/strategy/xfundingv2/twap.go +++ b/pkg/strategy/xfundingv2/twap.go @@ -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 @@ -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) @@ -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 @@ -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), @@ -625,14 +627,17 @@ func (w *TWAPWorker) shouldUpdateActiveOrder(orderBook types.OrderBook) bool { return false } + remaining := w.activeOrder.GetRemainingQuantity() // the current active order is filled -> do not update - if w.activeOrder.Status == types.OrderStatusFilled { + 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 { + w.logger.Infof("[TWAP shouldUpdateOrder] taker order is not filled, should update: %s", w.activeOrder) return true } @@ -642,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 } @@ -661,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 }