Skip to content

Commit 3d0a03a

Browse files
committed
✨ feat(xfundingv2): implement persistence for strategy state
1 parent f658084 commit 3d0a03a

1 file changed

Lines changed: 52 additions & 32 deletions

File tree

pkg/strategy/xfundingv2/strategy.go

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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

204190
func (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
544559
func (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

Comments
 (0)