from __future__ import annotations import datetime as dt from pathlib import Path from zoneinfo import ZoneInfo from apps.backtester.run import BacktestRunner, _extend_store_to_requested_window from libs.backtest.domain import FilledTrade from libs.backtest.domain import BacktestConfig from libs.backtest.domain import Candidate, ExitReason, OpenPosition, PlannedOrder from libs.backtest.snapshot_store import SnapshotStore def _make_config() -> BacktestConfig: config = BacktestConfig( strategy_name="test_strategy", dataset_snapshot_id="snap", ) config.risk.cash_parking_enabled = True config.risk.cash_parking_preset = None return config def test_extend_store_to_requested_window_backfills_macro_prefix(monkeypatch, tmp_path: Path) -> None: snapshot_dir = tmp_path / "snap" snapshot_dir.mkdir(parents=True) (snapshot_dir / "manifest.json").write_text("{}") (snapshot_dir / "train.parquet").write_text("x") (snapshot_dir / "valid.parquet").write_text("x") (snapshot_dir / "test.parquet").write_text("x") store = SnapshotStore( candidates_by_exec_date={}, bars_by_symbol_date={}, macro_by_date={ dt.date(2022, 3, 1): {"spy_close": 430.0, "qqqm_close": 153.0, "sgov_close": 100.0}, dt.date(2022, 3, 2): {"spy_close": 431.0, "qqqm_close": 154.0, "sgov_close": 100.0}, }, ) async def _fake_fetch_macro(date_range, _db_dsn): assert date_range == (dt.date(2022, 1, 1), dt.date(2022, 2, 28)) return { dt.date(2022, 1, 3): {"VIXCLS": 20.0}, dt.date(2022, 1, 4): {"VIXCLS": 21.0}, } async def _fake_fetch_spy_macro(date_range, _oracle_url): assert date_range == (dt.date(2022, 1, 1), dt.date(2022, 2, 28)) return { dt.date(2022, 1, 3): {"spy_close": 470.0, "qqqm_close": 180.0, "sgov_close": 100.1}, dt.date(2022, 1, 4): {"spy_close": 471.0, "qqqm_close": 180.5, "sgov_close": 100.1}, } monkeypatch.setattr("apps.backtester.run.resolve_snapshot_path", lambda *_args, **_kwargs: snapshot_dir) monkeypatch.setattr(SnapshotStore, "_fetch_macro", staticmethod(_fake_fetch_macro)) monkeypatch.setattr(SnapshotStore, "_fetch_spy_macro", staticmethod(_fake_fetch_spy_macro)) extended = _extend_store_to_requested_window( store=store, config=_make_config(), start_date=dt.date(2022, 1, 1), end_date=dt.date(2022, 4, 1), ) assert min(extended._macro) == dt.date(2022, 1, 3) assert extended.all_trading_days()[0] == dt.date(2022, 1, 3) assert extended.get_macro_for_date(dt.date(2022, 1, 3))["qqqm_close"] == 180.0 def test_extend_store_to_requested_window_reuses_cached_macro_window(monkeypatch, tmp_path: Path) -> None: snapshot_dir = tmp_path / "snap" snapshot_dir.mkdir(parents=True) (snapshot_dir / "manifest.json").write_text("{}") (snapshot_dir / "train.parquet").write_text("x") (snapshot_dir / "valid.parquet").write_text("x") (snapshot_dir / "test.parquet").write_text("x") cache_file = snapshot_dir / "macro_window_2022-01-01_2022-02-28.pkl" cached_store = SnapshotStore( candidates_by_exec_date={}, bars_by_symbol_date={}, macro_by_date={ dt.date(2022, 3, 1): {"spy_close": 430.0, "qqqm_close": 153.0, "sgov_close": 100.0}, }, ) async def _initial_fetch_macro(date_range, _db_dsn): return {dt.date(2022, 1, 3): {"VIXCLS": 20.0}} async def _initial_fetch_spy_macro(date_range, _oracle_url): return {dt.date(2022, 1, 3): {"spy_close": 470.0, "qqqm_close": 180.0, "sgov_close": 100.1}} monkeypatch.setattr("apps.backtester.run.resolve_snapshot_path", lambda *_args, **_kwargs: snapshot_dir) monkeypatch.setattr(SnapshotStore, "_fetch_macro", staticmethod(_initial_fetch_macro)) monkeypatch.setattr(SnapshotStore, "_fetch_spy_macro", staticmethod(_initial_fetch_spy_macro)) _extend_store_to_requested_window( store=cached_store, config=_make_config(), start_date=dt.date(2022, 1, 1), end_date=dt.date(2022, 2, 28), ) assert cache_file.exists() store = SnapshotStore( candidates_by_exec_date={}, bars_by_symbol_date={}, macro_by_date={ dt.date(2022, 3, 1): {"spy_close": 430.0, "qqqm_close": 153.0, "sgov_close": 100.0}, }, ) async def _unexpected_fetch_macro(*_args, **_kwargs): raise AssertionError("macro fetch should not run when cached macro window exists") async def _unexpected_fetch_spy_macro(*_args, **_kwargs): raise AssertionError("spy macro fetch should not run when cached macro window exists") monkeypatch.setattr(SnapshotStore, "_fetch_macro", staticmethod(_unexpected_fetch_macro)) monkeypatch.setattr(SnapshotStore, "_fetch_spy_macro", staticmethod(_unexpected_fetch_spy_macro)) extended = _extend_store_to_requested_window( store=store, config=_make_config(), start_date=dt.date(2022, 1, 1), end_date=dt.date(2022, 2, 28), ) assert min(extended._macro) == dt.date(2022, 1, 3) assert extended.get_macro_for_date(dt.date(2022, 1, 3))["sgov_close"] == 100.1 def test_backtest_runner_uses_requested_window_for_simulation_dates() -> None: store = SnapshotStore( candidates_by_exec_date={ dt.date(2022, 3, 3): [{"event_id": "evt", "symbol": "AAPL"}], }, bars_by_symbol_date={}, macro_by_date={ dt.date(2022, 1, 3): {"spy_close": 470.0, "qqqm_close": 180.0, "sgov_close": 100.1}, dt.date(2022, 3, 3): {"spy_close": 430.0, "qqqm_close": 153.0, "sgov_close": 100.0}, }, ) setattr(store, "_requested_start_date", dt.date(2022, 1, 3)) setattr(store, "_requested_end_date", dt.date(2022, 3, 4)) runner = BacktestRunner.__new__(BacktestRunner) runner.store = store runner.config = _make_config() runner._active_strategy_engines = [] simulation_dates = runner._get_simulation_dates() assert simulation_dates[0] == dt.date(2022, 1, 3) assert simulation_dates[-1] == dt.date(2022, 3, 4) def test_record_parking_trade_keeps_actual_sgov_share_count() -> None: runner = BacktestRunner.__new__(BacktestRunner) runner._parking_entry_date = dt.date(2022, 1, 3) runner._parking_trade_counter = 0 runner._closed_trades = [] runner._record_parking_trade( exit_date=dt.date(2022, 4, 12), symbol="sgov", shares=117, entry_price=85.16, exit_price=85.19, ) assert len(runner._closed_trades) == 1 trade: FilledTrade = runner._closed_trades[0] assert trade.symbol == "SGOV" assert trade.shares == 117 assert round(trade.net_pnl, 2) == round((85.19 - 85.16) * 117, 2) def test_force_close_all_uses_requested_end_bar_and_end_of_backtest_reason() -> None: utc = ZoneInfo("UTC") candidate = Candidate( event_id="EVT::TEST", symbol="ENB", score=0.9, sector="Energy", event_type="earnings", event_timestamp=dt.datetime(2026, 3, 23, 21, 0, tzinfo=utc), event_date=dt.date(2026, 3, 23), filing_time_bucket="post_market", reaction_date=dt.date(2026, 3, 24), execution_date=dt.date(2026, 3, 25), entry_price_est=52.9, avg_dollar_volume=10_000_000.0, atr_14=1.2, score_bucket="high", ) plan = PlannedOrder( candidate=candidate, shares=100, entry_price_limit=52.9, stop_price=50.0, target_price=60.0, risk_dollars=290.0, ) position = OpenPosition( position_id="pos-1", plan=plan, entry_date=dt.date(2026, 3, 25), entry_price=52.9, entry_fill_slippage_bps=10.0, current_stop=50.0, target_price=60.0, peak_price=53.5, shares_open=100, shares_total=100, ) store = SnapshotStore( candidates_by_exec_date={dt.date(2026, 3, 31): [{"event_id": "evt"}]}, bars_by_symbol_date={ "ENB": { dt.date(2026, 3, 31): {"close": 53.75}, dt.date(2026, 4, 1): {"close": 54.25}, } }, macro_by_date={ dt.date(2026, 3, 31): {"spy_close": 560.0}, dt.date(2026, 4, 1): {"spy_close": 562.0}, }, ) runner = BacktestRunner.__new__(BacktestRunner) runner.store = store runner.config = _make_config() runner._open_positions = [position] runner._closed_trades = [] runner._candidate_map = {} runner._realized_pnl = 0.0 runner._cash = 0.0 runner._force_close_all(dt.date(2026, 4, 1), reason="end_of_backtest") assert not runner._open_positions assert len(runner._closed_trades) == 1 trade = runner._closed_trades[0] assert trade.exit_date == dt.date(2026, 4, 1) assert trade.exit_reason == ExitReason.END_OF_BACKTEST assert trade.exit_price > 54.0