"""Replay test: same fixture → same parser output (determinism).""" import pytest SAMPLE_TEXT = """ Item 2.02 Results of Operations Apple today announced record quarterly revenue of $124.3 billion. Guidance raised for Q2. Demand remains strong. Margin expansion driven by Services. Customer additions in enterprise continue. Adjusted EPS excludes non-GAAP items. """ @pytest.mark.replay def test_parser_determinism(): """Running parser twice on same text yields identical output.""" from libs.parser.rule_parser import RuleBasedParser p = RuleBasedParser() metadata = {"filing_date": "2026-01-29", "accepted_at_utc": "2026-01-29T21:05:00Z"} out1 = p.parse("DOC::test", "8-K", SAMPLE_TEXT, metadata) out2 = p.parse("DOC::test", "8-K", SAMPLE_TEXT, metadata) assert out1.event_type == out2.event_type assert out1.event_direction == out2.event_direction assert out1.guidance.status == out2.guidance.status assert out1.confidence.overall == out2.confidence.overall assert out1.filing_time_bucket == out2.filing_time_bucket @pytest.mark.replay def test_parser_determinism_negative_text(): """Determinism holds for negative/mixed text too.""" from libs.parser.rule_parser import RuleBasedParser negative_text = """ Item 2.02 Results of Operations Revenue below expectations. Guidance lowered. Demand softness observed. Convertible note offering announced. Margin compression continues. """ p = RuleBasedParser() metadata = {"filing_date": "2026-02-01"} out1 = p.parse("DOC::test2", "8-K", negative_text, metadata) out2 = p.parse("DOC::test2", "8-K", negative_text, metadata) assert out1.event_type == out2.event_type assert out1.guidance.status == out2.guidance.status @pytest.mark.replay def test_feature_determinism(sample_parser_output): """Event features are deterministic for same parser output.""" from libs.features.event_features import compute_event_features f1 = compute_event_features(sample_parser_output) f2 = compute_event_features(sample_parser_output) assert f1 == f2 # --------------------------------------------------------------------------- # Backtest determinism tests # --------------------------------------------------------------------------- def _build_synthetic_store(): """Reusable synthetic SnapshotStore for backtest replay tests.""" import datetime as dt from zoneinfo import ZoneInfo from libs.backtest.snapshot_store import SnapshotStore candidates = { dt.date(2026, 1, 5): [ { "event_id": "EVT::REPLAY::001", "symbol": "AAPL", "execution_date": dt.date(2026, 1, 5), "entry_date": "2026-01-05", "entry_price": 150.0, "score": 0.85, "sector": "Technology", "event_type": "earnings", "event_timestamp": "2026-01-02T21:00:00+00:00", "filing_time_bucket": "post_market", "reaction_date": "2026-01-02", "avg_dollar_volume": 5_000_000.0, "atr_14": 3.0, } ], } bars = { "AAPL": { dt.date(2026, 1, 5): { "date": dt.date(2026, 1, 5), "open": 150.0, "high": 160.0, "low": 148.0, "close": 157.0, "volume": 1_000_000, }, dt.date(2026, 1, 6): { "date": dt.date(2026, 1, 6), "open": 157.0, "high": 168.0, "low": 155.0, "close": 165.0, "volume": 900_000, }, } } return SnapshotStore(candidates_by_exec_date=candidates, bars_by_symbol_date=bars) def _make_backtest_config(): from libs.backtest.domain import ( BacktestConfig, ExecutionConfig, ReportingConfig, RiskConfig, SignalConfig, UniverseConfig, ) return BacktestConfig( strategy_name="replay_test", dataset_snapshot_id="test_snapshot", universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000), signal=SignalConfig(score_threshold=0.5, max_candidates_per_day=5), risk=RiskConfig( per_trade_risk_pct=0.01, max_daily_new_risk_pct=0.05, max_positions=10, max_positions_per_sector=5, ), execution=ExecutionConfig( entry_fill_model="next_open", exit_fill_model="daily_bar_approximation", slippage_bps_base=10.0, commission_per_share=0.005, same_bar_priority="stop_first_conservative", max_holding_days=10, ), reporting=ReportingConfig( write_trade_blotter=False, write_equity_curve=False, write_metrics_summary=False, ), ) @pytest.mark.replay def test_backtest_determinism(): """Running the backtest twice on the same synthetic store yields identical metrics.""" from apps.backtester.run import BacktestRunner from libs.backtest.domain import ExperimentManifest manifest = ExperimentManifest( experiment_name="replay_test", dataset_snapshot_id="test_snapshot", base_config="configs/backtest/defaults.json", overrides={}, ) config = _make_backtest_config() store1 = _build_synthetic_store() runner1 = BacktestRunner(manifest=manifest, config=config, store=store1, initial_equity=100_000.0) result1 = runner1.run() store2 = _build_synthetic_store() runner2 = BacktestRunner(manifest=manifest, config=config, store=store2, initial_equity=100_000.0) result2 = runner2.run() # Core metrics must be identical assert result1.metrics.trade_count == result2.metrics.trade_count assert result1.metrics.win_rate == result2.metrics.win_rate assert result1.metrics.total_return_pct == result2.metrics.total_return_pct assert result1.metrics.max_drawdown_pct == result2.metrics.max_drawdown_pct assert result1.total_candidates_seen == result2.total_candidates_seen assert result1.total_orders_rejected == result2.total_orders_rejected assert result1.total_trading_days == result2.total_trading_days @pytest.mark.replay def test_backtest_selector_determinism(): """Selector ranking is deterministic across multiple calls.""" from libs.backtest.domain import SignalConfig, UniverseConfig from libs.backtest.selector import select_candidates rows = [ { "event_id": f"EVT::{'ABCDE'[i]}", "symbol": "ABCDE"[i], "entry_date": "2026-01-05", "entry_price": 100.0 + i, "score": 0.9 - i * 0.05, "sector": "Technology", "event_type": "earnings", "event_timestamp": "2026-01-02T21:00:00+00:00", "filing_time_bucket": "post_market", "reaction_date": "2026-01-02", "avg_dollar_volume": 5_000_000.0 + i * 100_000, "atr_14": 2.0, } for i in range(5) ] u = UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000) s = SignalConfig(score_threshold=0.5, max_candidates_per_day=10) result1 = select_candidates(rows, u, s) result2 = select_candidates(rows, u, s) assert [c.symbol for c in result1] == [c.symbol for c in result2]