diff --git a/app/services/backtest_service.py b/app/services/backtest_service.py index 5f4eb67..6be648c 100644 --- a/app/services/backtest_service.py +++ b/app/services/backtest_service.py @@ -156,6 +156,28 @@ def _sr_research_variant() -> str: return value +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 == "legacy_geometry_neutral": + # Neutrality must be enforced after the shared zone cluster: its legacy + # sum mode can otherwise turn two 50-strength constituents back into a + # 100-strength target and silently invalidate the ablation. + for level in zone_levels: + level.strength = 50 + return zone_levels + + +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 == "legacy_geometry_neutral": + # Neutrality must be enforced after the shared zone cluster: its legacy + # sum mode can otherwise turn two 50-strength constituents back into a + # 100-strength target and silently invalidate the ablation. + for level in zone_levels: + level.strength = 50 + return zone_levels + + def _backtest_entry_bounds() -> tuple[date | None, date | None]: """Optional research-only entry bounds used to protect validation data.""" parsed: list[date | None] = [] @@ -328,6 +350,8 @@ def _window_setups( entry, strength_mode=zone_strength_mode, ) + zone_levels = _apply_zone_strength_variant(zone_levels, sr_variant) + zone_levels = _apply_zone_strength_variant(zone_levels, sr_variant) targets = target_generator.generate_targets(direction, entry, stop, zone_levels, atr) if not targets: fallback_k = _atr_target_fallback_k() diff --git a/docs/research/sr-levels-and-exits.md b/docs/research/sr-levels-and-exits.md index 0484ddc..45bc019 100644 --- a/docs/research/sr-levels-and-exits.md +++ b/docs/research/sr-levels-and-exits.md @@ -498,6 +498,29 @@ Run on macOS: Do not validate any traffic arm yet. First establish whether geometry, pivots, or the range-occupancy grid reproduces production on pre-2024 training data. +Initial result: + +| arm | Sharpe | CAGR | MaxDD | net avg R | ex-top-5% | +|---|---:|---:|---:|---:|---:| +| production control | 1.28 | 28.8% | 21.4% | 0.230 | 0.066 | +| pivots only | 1.38 | 32.9% | 25.2% | 0.236 | 0.076 | +| traffic grid only | **1.72** | **41.8%** | **16.8%** | **0.271** | **0.136** | + +The traffic grid is the first research arm to beat control simultaneously on +Sharpe, CAGR, drawdown, and robust expectancy. It retains 264 production setups, +adds 240 positive-expectancy setups, and removes 412 weaker setups. This supports +the hypothesis that the useful legacy feature is the volume-weighted range- +occupancy grid, not pivots or visually meaningful S/R. + +The first geometry-neutral result is not interpretable: neutral strength was set +before the shared zone cluster, which summed multiple 50-strength constituents +back to 100. Neutrality is now enforced after clustering. Rerun only that arm: + +```bash +.venv/bin/python scripts/run_sr_v2_matrix.py traffic \ + --only-arm legacy_geometry_neutral --workers 14 +``` + **Next runs, if picked back up:** - A **per-name target model** for clear-air setups instead of a constant k×ATR. This diff --git a/scripts/run_sr_v2_matrix.py b/scripts/run_sr_v2_matrix.py index e4b1756..716fc3b 100644 --- a/scripts/run_sr_v2_matrix.py +++ b/scripts/run_sr_v2_matrix.py @@ -50,6 +50,12 @@ def _args() -> argparse.Namespace: help="Isolate legacy geometry, pivots, and price-traffic grid on training data.", ) _add_common(traffic) + traffic.add_argument( + "--only-arm", + choices=TRAFFIC_ARMS, + default=None, + help="Rerun one hidden-feature arm without repeating the full matrix.", + ) validate = commands.add_parser( "validate", help="Run production control and one locked arm from 2024-07-01.", @@ -135,7 +141,8 @@ def main() -> None: if args.command == "train": _train(args, TRAINING_ARMS, "backtest-sr-v2-train") elif args.command == "traffic": - _train(args, TRAFFIC_ARMS, "backtest-sr-traffic-train") + arms = (args.only_arm,) if args.only_arm else TRAFFIC_ARMS + _train(args, arms, "backtest-sr-traffic-train") else: _validate(args) diff --git a/tests/unit/test_backtest_service.py b/tests/unit/test_backtest_service.py index 096d886..b3b0486 100644 --- a/tests/unit/test_backtest_service.py +++ b/tests/unit/test_backtest_service.py @@ -766,6 +766,15 @@ def test_sr_research_variant_is_explicit_and_validated(monkeypatch): bt._sr_research_variant() +def test_geometry_neutral_strength_is_enforced_after_zone_clustering(): + levels = [ + SimpleNamespace(strength=100), + SimpleNamespace(strength=75), + ] + result = bt._apply_zone_strength_variant(levels, "legacy_geometry_neutral") + assert [level.strength for level in result] == [50, 50] + + def test_backtest_entry_bounds_validate_dates(monkeypatch): monkeypatch.setenv("BACKTEST_ENTRY_START", "2024-07-01") monkeypatch.setenv("BACKTEST_ENTRY_END", "2024-12-31")