@@ -68,15 +68,15 @@ type Strategy struct {
6868 costEstimator * CostEstimator
6969 preliminaryMarketSelector * MarketSelector
7070
71- pendingRounds map [string ]* PendingRound
72- activeRounds map [string ]* ArbitrageRound
73-
7471 coinmarketcapClient * coinmarketcap.DataSource
7572
76- // persist the positions
73+ // persistence states
74+ // pending rounds and active rounds
75+ pendingRounds map [string ]* PendingRound `persistence:"pendingRounds"`
76+ activeRounds map [string ]* ArbitrageRound `persistence:"activeRounds"`
7777 // the positions are shared across rounds and the executors of the same symbol.
78- spotPositions map [string ]* types.Position `persistence:"spot_positions "`
79- futuresPositions map [string ]* types.Position `persistence:"futures_positions "`
78+ spotPositions map [string ]* types.Position `persistence:"spotPositions,omitempty "`
79+ futuresPositions map [string ]* types.Position `persistence:"futuresPositions,omitempty "`
8080
8181 // order executors for each symbol
8282 // we need to cache the executors as map at startup since the executors are bound to the user data stream (via `.Bind()`).
@@ -146,29 +146,15 @@ func (s *Strategy) Initialize() error {
146146 s .futuresOrderBooks = make (map [string ]* types.StreamOrderBook )
147147 s .spotOrderBooks = make (map [string ]* types.StreamOrderBook )
148148
149- // Initialize position maps (may be populated by LoadState if persisted state exists)
150- if s .spotPositions == nil {
151- s .spotPositions = make (map [string ]* types.Position )
152- }
153- if s .futuresPositions == nil {
154- s .futuresPositions = make (map [string ]* types.Position )
155- }
156-
157149 // Initialize executor maps
158- if s .spotGeneralOrderExecutors == nil {
159- s .spotGeneralOrderExecutors = make (map [string ]* bbgo.GeneralOrderExecutor )
160- }
161- if s .futuresGeneralOrderExecutors == nil {
162- s .futuresGeneralOrderExecutors = make (map [string ]* bbgo.GeneralOrderExecutor )
163- }
150+ s .spotGeneralOrderExecutors = make (map [string ]* bbgo.GeneralOrderExecutor )
151+ s .futuresGeneralOrderExecutors = make (map [string ]* bbgo.GeneralOrderExecutor )
164152 if ! bbgo .IsBackTesting {
165153 s .logLimiter = rate .NewLimiter (rate .Every (time .Minute * 10 ), 1 )
166154 }
167155 if s .MaxPositionExposure == nil {
168156 s .MaxPositionExposure = make (map [string ]fixedpoint.Value )
169157 }
170- s .activeRounds = make (map [string ]* ArbitrageRound )
171- s .pendingRounds = make (map [string ]* PendingRound )
172158 return nil
173159}
174160
@@ -202,8 +188,24 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {
202188}
203189
204190func (s * Strategy ) CrossRun (
205- ctx context.Context , orderExecutionRouter bbgo.OrderExecutionRouter , sessions map [string ]* bbgo.ExchangeSession ,
191+ ctx context.Context , _ bbgo.OrderExecutionRouter , sessions map [string ]* bbgo.ExchangeSession ,
206192) error {
193+ // Initialize position maps (may be populated by LoadState if persisted state exists)
194+ if s .spotPositions == nil {
195+ s .spotPositions = make (map [string ]* types.Position )
196+ }
197+ if s .futuresPositions == nil {
198+ s .futuresPositions = make (map [string ]* types.Position )
199+ }
200+
201+ // Initialize round maps (may be populated by LoadState if persisted state exists)
202+ if s .activeRounds == nil {
203+ s .activeRounds = make (map [string ]* ArbitrageRound )
204+ }
205+ if s .pendingRounds == nil {
206+ s .pendingRounds = make (map [string ]* PendingRound )
207+ }
208+
207209 s .spotSession = sessions [s .SpotSession ]
208210 s .futuresSession = sessions [s .FuturesSession ]
209211
@@ -377,6 +379,19 @@ func (s *Strategy) CrossRun(
377379 binanceEx , _ := s .futuresSession .Exchange .(* binance.Exchange )
378380 s .preliminaryMarketSelector = NewMarketSelector (* s .MarketSelectionConfig , binanceEx , s .logger )
379381
382+ // runtime init done, load pending and active rounds
383+ for symbol , pendingRound := range s .pendingRounds {
384+ if err := pendingRound .LoadStrategy (ctx , s ); err != nil {
385+ return fmt .Errorf ("failed to restore pending round (%s): %w" , symbol , err )
386+ }
387+ }
388+ for symbol , activeRound := range s .activeRounds {
389+ if err := activeRound .LoadStrategy (ctx , s ); err != nil {
390+ return fmt .Errorf ("failed to restore active round (%s): %w" , symbol , err )
391+ }
392+ }
393+
394+ // setup callbacks
380395 for _ , sess := range []* bbgo.ExchangeSession {s .spotSession , s .futuresSession } {
381396 sess .MarketDataStream .OnKLineClosed (types .KLineWith (s .TickSymbol , types .Interval1m , func (kline types.KLine ) {
382397 s .tick (ctx , kline .EndTime .Time ())
@@ -449,8 +464,8 @@ func (s *Strategy) tick(ctx context.Context, tickTime time.Time) {
449464
450465 // 4. tick existing active rounds
451466 for _ , round := range s .activeRounds {
452- spotOrderBook := s .spotOrderBooks [round .spotWorker . Symbol ()].Copy ()
453- futuresOrderBook := s .futuresOrderBooks [round .futuresWorker . Symbol ()].Copy ()
467+ spotOrderBook := s .spotOrderBooks [round .SpotSymbol ()].Copy ()
468+ futuresOrderBook := s .futuresOrderBooks [round .FuturesSymbol ()].Copy ()
454469 round .Tick (tickTime , spotOrderBook , futuresOrderBook )
455470 }
456471}
@@ -544,18 +559,23 @@ func (s *Strategy) transitClosingRound(ctx context.Context, round *ArbitrageRoun
544559func (s * Strategy ) checkOpenNewRound (ctx context.Context , currentTime time.Time ) {
545560 var lastOpenTime time.Time
546561 for _ , round := range s .activeRounds {
562+ startTime := round .StartTime ()
547563 if lastOpenTime .IsZero () {
548- lastOpenTime = round .StartTime ()
564+ lastOpenTime = startTime
565+ continue
566+ }
567+ if startTime .After (lastOpenTime ) {
568+ lastOpenTime = startTime
549569 }
550570 }
551571 if ! lastOpenTime .IsZero () && currentTime .Sub (lastOpenTime ) < s .OpenPositionInterval .Duration () {
552572 // still within the open position cooldown time, do not try to open new round
553573 return
554574 }
555575
576+ // Only open new round when there is no active round
577+ // TODO: support multiple active rounds for different symbols concurrently (e.g BTCUSDT and ETHUSDT)
556578 if len (s .activeRounds ) == 0 {
557- // Only open new round when there is no active round
558- // TODO: support multiple active rounds for different symbols concurrently (e.g BTCUSDT and ETHUSDT)
559579 candidates , err := s .preliminaryMarketSelector .SelectMarkets (ctx , s .candidateSymbols )
560580 if err != nil {
561581 s .logger .WithError (err ).Error ("failed to select market candidates" )
@@ -581,14 +601,14 @@ func (s *Strategy) checkOpenNewRound(ctx context.Context, currentTime time.Time)
581601 if selectedCandidate .MinHoldingDuration <= s .MarketSelectionConfig .MaxHoldingHours .Duration () {
582602 spotExecutor := s .spotGeneralOrderExecutors [selectedCandidate .Symbol ]
583603 spotTwap , err := NewTWAPWorker (ctx , selectedCandidate .Symbol , s .spotSession , spotExecutor , s .TWAPWorkerConfig )
584- if err != nil {
604+ if err != nil || spotTwap == nil {
585605 s .logger .WithError (err ).Errorf ("failed to create TWAP worker for spot %s" , selectedCandidate .Symbol )
586606 return
587607 }
588608 spotTwap .SetTargetPosition (selectedCandidate .TargetFuturesPosition .Neg ())
589609 futuresExecutor := s .futuresGeneralOrderExecutors [selectedCandidate .Symbol ]
590610 futuresTwap , err := NewTWAPWorker (ctx , selectedCandidate .Symbol , s .futuresSession , futuresExecutor , s .TWAPWorkerConfig )
591- if err != nil {
611+ if err != nil || futuresTwap == nil {
592612 s .logger .WithError (err ).Errorf ("failed to create TWAP worker for futures %s" , selectedCandidate .Symbol )
593613 return
594614 }
@@ -827,10 +847,10 @@ func (s *Strategy) handleRoundExit(ctx context.Context, round *ArbitrageRound, t
827847 switch s .MarketSelectionConfig .FuturesDirection {
828848 case types .PositionShort :
829849 // short futures -> transfer base currency
830- asset = round .futuresWorker . Market ().BaseCurrency
850+ asset = round .FuturesMarket ().BaseCurrency
831851 case types .PositionLong :
832852 // long futures -> transfer quote currency
833- asset = round .futuresWorker . Market ().QuoteCurrency
853+ asset = round .FuturesMarket ().QuoteCurrency
834854 }
835855 account := s .futuresSession .GetAccount ()
836856 balance , ok := account .Balance (asset )
0 commit comments