diff --git a/app/services/backtest_service.py b/app/services/backtest_service.py index 03995eb..f0eaff1 100644 --- a/app/services/backtest_service.py +++ b/app/services/backtest_service.py @@ -137,6 +137,10 @@ SR_RESEARCH_VARIANTS = { "soft_zones", "confirmed_rounds", "gate_v2", + "rewrite_legacy_primary", + "soft_zones_legacy_primary", + "confirmed_rounds_legacy_primary", + "gate_v2_legacy_primary", } @@ -268,10 +272,11 @@ def _window_setups( return [] sr_variant = _sr_research_variant() - if sr_variant in {"production_control", "rr_aligned_control"}: + detector_variant = sr_variant.removesuffix("_legacy_primary") + if detector_variant in {"production_control", "rr_aligned_control"}: detected_levels = detect_sr_levels_legacy(highs, lows, closes, volumes) else: - detector_cap = 0 if sr_variant == "gate_v2" else MAX_LEVELS + detector_cap = 0 if detector_variant == "gate_v2" else MAX_LEVELS detected_levels = detect_sr_levels( highs, lows, closes, volumes, max_levels=detector_cap ) @@ -281,11 +286,11 @@ def _window_setups( gate_levels = _gate_eligible_levels( sr_levels, - confirmed_rounds_only=sr_variant in {"confirmed_rounds", "gate_v2"}, + confirmed_rounds_only=detector_variant in {"confirmed_rounds", "gate_v2"}, ) zone_strength_mode = ( "soft" - if sr_variant in {"soft_zones", "confirmed_rounds", "gate_v2"} + if detector_variant in {"soft_zones", "confirmed_rounds", "gate_v2"} else "sum" ) @@ -326,7 +331,7 @@ def _window_setups( targets = _prune_floor_pinned_targets(targets) primary_min_rr = ( 1.5 - if sr_variant == "production_control" + if sr_variant == "production_control" or sr_variant.endswith("_legacy_primary") else float(activation.get("min_rr", 0.0)) ) primary = _select_primary_target( diff --git a/docs/research/sr-levels-and-exits.md b/docs/research/sr-levels-and-exits.md index 12dcdba..d27da95 100644 --- a/docs/research/sr-levels-and-exits.md +++ b/docs/research/sr-levels-and-exits.md @@ -436,6 +436,17 @@ calls `scripts/compare_sr_variants.py` to produce the paired cohort CSV and JSON > the test period. The simulator now treats `BACKTEST_ENTRY_END` as an inclusive > entry bound and truncates the calendar after the final position can resolve. +The isolated `rr_aligned_control` validation also failed (Sharpe 2.78 to 1.32, +CAGR 73.3% to 33.1%, drawdown 11.7% to 18.4%). The live 1.5 primary-selection +behavior is therefore frozen: although it predates the 2.0 activation gate, it +acts as a useful selectivity mechanism. The final detector matrix holds that +behavior constant and varies only detection/zone policy: + +- `rewrite_legacy_primary` +- `soft_zones_legacy_primary` +- `confirmed_rounds_legacy_primary` +- `gate_v2_legacy_primary` + The post-2024 interval has informed earlier research, so this is validation rather than a pristine holdout; do not sweep variants on it. No deployment follows automatically. A lower validation Sharpe or higher drawdown remains a no-ship diff --git a/scripts/run_backtest_snapshot.py b/scripts/run_backtest_snapshot.py index 859d280..3f5953e 100644 --- a/scripts/run_backtest_snapshot.py +++ b/scripts/run_backtest_snapshot.py @@ -51,6 +51,8 @@ def _parse_args() -> argparse.Namespace: choices=( "production_control", "rr_aligned_control", "rewrite", "soft_zones", "confirmed_rounds", "gate_v2", + "rewrite_legacy_primary", "soft_zones_legacy_primary", + "confirmed_rounds_legacy_primary", "gate_v2_legacy_primary", ), default=None, help="Research-only S/R detector/gate arm.", diff --git a/scripts/run_sr_v2_matrix.py b/scripts/run_sr_v2_matrix.py index f5f4286..eba9df4 100644 --- a/scripts/run_sr_v2_matrix.py +++ b/scripts/run_sr_v2_matrix.py @@ -17,17 +17,12 @@ RUNNER = ROOT / "scripts" / "run_backtest_snapshot.py" COMPARE = ROOT / "scripts" / "compare_sr_variants.py" TRAINING_ARMS = ( "production_control", - "rr_aligned_control", - "rewrite", - "soft_zones", - "confirmed_rounds", - "gate_v2", -) -# confirmed_rounds failed the post-2024 validation decisively and must not be -# selected again merely because a corrected training curve looks attractive. -LOCKABLE_ARMS = tuple( - arm for arm in TRAINING_ARMS[1:] if arm != "confirmed_rounds" + "rewrite_legacy_primary", + "soft_zones_legacy_primary", + "confirmed_rounds_legacy_primary", + "gate_v2_legacy_primary", ) +LOCKABLE_ARMS = TRAINING_ARMS[1:] def _add_common(parser: argparse.ArgumentParser) -> None: