"""Assembles a synthetic SnapshotStore from a ScenarioConfig. Orchestrates the full synthetic data generation pipeline: 1. Trading dates (real NYSE calendar) 2. Market ETF price paths (SPY, QQQ) 3. Individual stock price paths (correlated with market) 4. Macro indicators (VIX, HY spread, SPY/QQQ rolling stats) 5. Event candidates (parameterized feature distributions) 6. Event-price coupling (signal-to-noise injection) 7. SnapshotStore assembly """ from __future__ import annotations import datetime as dt from typing import Any import numpy as np from libs.backtest.scenarios.coupling import couple_events_to_prices from libs.backtest.scenarios.event_gen import generate_events from libs.backtest.scenarios.macro_gen import generate_macro_data from libs.backtest.scenarios.price_gen import generate_market_etf_paths, generate_price_paths from libs.backtest.scenarios.scenarios import ScenarioConfig from libs.backtest.snapshot_store import SnapshotStore # Starting dates for synthetic scenarios (real NYSE calendar) # Using a date range that's safely after 2020 (no pandemic-era data issues) _SCENARIO_START_DATE = dt.date(2024, 1, 2) def _get_trading_dates(n_days: int, start: dt.date = _SCENARIO_START_DATE) -> list[dt.date]: """Return n_days consecutive NYSE trading dates starting from start.""" from libs.backtest.calendar import get_trading_days # Request a window of n_days * 1.5 calendar days to account for weekends/holidays end_estimate = start + dt.timedelta(days=int(n_days * 1.5) + 30) all_days = get_trading_days(start, end_estimate) return all_days[:n_days] def _total_regime_days(scenario: ScenarioConfig) -> int: """Sum of all regime durations in the scenario.""" return sum(r.duration_days for r in scenario.price_regimes) def build_synthetic_store( scenario: ScenarioConfig, rng: np.random.Generator | None = None, ) -> SnapshotStore: """Assemble a complete synthetic SnapshotStore from a ScenarioConfig. The returned store has no connection to real market data. It can be passed directly to BacktestRunner without any Oracle API or database. Args: scenario: Fully specified synthetic market scenario. rng: NumPy random generator. If None, uses scenario.seed or random state. Returns: SnapshotStore ready for BacktestRunner.run(). """ if rng is None: seed = scenario.seed rng = np.random.default_rng(seed) # 1. Trading dates n_days = _total_regime_days(scenario) + 30 # extra buffer for parking tail trading_dates = _get_trading_dates(n_days) # 2. Market ETF paths (SPY, QQQ) + market log-returns for macro correlation spy_bars, qqq_bars, market_log_rets = generate_market_etf_paths( regimes=scenario.price_regimes, trading_dates=trading_dates, rng=rng, ) # 3. Individual stock paths n_sym = scenario.n_symbols tickers = [f"SYN{i:03d}" for i in range(n_sym)] bars_by_symbol = generate_price_paths( n_symbols=n_sym, initial_prices=None, regimes=scenario.price_regimes, trading_dates=trading_dates, market_beta_range=(0.5, 1.4), rng=rng, tickers=tickers, ) assert isinstance(bars_by_symbol, dict), "generate_price_paths must return dict" # Merge ETF bars in as well (for parking lookups) bars_by_symbol["SPY"] = spy_bars bars_by_symbol["QQQ"] = qqq_bars # 4. Macro data macro_by_date = generate_macro_data( spy_bars=spy_bars, qqq_bars=qqq_bars, vix_config=scenario.vix_config, hy_config=scenario.hy_config, trading_dates=trading_dates, rng=rng, market_log_rets=market_log_rets, ) # 5. Event candidates candidates_by_exec_date = generate_events( trading_dates=trading_dates, symbols=tickers, dist=scenario.event_distribution, rng=rng, bars_by_symbol=bars_by_symbol, ) # 5b. Inject macro_vix / macro_hy_spread into each candidate row. # selector._row_matches_strategy_engine_filters() reads macro_vix and # macro_hy_spread directly from the row dict (not from macro_by_date), # so we must populate them here. for exec_date, rows in candidates_by_exec_date.items(): macro = macro_by_date.get(exec_date, {}) mv = macro.get("macro_vix") hy = macro.get("macro_hy_spread") for row in rows: if mv is not None: row["macro_vix"] = mv if hy is not None: row["macro_hy_spread"] = hy # 6. Event-price coupling (signal injection) couple_events_to_prices( candidates=candidates_by_exec_date, bars_by_symbol=bars_by_symbol, trading_dates=trading_dates, signal_strength=scenario.signal_strength, signal_decay_days=scenario.signal_decay_days, false_positive_rate=scenario.false_positive_rate, rng=rng, ) # 7. Assemble SnapshotStore return SnapshotStore( candidates_by_exec_date=candidates_by_exec_date, bars_by_symbol_date=bars_by_symbol, macro_by_date=macro_by_date, )