diff --git a/research/experiments/replay.py b/research/experiments/replay.py index 9816f11..2ce66b0 100644 --- a/research/experiments/replay.py +++ b/research/experiments/replay.py @@ -157,7 +157,8 @@ def reconstruct_fires(store, since: str | None = None) -> list[Fire]: def reconstruct_fires_from_setups(store, since: str | None = None, - limit: int | None = None) -> list[Fire]: + limit: int | None = None, + setup_version: str | None = None) -> list[Fire]: """Simulated fires from the labeler's setup_events — the 18k-strong backtestable ORB universe, vs the handful of LIVE round-trips. Each setup carries the ORB entry (entry_reference_price) and stop (invalidation_price); the exit @@ -173,6 +174,12 @@ def reconstruct_fires_from_setups(store, since: str | None = None, if since: q += " AND session_date >= ?" args.append(since) + if setup_version: + # scope to one track — the /backtest replay judges the LIVE strategy + # (v1) only; shadow research tracks (liq1/vr1/run1/pm1) share this + # table and would dominate 8:1 with tight-stop artifacts. + q += " AND setup_version = ?" + args.append(setup_version) q += " ORDER BY session_date, setup_time" if limit: q += f" LIMIT {int(limit)}" @@ -654,7 +661,7 @@ def replay_window(store, since, until, *, population_cfg=None, harness) — assembly only, no new simulation logic.""" fidelity = fidelity or {"atr_frac": 0.25, "spread_mult": 3, "adverse_ticks": 1} until_d = _to_date(until) - fires = [f for f in reconstruct_fires_from_setups(store, since=str(since)) + fires = [f for f in reconstruct_fires_from_setups(store, since=str(since), setup_version="v1") if f.session_date <= until_d] screened_population = 0 diff --git a/tests/unit/api/test_backtest_endpoint.py b/tests/unit/api/test_backtest_endpoint.py index 5b4f04a..e4a23f6 100644 --- a/tests/unit/api/test_backtest_endpoint.py +++ b/tests/unit/api/test_backtest_endpoint.py @@ -225,3 +225,22 @@ def test_backtest_route_serves_html(): if __name__ == "__main__": # pragma: no cover pytest.main([__file__, "-q"]) + + +def test_replay_window_replays_live_track_only(): + """/backtest must judge the strategy that actually trades: setup_version v1. + Shadow research tracks (liq1/vr1/run1/pm1 -- refuted, never traded live) + share setup_events and would otherwise dominate the replay 8:1 with + tight-stop artifacts (the 2026-07-17 -1.8R/trade garbage-scale bug).""" + store = _seed_store() + # a liq1 shadow fire on the same session/bars -- must NOT be replayed + store.con.execute( + "INSERT INTO setup_events (setup_id, symbol, session_date, setup_time, " + "setup_name, setup_version, entry_reference_price, invalidation_price) " + "VALUES (?,?,?,?,?,?,?,?)", + [f"liq-AAA-{SESS1}", "AAA", SESS1, + datetime(SESS1.year, SESS1.month, SESS1.day, 13, 35), + "liquid_breakout", "liq1", 10.0, 9.0]) + from research.experiments.replay import replay_window + out = replay_window(store, str(SESS1), str(SESS2)) + assert len(out["trades"]) == 4 # the 4 v1 fires only; liq1 excluded