Futures Rollover #17

Open
derfenix wants to merge 30 commits from feature/futures-resolver into master
Owner
No description provided.
- YAML futures_groups[] config block with validation
- domain FuturesContract / FuturesGroupState models
- FuturesProvider interface on domain.Provider
- Tinkoff adapter: GetFuturesByBasicAsset with process-wide 1h cache
- futuresresolver.Service: Refresh, Near/Next, IsRolloverDue, SwitchToNext
- GET /status endpoint with futures groups state (ogen)
- Logging when near contract approaches expiry window
- Full test suite: resolution, cache, boundary dates, expired contracts
- CandleProcessor type and integration in TradingPipelineWrapper
- Trader.SwitchInstrument: runtime FIGI change with channel-based stream restart
- futuresrollover.Controller: multi-candle close/open rollover with retry
- Wiring in TraderFactory: auto-detect futures_group, create controller
- Pass FuturesResolver through factory to RolloverController
- Tests: rollover flow with/without position, no-next, unknown group
- futuresresolver: remove dead globalFuturesCache, delegate Refresh→RefreshAt
- futuresrollover: remove unused RolloverDecision, provider, SessionReportAtLastMark;
  propagate ExecuteDecision errors; wait for close settlement before opening next;
  fix phase flow (close→wait for fill→open)
- trader: MetricsProjectionKey uses Instrument() with RLock; add candleCtx/candleCancel
  for proper stream cleanup on FIGI switch; fix unlocked t.instrument reads
- config: validate duplicate ticker_root
- tinkoff: inline no-op globalFuturesCacheKey
- api: nil-guard GetStatus
- controller: phaseOpenNext устанавливается только после успешного ExecuteDecision
  (retry при ошибке открытия next-контракта)
- trader: streamRootCtx — корневой контекст для всех стримов, дочерний от candleCtx;
  новый стрим при FIGI-свитче создаётся от streamRootCtx, а не Background(),
  поэтому корректно отменяется при Stop трейдера
- futuresresolver.ForceRollover: bypasses window check for one rollover cycle
- POST /rollover/{tickerRoot} — validates group exists, calls ForceRollover
- GET /status already covers dashboard status
- Controller.SetNearFIGIUpdater callback, fired after SwitchToNext
- TraderFactory wires updater: updates Config.FuturesGroups[].NearFIGI
  and calls config.Service.Save
- onNextFrame to ensure FuturesGroups round-trips via YAML encode/decode
- refreshGroupAt: prefer config NearFIGI as nearIdx over date-based selection
- GroupStateAt: fall back to cfg.NearFIGI when near() is nil (no contracts loaded)
- AllGroupStatesAt: same fallback for NextFIGI
- Tests: config near_figi fallback + refresh matching
- Application.Start calls futuresResolver.Refresh after env.Start
  so group status (nearFIGI, daysUntilExpiry) is available immediately
  via GET /status, not only after the first candle triggers the controller
The api_trade_available_flag filter excluded expired/near-expiry contracts,
so the config near_figi (set after a rollover or manually) could not be matched
in the contract list. The resolver now sees all contracts for a basic asset
regardless of tradeability flag — the execution path (PostOrder) handles
tradeability checks.
Added debug-level logs to trace:
- refreshGroupAt: contract count, matching near_figi, nearIdx result
- GroupStateAt: why fallback triggers, contract load status
- Application.Start: resolver pre-population attempt
When GetFuturesByBasicAsset returns 0 contracts, log up to 5 distinct
basic_asset values from the API response so the user can fix their
config ticker_root.
Restored the api_trade_available_flag filter to avoid resolving ancient
expired contracts that lack min_price_increment data.
If the configured near_figi was filtered out (non-tradeable), it is resolved
individually via ResolveFuturesContract and prepended to the contract list.
- next() now skips contracts with LastTradingDay equal to near (mini variants)
- SwitchToNext finds the correct index by FIGI, not just nearIdx++
- Test: same-expiry mini contract skipped correctly
- figiPrefix helper extracts leading letters from FIGI (e.g. FUTSILV06260 → FUTSILV)
- next() now only considers contracts with the same prefix as the near contract
  (skips different series like FSL* and mini variants with different prefixes)
- SwitchToNext uses FIGI match for correct index
- Tests: different-series skip + figiPrefix
- Removed Config.FuturesGroups, FuturesGroupConfig, yamlFuturesGroup
- InstrumentConfig.FuturesGroup → FuturesRolloverEnabled (bool) + RolloverWindowDays
- buildFuturesResolver resolves each instrument FIGI via FuturesProvider
  to discover BasicAsset, groups by basic asset
- after rollover, instruments[].id is updated to new FIGI
- next() filters by FIGI prefix (same series), skips mini/different-series
- Tests updated for new types and FIGI prefix matching
- NearFIGIUpdater now passes oldFIGI + accountID for exact config entry match
  (not just first rollover-enabled instrument with different ID)
- finalizeRollover captures old near's FIGI before SwitchToNext
- Factory updater matches by ID + AccountID, preventing wrong-instrument updates
- On restart: new FIGI from config is used for instrument resolution and
  stream creation; resolver auto-discovers group from new FIGI
Removed Debug-level log statements added during troubleshooting:
- refreshGroupAt: contract listing, near_figi matching, fallback path
- GroupStateAt: nil near fallback diagnostics
- Application.Start: Pre-populating resolver message
- finalizeRollover: SwitchInstrument error теперь возвращается (не глотается),
  stream не переключается → rollover не завершается
- finalizeRollover: nil-guard для newNear.FIGI в логе
- figiPrefix: если FIGI начинается с цифры, возвращается весь FIGI (не пустая строка)
- добавил switched bool — SwitchToNext выполняется только один раз
  при повторном вызове finalizeRollover (после ошибки SwitchInstrument)
  nearIdx не сдвигается повторно
- при next == nil сбрасывается phaseIdle, чтобы не было бесконечного retry
derfenix changed title from WIP: Futures Rollover to Futures Rollover 2026-06-01 12:50:50 +00:00
- pkg/backtest/merge: Build, MergedCandlesSource, Config (ticker_root, overlap_days)
- YAML backtest.merge_futures + runtime config MergeFuturesConfig
- BacktestProviderAdapter.WithMergeFutures
- ContainerBuilder.WithBacktestMergeFutures → BacktestEnvironment.SetMergeFutures
- Integration: runSingleWindow extracts MergeFutures from BacktestBlock
- Tests: two contracts, single contract, no overlap, empty ticker_root, edge cuts
- merge loop: non-first segments now trim start to prevRollDate (no duplicates)
- contracts sorted by LastTradingDay before processing
- removed unused ContractInfo struct
- overlap_days < 1 returns error instead of silent default 3
- tests use explicit dates (no AddDate off-by-one), exact assertions
- WithMergeFutures comment corrected
- added TestBuild_NegativeOverlap, TestBuild_ThreeContracts
- BacktestProviderAdapter: debug log when type assertion fails
  so the user knows why merge was skipped
- merge.Config.TickerRoot удалён — basic asset определяется из FIGI
  через FuturesDiscoverer.ResolveFuturesContract
- merge.Build принимает instrumentFIGI вместо cfg.TickerRoot
- MergeFuturesConfig/Block: удалён TickerRoot
- BacktestProviderAdapter.NewCandleSource: resolve из instrument FIGI
- tests: testContract helper с BasicAsset, все Build вызовы обновлены
- removed MergeFuturesBlock, MergeFuturesConfig, yamlMergeFuturesBlock
- just backtest.merge_overlap_days: > 0 enables merge with that overlap
- adapter/environment/container: mergeOverlapDays int instead of struct
- cleaner YAML: backtest: merge_overlap_days: 3
Simulated stops (backtest/paper): use candle.Close as operation price
instead of trigger.FillPrice (stop.StopLoss +/- 2 ticks). The actual
close from SimulatePostOrder's execution costs is now recorded.

Add missing DecisionRecord for simulated stop closes so they appear in
GET /decisions with Executed=true and the actual fill price.

Broker-detected stops (live): prefer LastSessionMark over
ActiveStop.StopLoss in brokerCloseCandlePrice fallback chain.
- Remove global SimulatedStopSlippageTicks and SetSimulatedStopSlippageTicks
- Add StopSlippageConfig interface to domain (optional, for simulated envs)
- Add StopSlippageTicks to SimulatedExecutionSpec and SimulatedExecutionPolicy
- Add simStopSlippageTicks field to TradingPipelineWrapper + setter
- Wire value: backtest.simulated_execution → BacktestEnvironment → factory → wrapper → DetectTriggeredStop
- Update DetectTriggeredStop to accept ticks parameter (default 2)
- Fix broker close tests: move PnL assertions before SessionAccount (which zeroes
  PendingPnL via resetBrokerTradingDayIfNeededLocked on wall-clock day mismatch)
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feature/futures-resolver:feature/futures-resolver
git switch feature/futures-resolver

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch master
git merge --no-ff feature/futures-resolver
git switch feature/futures-resolver
git rebase master
git switch master
git merge --ff-only feature/futures-resolver
git switch feature/futures-resolver
git rebase master
git switch master
git merge --no-ff feature/futures-resolver
git switch master
git merge --squash feature/futures-resolver
git switch master
git merge --ff-only feature/futures-resolver
git switch master
git merge feature/futures-resolver
git push origin master
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
trading/tradebot-ng!17
No description provided.