You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
147 lines
5.1 KiB
Python
147 lines
5.1 KiB
Python
"""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,
|
|
)
|