Futures Rollover #17

Open
derfenix wants to merge 31 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)
- Add multi-ID-type resolution (FIGI/UID) in instrument_resolve.go based on
  instrument ID prefix (BBG→FIGI, FUT→FIGI+UID fallback, RU/UUID→UID).
- Extract FuturesSeriesPrefix into core/domain for reuse across resolver,
  merge, and rollover; add FilterFuturesSameSeries to exclude mini/alternate
  series with the same basic_asset.
- Resolve Instrument for near/next contracts in futuresresolver.Service
  so rollover controller can build orders; fix forceRollover ordering.
- Refactor backtest merge: extract DiscoverRelevant/RelevantContracts into
  contracts.go, apply FilterFuturesSameSeries before window filter.
- Add ExtendCandleCacheForFuturesMerge and WarmInstrumentResolve helpers
  for pre-caching merge candles and instrument metadata.
- Delegate FuturesProvider methods in CachedCandlesProvider.
- Increase resolve cache TTL to 24h (stable metadata, long GA sessions).
- Remove legacy PnL migration layer (pnl_migrate.go), drop v1-v3 support
  in live_state and paper_state, remove legacy paper filename fallback.
- Keep stopPricesInPoints=true for futures (prices in points).
- Update AGENTS.md with new conventions.
Commenting is not possible because the repository is archived.
No description provided.