@@ -71,11 +71,15 @@ type Strategy struct {
7171
7272 // CandidateSymbols is the list of symbols to consider for selection
7373 // IMPORTANT: xfundingv2 is now assuming trading on U-major pairs
74- CandidateSymbols []string `json:"candidateSymbols"`
75- OpenPositionInterval types.Duration `json:"openPositionInterval"`
76- TransitRoundInterval types.Duration `json:"transitRoundInterval"`
77- RoundRebalanceInterval types.Duration `json:"roundRebalanceInterval"`
78- MaxClosingLossRatio fixedpoint.Value `json:"maxClosingLossRatio"`
74+ CandidateSymbols []string `json:"candidateSymbols"`
75+ OpenPositionInterval types.Duration `json:"openPositionInterval"`
76+ TransitRoundInterval types.Duration `json:"transitRoundInterval"`
77+ RoundRebalanceInterval types.Duration `json:"roundRebalanceInterval"`
78+
79+ // round closing conditions
80+ // TODO: move all the closing conditions into a separate struct
81+ MaxClosingLossRatio fixedpoint.Value `json:"maxClosingLossRatio"`
82+ MinExitRate fixedpoint.Value `json:"minExitRate"`
7983
8084 // TickSymbol is the symbol used for ticking the strategy, default to the first candidate symbol
8185 TickSymbol string `json:"tickSymbol"`
@@ -206,6 +210,15 @@ func (s *Strategy) Defaults() error {
206210 }
207211 s .MarketSelectionConfig .Defaults ()
208212
213+ if s .MinExitRate .IsZero () {
214+ // default to 4% (ref: US short-term treasury rate)
215+ s .MinExitRate = fixedpoint .NewFromFloat (0.04 ) // 4%
216+ }
217+ s .MinExitRate = fixedpoint .Min (
218+ s .MinExitRate ,
219+ s .MarketSelectionConfig .MinAnnualizedRate ,
220+ )
221+
209222 if s .TradeBalanceRatio .IsZero () {
210223 s .TradeBalanceRatio = fixedpoint .NewFromFloat (0.8 )
211224 }
@@ -792,7 +805,10 @@ func (s *Strategy) CrossRun(
792805 round .SpotSymbol (),
793806 round .FuturesSymbol (),
794807 )
795- bbgo .Notify ("⚠️ Round is set to closing state on startup" , round .NewNotification (spotPrice , futuresPrice ))
808+ bbgo .Notify ("⚠️ Round is set to closing state on startup: %s" ,
809+ round .String (),
810+ round .NewNotification (spotPrice , futuresPrice ),
811+ )
796812 }
797813 s .PendingRounds = make (map [string ]* PendingRound )
798814 s .mu .Unlock ()
@@ -1121,18 +1137,20 @@ func (s *Strategy) transitRound(ctx context.Context, round *ArbitrageRound, curr
11211137
11221138func (s * Strategy ) transitOpeningOrReadyRoundToClosing (round * ArbitrageRound , index * types.PremiumIndex , currentTime time.Time ) {
11231139 // if the current funding rate is still favorable, stay in current state, otherwise transit to closing
1140+ lastAnnualizedFundingRate := AnnualizedRate (index .LastFundingRate , round .syncState .FundingIntervalHours )
1141+ spotPrice , futuresPrice , _ := s .getLastPrices (
1142+ round .SpotSymbol (),
1143+ round .FuturesSymbol (),
1144+ )
11241145 if round .TriggeredFundingRate ().Sign ()* index .LastFundingRate .Sign () <= 0 {
11251146 // the funding rate has flipped
1126- spotPrice , futuresPrice , _ := s .getLastPrices (
1127- round .SpotSymbol (),
1128- round .FuturesSymbol (),
1129- )
11301147 rateDiffAbs := index .LastFundingRate .Sub (round .TriggeredFundingRate ()).Abs ()
11311148 if rateDiffAbs .Compare (s .CriticalErrorConfig .MaxFundingRateFlip ) > 0 {
1132- bbgo .Notify ("🚨 Round funding rate flip is too large: %s -> %s (threshold %s), closing round " ,
1149+ bbgo .Notify ("🚨 Round funding rate flip is too large: %s -> %s (threshold %s), closing: %s " ,
11331150 round .TriggeredFundingRate (),
11341151 index .LastFundingRate ,
11351152 s .CriticalErrorConfig .MaxFundingRateFlip ,
1153+ round .String (),
11361154 round .NewCriticalNotification (spotPrice , futuresPrice ),
11371155 )
11381156 round .SetClosing (currentTime , s .TWAPWorkerConfig .ClosingDuration )
@@ -1155,9 +1173,10 @@ func (s *Strategy) transitOpeningOrReadyRoundToClosing(round *ArbitrageRound, in
11551173
11561174 // the round is beyond the max holding time, transit to closing
11571175 if currentTime .Sub (round .StartedAt ()) >= s .MarketSelectionConfig .MaxHoldingDuration .Duration () {
1158- s .logger .Infof (
1159- "[transitOpeningOrReadyRound %s] max holding hours reached, transit state %s -> closing, current funding rate %s: %s" ,
1160- currentTime .Format (time .RFC3339 ), round .State (), index .LastFundingRate , round ,
1176+ bbgo .Notify (
1177+ "⚠️ Max holding hours reached, transit state %s -> closing, current funding rate %s: %s" ,
1178+ round .State (), index .LastFundingRate , round .String (),
1179+ round .NewNotification (spotPrice , futuresPrice ),
11611180 )
11621181 round .SetClosing (currentTime , s .TWAPWorkerConfig .ClosingDuration )
11631182 return
@@ -1187,12 +1206,19 @@ func (s *Strategy) transitOpeningOrReadyRoundToClosing(round *ArbitrageRound, in
11871206 "[transitOpeningOrReadyRound %s] unrealized total PnL: %s, next funding income: %s, futures position notional: %s, max closing loss ratio: %s" ,
11881207 unrealizedTotalPnL , nextFundingIncome , futuresPositionNotional , s .MaxClosingLossRatio ,
11891208 )
1190- s .logger .Infof (
1191- "[transitOpeningOrReadyRound %s] transit state %s -> closing, current funding rate %s: %s" ,
1192- currentTime .Format (time .RFC3339 ), round .State (), index .LastFundingRate , round )
1209+ bbgo .Notify (
1210+ "⚠️ Unrealized total PnL too large (%s), transit state %s -> closing, current funding rate %s: %s" ,
1211+ unrealizedTotalPnL , round .State (), index .LastFundingRate , round .String (),
1212+ round .NewNotification (spotPrice , futuresPrice ),
1213+ )
11931214 round .SetClosing (currentTime , s .TWAPWorkerConfig .ClosingDuration )
11941215 return
11951216 }
1217+ } else if lastAnnualizedFundingRate .Abs ().Compare (s .MinExitRate ) <= 0 {
1218+ bbgo .Notify ("⚠️ Last funding rate %s(annualized %s) is below the min exit rate %s, transit state %s -> closing: %s" ,
1219+ index .LastFundingRate , lastAnnualizedFundingRate , s .MinExitRate , round .State (), round .String (),
1220+ round .NewNotification (spotPrice , futuresPrice ),
1221+ )
11961222 }
11971223
11981224 if s .allowLog (currentTime ) {
0 commit comments