From e31d1704a211e7ae112fd91c79ef81d8f0b24b06 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Mon, 13 Jul 2026 09:05:28 +0200 Subject: [PATCH] Isolate residual S/R target selection --- app/services/backtest_service.py | 30 +++++++++++++------ app/services/recommendation_service.py | 10 +++++-- docs/research/sr-levels-and-exits.md | 35 +++++++++++++++++++++++ scripts/run_backtest_snapshot.py | 2 ++ scripts/run_sr_v2_matrix.py | 18 ++++++++++++ tests/unit/test_backtest_service.py | 22 ++++++++++++++ tests/unit/test_recommendation_service.py | 3 ++ 7 files changed, 109 insertions(+), 11 deletions(-) diff --git a/app/services/backtest_service.py b/app/services/backtest_service.py index beb4901..11f4fa8 100644 --- a/app/services/backtest_service.py +++ b/app/services/backtest_service.py @@ -94,9 +94,14 @@ HORIZON = 30 # trading days to resolve an outcome (matches the evaluat ATR_MULTIPLIER = 1.5 RANGE_FACTOR_LOOKBACK = 504 RANGE_FACTOR_MIN_LOG = 1.0 # approximately a 2.7x high/low span +RANGE_RESIDUAL_VARIANTS = { + "rewrite_range504_structural_legacy_primary", + "rewrite_range504_structural_primary2", +} RANGE_FACTOR_VARIANTS = { "production_range504", "rewrite_range504_legacy_primary", + *RANGE_RESIDUAL_VARIANTS, } # Cross-sectional signal evaluation (factor IC). Each candidate signal is a @@ -169,7 +174,10 @@ def _sr_detector_variant(sr_variant: str) -> str: """Map factor-gated research arms to the detector they hold fixed.""" if sr_variant == "production_range504": return "production_control" - if sr_variant == "rewrite_range504_legacy_primary": + if ( + sr_variant == "rewrite_range504_legacy_primary" + or sr_variant in RANGE_RESIDUAL_VARIANTS + ): return "rewrite" return sr_variant.removesuffix("_legacy_primary") @@ -195,6 +203,17 @@ def _range_factor_allows(sr_variant: str, range_504_log: float) -> bool: ) +def _primary_min_rr_for_variant(sr_variant: str, activation: dict) -> float: + """Preserve the deployed selector except in explicit primary-2 research.""" + if ( + sr_variant in {"production_control", "production_range504"} + or sr_variant.endswith("_legacy_primary") + or sr_variant.startswith("legacy_") + ): + return 1.5 + return float(activation.get("min_rr", 0.0)) + + def _apply_zone_strength_variant(zone_levels: list[Any], sr_variant: str) -> list[Any]: """Apply post-cluster research controls without changing zone geometry.""" if sr_variant in {"legacy_geometry_neutral", "legacy_range_grid_neutral"}: @@ -363,6 +382,7 @@ def _window_setups( gate_levels = _gate_eligible_levels( sr_levels, confirmed_rounds_only=detector_variant in {"confirmed_rounds", "gate_v2"}, + exclude_standalone_rounds=sr_variant in RANGE_RESIDUAL_VARIANTS, ) zone_strength_mode = ( "soft" @@ -406,13 +426,7 @@ def _window_setups( # Collapse duplicate floor-pinned lottery targets (parity with # enhance_trade_setup). targets = _prune_floor_pinned_targets(targets) - primary_min_rr = ( - 1.5 - if sr_variant in {"production_control", "production_range504"} - or sr_variant.endswith("_legacy_primary") - or sr_variant.startswith("legacy_") - else float(activation.get("min_rr", 0.0)) - ) + primary_min_rr = _primary_min_rr_for_variant(sr_variant, activation) primary = _select_primary_target( targets, min_rr=primary_min_rr, diff --git a/app/services/recommendation_service.py b/app/services/recommendation_service.py index 0e89fec..d2d6f87 100644 --- a/app/services/recommendation_service.py +++ b/app/services/recommendation_service.py @@ -60,6 +60,7 @@ def _gate_eligible_levels( sr_levels: list[Any], *, confirmed_rounds_only: bool = False, + exclude_standalone_rounds: bool = False, min_round_rejections: int = 2, ) -> list[Any]: """Return structures allowed to influence entry qualification. @@ -69,7 +70,7 @@ def _gate_eligible_levels( either confluence with a pivot/volume source or distinct rejection clusters before such a level is allowed to manufacture a gate target. """ - if not confirmed_rounds_only: + if not confirmed_rounds_only and not exclude_standalone_rounds: return list(sr_levels) eligible: list[Any] = [] @@ -79,8 +80,11 @@ def _gate_eligible_levels( ]) is_round_only = sources == {"round_number"} rejections = int(getattr(level, "rejection_count", 0) or 0) - if not is_round_only or rejections >= min_round_rejections: - eligible.append(level) + if exclude_standalone_rounds and is_round_only: + continue + if confirmed_rounds_only and is_round_only and rejections < min_round_rejections: + continue + eligible.append(level) return eligible diff --git a/docs/research/sr-levels-and-exits.md b/docs/research/sr-levels-and-exits.md index 45a68a1..651fb13 100644 --- a/docs/research/sr-levels-and-exits.md +++ b/docs/research/sr-levels-and-exits.md @@ -585,6 +585,41 @@ Run on pre-2024 training data only: .venv/bin/python scripts/run_sr_v2_matrix.py factor --workers 14 ``` +Result: + +| training arm | qualified | Sharpe | CAGR | MaxDD | net avg R | ex-top-5% | +|---|---:|---:|---:|---:|---:|---:| +| production control | 676 | 1.28 | 28.8% | 21.4% | 0.230 | 0.066 | +| production + range504 | 180 | **1.65** | 31.4% | **15.9%** | **0.476** | **0.250** | +| clean rewrite | 1,200 | 0.96 | 21.0% | 22.2% | 0.037 | -0.102 | +| clean rewrite + range504 | 291 | 1.46 | **31.5%** | 18.4% | 0.292 | 0.158 | + +The scalar recovers most of the detector rewrite's regression. The clean arm now +beats the original production baseline on Sharpe, CAGR, drawdown, and robust +expectancy. Old target geometry retains a smaller edge over the equally filtered +clean arm. + +The residual cohort is target selection, not another range feature. The clean arm +retains 63 exceptionally strong common setups (+0.665R ex-top-5%), adds 228 weak +setups (+0.018R), and misses 117 strong old-geometry setups (+0.175R). Of the +clean-only additions, 156 have a standalone round-number primary; all 162 +round-only qualified setups have fewer than two observed rejections and return +-0.024R ex-top-5%. Structural-only and round-confluent primaries return +0.394R +and +0.369R. + +For 113 of the 117 missed setups, the clean detector does produce a target, but +its selected primary averages 1.56R; 77 land in the 1.5-2R veto band. A final +two-arm residual matrix therefore holds the clean detector and range factor fixed: + +- `rewrite_range504_structural_legacy_primary`: exclude standalone round targets, + retain the deployed 1.5 primary floor; +- `rewrite_range504_structural_primary2`: identical, but select the primary from + targets clearing 2.0R. + +```bash +.venv/bin/python scripts/run_sr_v2_matrix.py residual --workers 14 +``` + 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/scripts/run_backtest_snapshot.py b/scripts/run_backtest_snapshot.py index e944881..9f0c9da 100644 --- a/scripts/run_backtest_snapshot.py +++ b/scripts/run_backtest_snapshot.py @@ -57,6 +57,8 @@ def _parse_args() -> argparse.Namespace: "legacy_traffic_grid_only", "legacy_range_grid_touch", "legacy_range_grid_neutral", "production_range504", "rewrite_range504_legacy_primary", + "rewrite_range504_structural_legacy_primary", + "rewrite_range504_structural_primary2", ), 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 9d4e0c6..a11c7be 100644 --- a/scripts/run_sr_v2_matrix.py +++ b/scripts/run_sr_v2_matrix.py @@ -38,6 +38,10 @@ RANGE_FACTOR_ARMS = ( "production_range504", "rewrite_range504_legacy_primary", ) +RANGE_RESIDUAL_ARMS = ( + "rewrite_range504_structural_legacy_primary", + "rewrite_range504_structural_primary2", +) def _add_common(parser: argparse.ArgumentParser) -> None: @@ -76,6 +80,17 @@ def _args() -> argparse.Namespace: default=None, help="Run one range-factor arm without repeating the pair.", ) + residual = commands.add_parser( + "residual", + help="Isolate standalone rounds and the 1.5-2R primary-target veto.", + ) + _add_common(residual) + residual.add_argument( + "--only-arm", + choices=RANGE_RESIDUAL_ARMS, + default=None, + help="Run one residual target-geometry arm without repeating the pair.", + ) validate = commands.add_parser( "validate", help="Run production control and one locked arm from 2024-07-01.", @@ -166,6 +181,9 @@ def main() -> None: elif args.command == "factor": arms = (args.only_arm,) if args.only_arm else RANGE_FACTOR_ARMS _train(args, arms, "backtest-sr-range-factor-train") + elif args.command == "residual": + arms = (args.only_arm,) if args.only_arm else RANGE_RESIDUAL_ARMS + _train(args, arms, "backtest-sr-range-residual-train") else: _validate(args) diff --git a/tests/unit/test_backtest_service.py b/tests/unit/test_backtest_service.py index 67df269..6729c04 100644 --- a/tests/unit/test_backtest_service.py +++ b/tests/unit/test_backtest_service.py @@ -765,6 +765,8 @@ def test_sr_research_variant_is_explicit_and_validated(monkeypatch): "legacy_range_grid_neutral", "production_range504", "rewrite_range504_legacy_primary", + "rewrite_range504_structural_legacy_primary", + "rewrite_range504_structural_primary2", ): monkeypatch.setenv("BACKTEST_SR_VARIANT", variant) assert bt._sr_research_variant() == variant @@ -776,6 +778,10 @@ def test_sr_research_variant_is_explicit_and_validated(monkeypatch): def test_range_factor_detector_mapping_is_explicit(): assert bt._sr_detector_variant("production_range504") == "production_control" assert bt._sr_detector_variant("rewrite_range504_legacy_primary") == "rewrite" + assert bt._sr_detector_variant( + "rewrite_range504_structural_legacy_primary" + ) == "rewrite" + assert bt._sr_detector_variant("rewrite_range504_structural_primary2") == "rewrite" assert bt._sr_detector_variant("soft_zones_legacy_primary") == "soft_zones" @@ -789,6 +795,10 @@ def test_range_factor_gate_is_research_arm_only(): below = bt.RANGE_FACTOR_MIN_LOG - 0.01 assert not bt._range_factor_allows("production_range504", below) assert not bt._range_factor_allows("rewrite_range504_legacy_primary", below) + assert not bt._range_factor_allows( + "rewrite_range504_structural_primary2", + below, + ) assert bt._range_factor_allows( "production_range504", bt.RANGE_FACTOR_MIN_LOG, @@ -796,6 +806,18 @@ def test_range_factor_gate_is_research_arm_only(): assert bt._range_factor_allows("production_control", below) +def test_residual_arms_change_only_the_primary_rr_floor(): + activation = {"min_rr": 2.0} + assert bt._primary_min_rr_for_variant( + "rewrite_range504_structural_legacy_primary", + activation, + ) == 1.5 + assert bt._primary_min_rr_for_variant( + "rewrite_range504_structural_primary2", + activation, + ) == 2.0 + + @pytest.mark.parametrize( ("variant", "neutral_strength"), [ diff --git a/tests/unit/test_recommendation_service.py b/tests/unit/test_recommendation_service.py index 11e4ccf..e2c41b3 100644 --- a/tests/unit/test_recommendation_service.py +++ b/tests/unit/test_recommendation_service.py @@ -374,3 +374,6 @@ def test_gate_requires_confirmation_for_standalone_round_number(): assert _gate_eligible_levels( [untouched, confirmed, confluent], confirmed_rounds_only=True ) == [confirmed, confluent] + assert _gate_eligible_levels( + [untouched, confirmed, confluent], exclude_standalone_rounds=True + ) == [confluent]