"""Unit tests for libs/backtest/snapshot_store.py.""" from __future__ import annotations import datetime as dt import json from pathlib import Path import pyarrow as pa import pyarrow.parquet as pq import pytest def _build_store_from_fixture(tmp_path: Path) -> object: """Build a SnapshotStore directly from test data (no DB/HTTP).""" from libs.backtest.snapshot_store import SnapshotStore candidates = { dt.date(2026, 1, 6): [ { "event_id": "EVT::TEST::001", "symbol": "AAPL", "execution_date": dt.date(2026, 1, 6), "entry_date": "2026-01-06", "entry_price": 150.0, "score": 0.8, "sector": "Technology", "event_type": "earnings", "event_timestamp": "2026-01-05T21:00:00+00:00", "filing_time_bucket": "post_market", "reaction_date": "2026-01-05", "avg_dollar_volume": 5_000_000.0, "atr_14": 3.0, } ], dt.date(2026, 1, 7): [ { "event_id": "EVT::TEST::002", "symbol": "MSFT", "execution_date": dt.date(2026, 1, 7), "entry_date": "2026-01-07", "entry_price": 300.0, "score": 0.6, "sector": "Technology", "event_type": "guidance", "event_timestamp": "2026-01-06T20:00:00+00:00", "filing_time_bucket": "post_market", "reaction_date": "2026-01-06", "avg_dollar_volume": 10_000_000.0, "atr_14": 5.0, } ], } bars = { "AAPL": { dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 150.0, "high": 155.0, "low": 148.0, "close": 152.0, "volume": 1_000_000}, dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 152.0, "high": 162.0, "low": 150.0, "close": 159.0, "volume": 900_000}, }, "MSFT": { dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 300.0, "high": 310.0, "low": 295.0, "close": 305.0, "volume": 500_000}, }, } return SnapshotStore( candidates_by_exec_date=candidates, bars_by_symbol_date=bars, ) class TestSnapshotStoreQuery: def test_get_candidates_for_date(self, tmp_path): store = _build_store_from_fixture(tmp_path) rows = store.get_candidates_for_date(dt.date(2026, 1, 6)) assert len(rows) == 1 assert rows[0]["symbol"] == "AAPL" def test_get_candidates_empty_date(self, tmp_path): store = _build_store_from_fixture(tmp_path) rows = store.get_candidates_for_date(dt.date(2026, 1, 1)) assert rows == [] def test_no_lookahead(self, tmp_path): """Candidates for Jan 7 should NOT appear when querying Jan 6.""" store = _build_store_from_fixture(tmp_path) rows = store.get_candidates_for_date(dt.date(2026, 1, 6)) symbols = [r["symbol"] for r in rows] assert "MSFT" not in symbols def test_get_bar_exists(self, tmp_path): store = _build_store_from_fixture(tmp_path) bar = store.get_bar("AAPL", dt.date(2026, 1, 6)) assert bar is not None assert bar["open"] == 150.0 assert bar["close"] == 152.0 def test_get_bar_missing_returns_none(self, tmp_path): store = _build_store_from_fixture(tmp_path) bar = store.get_bar("AAPL", dt.date(2025, 12, 31)) assert bar is None def test_get_bar_unknown_symbol_returns_none(self, tmp_path): store = _build_store_from_fixture(tmp_path) assert store.get_bar("UNKNOWN", dt.date(2026, 1, 6)) is None def test_all_execution_dates_sorted(self, tmp_path): store = _build_store_from_fixture(tmp_path) dates = store.all_execution_dates() assert dates == sorted(dates) assert dt.date(2026, 1, 6) in dates assert dt.date(2026, 1, 7) in dates def test_macro_default_empty(self, tmp_path): store = _build_store_from_fixture(tmp_path) macro = store.get_macro_for_date(dt.date(2026, 1, 6)) assert macro == {} def test_candidates_copy_returned(self, tmp_path): """Modifying returned list should not affect internal state.""" store = _build_store_from_fixture(tmp_path) rows1 = store.get_candidates_for_date(dt.date(2026, 1, 6)) rows1.append({"extra": "data"}) rows2 = store.get_candidates_for_date(dt.date(2026, 1, 6)) assert len(rows2) == 1 # unchanged class TestSnapshotStoreLoadGuard: def test_raises_in_running_event_loop(self, tmp_path): """load() should raise RuntimeError if called from a running event loop.""" import asyncio from libs.backtest.snapshot_store import SnapshotStore async def _test(): with pytest.raises(RuntimeError, match="running event loop"): SnapshotStore.load(tmp_path, "train", "http://localhost", "postgres://") asyncio.run(_test()) class TestSnapshotStoreFromParquet: def test_compute_date_range(self, tmp_path): from libs.backtest.snapshot_store import SnapshotStore rows = [ {"entry_date": "2026-01-05"}, {"entry_date": "2026-01-10"}, {"entry_date": "2026-01-07"}, ] result = SnapshotStore._compute_date_range(rows) assert result == (dt.date(2026, 1, 5), dt.date(2026, 1, 10)) def test_compute_date_range_empty(self, tmp_path): from libs.backtest.snapshot_store import SnapshotStore assert SnapshotStore._compute_date_range([]) is None