From f0e6a8fc19512dad01fe4220ed9e1d0607b32cf9 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Mon, 13 Jul 2026 10:49:44 +0200 Subject: [PATCH] Isolate explicit S/R gate target ladder --- app/services/backtest_service.py | 11 +++++++++ app/services/sr_service.py | 3 ++- docs/research/sr-levels-and-exits.md | 31 +++++++++++++++++++++++ reports/README.md | 8 ++++++ scripts/run_backtest_snapshot.py | 1 + scripts/run_sr_v2_matrix.py | 37 ++++++++++++++++++++++++++++ tests/unit/test_backtest_service.py | 30 ++++++++++++++++++++++ tests/unit/test_detect_sr_levels.py | 24 ++++++++++++++++++ 8 files changed, 144 insertions(+), 1 deletion(-) diff --git a/app/services/backtest_service.py b/app/services/backtest_service.py index e0a9fa5..6462d14 100644 --- a/app/services/backtest_service.py +++ b/app/services/backtest_service.py @@ -100,6 +100,7 @@ STRUCTURAL_OVERLAY_SOURCE_VARIANT = ( ) STRUCTURAL_OVERLAY_WEIGHT = 0.05 STRUCTURAL_OVERLAY_SCORE_KEY = "structural_overlay_95_5_score" +EXPLICIT_TARGET_LADDER_VARIANT = "explicit_target_ladder" RANGE_RESIDUAL_VARIANTS = { "rewrite_range504_structural_legacy_primary", "rewrite_range504_structural_primary2", @@ -163,6 +164,7 @@ SR_RESEARCH_VARIANTS = { "legacy_traffic_grid_only", "legacy_range_grid_touch", "legacy_range_grid_neutral", + EXPLICIT_TARGET_LADDER_VARIANT, STRUCTURAL_OVERLAY_VARIANT, *RANGE_FACTOR_VARIANTS, } @@ -216,6 +218,7 @@ def _primary_min_rr_for_variant(sr_variant: str, activation: dict) -> float: sr_variant in { "production_control", "production_range504", + EXPLICIT_TARGET_LADDER_VARIANT, STRUCTURAL_OVERLAY_VARIANT, } or sr_variant.endswith("_legacy_primary") @@ -371,6 +374,14 @@ def _window_setups( detected_levels = detect_sr_levels_legacy( highs, lows, closes, volumes, include_pivots=False ) + elif sr_variant == EXPLICIT_TARGET_LADDER_VARIANT: + detected_levels = detect_sr_levels_legacy( + highs, + lows, + closes, + volumes, + explicit_range_grid=True, + ) elif sr_variant in {"legacy_range_grid_touch", "legacy_range_grid_neutral"}: detected_levels = detect_sr_levels_legacy( highs, diff --git a/app/services/sr_service.py b/app/services/sr_service.py index eb90cc6..20e5819 100644 --- a/app/services/sr_service.py +++ b/app/services/sr_service.py @@ -451,8 +451,9 @@ def detect_sr_levels_legacy( if explicit_range_grid else _legacy_volume_profile_nodes(highs, lows, closes, volumes) ) + method = "range_grid" if explicit_range_grid else "volume_profile" for price in nodes: - candidates.append((float(price), "volume_profile")) + candidates.append((float(price), method)) except ValidationError: pass if include_pivots: diff --git a/docs/research/sr-levels-and-exits.md b/docs/research/sr-levels-and-exits.md index 5c6139e..fe0c344 100644 --- a/docs/research/sr-levels-and-exits.md +++ b/docs/research/sr-levels-and-exits.md @@ -675,6 +675,37 @@ at least 90% of production CAGR. The 1-year and 6-month rows must not both deteriorate. This remains contaminated full-history research, not promotion validation. +Result: **reject the overlay**. Production parity is exact, but the overlay +reduces full-period Sharpe from 2.03 to 2.00 and CAGR from 50.0% to 48.4%; max +drawdown improves from 21.4% to 20.0%. Confirmation has real standalone quality +(+0.263R ex-top-5% versus +0.024R), but little marginal ranking value: confirmed +setups already average production rank 92.3 and 46.1% come from ten symbols. The +5% boost changes ordering on only 45 active dates and overweights the same +concentrated names. + +### Explicit gate target ladder + +The legacy detector's volume-profile label is misleading. It returns both HVN +and LVN bins, whose union is the complete 20-bin price-range grid, then adds +unfiltered pivots and touch strength. For the gate this behaves as a broad +target-proposal ladder, not human-facing support/resistance. + +The `explicit_target_ladder` arm therefore replaces only the irrelevant volume +pass with the complete range grid. It retains pivots, touch strength, merge +geometry, primary selection, qualification, ranking, and exit behavior. Grid +levels are labelled `range_grid`, making the internal purpose explicit. + +```bash +.venv/bin/python scripts/run_sr_v2_matrix.py ladder --workers 14 +``` + +The arm advances only on exact parity with the full-period production control: +no added or removed qualified setups and identical production-book Sharpe, +CAGR, drawdown, and trade count. Passing parity supports a dual-purpose design: +clean structure for charts and alerts, explicit target ladder for the gate. +Failing parity means the supposedly irrelevant volume pass still affects an +edge case and must be located before any architectural change. + The post-2024 window has been opened and is now analysis data, not a valid final promotion holdout. These arms can isolate mechanism, but neither may ship without new future data or a separately pre-registered walk-forward protocol. diff --git a/reports/README.md b/reports/README.md index c2e0d1d..d634121 100644 --- a/reports/README.md +++ b/reports/README.md @@ -46,3 +46,11 @@ The follow-up breadth-preserving ranking experiment writes `backtest-sr-overlay-full.json`. Retain it only as the decision point for the pre-registered 5% clean-structure overlay; exploratory weight sweeps should not be committed. + +The explicit gate-ladder parity run writes: + +- `backtest-sr-full-explicit_target_ladder.json` +- `sr-explicit-target-ladder-comparison.json` +- `sr-explicit-target-ladder-cohorts.csv` + +Retain these only if they establish the final parity decision. diff --git a/scripts/run_backtest_snapshot.py b/scripts/run_backtest_snapshot.py index 5cf0f01..f87bb67 100644 --- a/scripts/run_backtest_snapshot.py +++ b/scripts/run_backtest_snapshot.py @@ -60,6 +60,7 @@ def _parse_args() -> argparse.Namespace: "rewrite_range504_structural_legacy_primary", "rewrite_range504_structural_primary2", "production_structural_overlay", + "explicit_target_ladder", ), 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 6f64236..f66639b 100644 --- a/scripts/run_sr_v2_matrix.py +++ b/scripts/run_sr_v2_matrix.py @@ -105,6 +105,11 @@ def _args() -> argparse.Namespace: help="Test the frozen 5% clean-structure rank overlay on production breadth.", ) _add_common(overlay) + ladder = commands.add_parser( + "ladder", + help="Verify the explicit volume-free gate target ladder against production.", + ) + _add_common(ladder) validate = commands.add_parser( "validate", help="Run production control and one locked arm from 2024-07-01.", @@ -225,6 +230,36 @@ def _overlay(args: argparse.Namespace) -> None: print("Full-period structural ranking overlay complete.") +def _ladder(args: argparse.Namespace) -> None: + control = ROOT / "reports" / "backtest-sr-full-production_control.json" + if not control.exists(): + raise SystemExit(f"Full-period production control not found: {control}") + variant = ROOT / "reports" / "backtest-sr-full-explicit_target_ladder.json" + _run_arm( + "explicit_target_ladder", + args.snapshot, + args.workers, + output=variant, + ) + subprocess.run( + [ + sys.executable, + str(COMPARE), + str(control), + str(variant), + "--out-csv", str( + ROOT / "reports" / "sr-explicit-target-ladder-cohorts.csv" + ), + "--out-json", str( + ROOT / "reports" / "sr-explicit-target-ladder-comparison.json" + ), + ], + cwd=ROOT, + check=True, + ) + print("Explicit target-ladder production parity comparison complete.") + + def main() -> None: args = _args() if args.command == "train": @@ -242,6 +277,8 @@ def main() -> None: _full(args) elif args.command == "overlay": _overlay(args) + elif args.command == "ladder": + _ladder(args) else: _validate(args) diff --git a/tests/unit/test_backtest_service.py b/tests/unit/test_backtest_service.py index 5603f9d..d9eaa36 100644 --- a/tests/unit/test_backtest_service.py +++ b/tests/unit/test_backtest_service.py @@ -802,6 +802,7 @@ def test_sr_research_variant_is_explicit_and_validated(monkeypatch): "rewrite_range504_structural_legacy_primary", "rewrite_range504_structural_primary2", "production_structural_overlay", + "explicit_target_ladder", ): monkeypatch.setenv("BACKTEST_SR_VARIANT", variant) assert bt._sr_research_variant() == variant @@ -856,6 +857,10 @@ def test_residual_arms_change_only_the_primary_rr_floor(): "production_structural_overlay", activation, ) == 1.5 + assert bt._primary_min_rr_for_variant( + "explicit_target_ladder", + activation, + ) == 1.5 def test_structural_overlay_tags_production_geometry_without_replacing_it(monkeypatch): @@ -934,6 +939,31 @@ def test_window_setups_routes_explicit_range_grid( } +def test_window_setups_routes_full_explicit_target_ladder(monkeypatch): + captured = {} + + def fake_detector(*args, **kwargs): + captured.update(kwargs) + return [] + + monkeypatch.setenv("BACKTEST_SR_VARIANT", bt.EXPLICIT_TARGET_LADDER_VARIANT) + monkeypatch.setattr(bt, "detect_sr_levels_legacy", fake_detector) + records = [ + SimpleNamespace( + date=date(2024, 1, 1) + timedelta(days=i), + open=100.0, + high=101.0, + low=99.0, + close=100.0, + volume=1_000_000, + ) + for i in range(bt.MIN_LOOKBACK) + ] + + assert bt._window_setups(records, {}, {}) == [] + assert captured == {"explicit_range_grid": True} + + @pytest.mark.parametrize( "variant", ["legacy_geometry_neutral", "legacy_range_grid_neutral"], diff --git a/tests/unit/test_detect_sr_levels.py b/tests/unit/test_detect_sr_levels.py index 8428443..906218f 100644 --- a/tests/unit/test_detect_sr_levels.py +++ b/tests/unit/test_detect_sr_levels.py @@ -277,6 +277,30 @@ class TestDetectSrLevels: assert sorted(deployed) == explicit assert len(explicit) == 20 + def test_explicit_target_ladder_preserves_full_legacy_mechanics(self): + highs, lows, closes, volumes = _make_series(n=500) + deployed = detect_sr_levels_legacy(highs, lows, closes, volumes) + explicit = detect_sr_levels_legacy( + highs, + lows, + closes, + volumes, + explicit_range_grid=True, + ) + + def mechanics(level): + return { + key: value + for key, value in level.items() + if key not in {"detection_method", "sources"} + } + + assert [mechanics(level) for level in explicit] == [ + mechanics(level) for level in deployed + ] + assert any("range_grid" in level["sources"] for level in explicit) + assert all("volume_profile" not in level["sources"] for level in explicit) + def test_explicit_range_grid_is_volume_independent(self): highs, lows, closes, volumes = _make_series(n=500) shifted_volumes = [volume * (i + 1) for i, volume in enumerate(volumes)]