Document S/R findings and isolate legacy gate features

This commit is contained in:
2026-07-12 23:38:09 +02:00
parent ce0df6a126
commit 93403b4d3a
7 changed files with 144 additions and 23 deletions
+1 -1
View File
@@ -430,7 +430,7 @@ Research-only flags, all off by default (the default report is byte-identical to
|---|---| |---|---|
| `BACKTEST_HOLDOUT_SPLIT=YYYY-MM-DD` | Adds a `holdout` section: train (entries before) vs test (entries on/after), as disjoint books | | `BACKTEST_HOLDOUT_SPLIT=YYYY-MM-DD` | Adds a `holdout` section: train (entries before) vs test (entries on/after), as disjoint books |
| `BACKTEST_MIN_RR_SWEEP=1` | Sweeps the activation R:R floor against portfolio Sharpe. Combine with `BACKTEST_HOLDOUT_SPLIT` to sweep out-of-sample | | `BACKTEST_MIN_RR_SWEEP=1` | Sweeps the activation R:R floor against portfolio Sharpe. Combine with `BACKTEST_HOLDOUT_SPLIT` to sweep out-of-sample |
| `BACKTEST_SR_VARIANT=<arm>` | Research-only S/R arm: `production_control`, `rr_aligned_control`, `rewrite`, `soft_zones`, `confirmed_rounds`, or `gate_v2` | | `BACKTEST_SR_VARIANT=<arm>` | Research-only S/R detector/gate arm; see `docs/research/sr-levels-and-exits.md` for the detector and hidden-feature matrices |
| `BACKTEST_ENTRY_START=YYYY-MM-DD` | Restrict candidate entry dates to a validation window | | `BACKTEST_ENTRY_START=YYYY-MM-DD` | Restrict candidate entry dates to a validation window |
| `BACKTEST_ENTRY_END=YYYY-MM-DD` | Restrict candidate entry dates to a training window | | `BACKTEST_ENTRY_END=YYYY-MM-DD` | Restrict candidate entry dates to a training window |
| `BACKTEST_SR_AUDIT=1` | Add momentum-slice candidate rows for paired S/R cohort comparison | | `BACKTEST_SR_AUDIT=1` | Add momentum-slice candidate rows for paired S/R cohort comparison |
+19 -2
View File
@@ -141,6 +141,9 @@ SR_RESEARCH_VARIANTS = {
"soft_zones_legacy_primary", "soft_zones_legacy_primary",
"confirmed_rounds_legacy_primary", "confirmed_rounds_legacy_primary",
"gate_v2_legacy_primary", "gate_v2_legacy_primary",
"legacy_geometry_neutral",
"legacy_pivots_only",
"legacy_traffic_grid_only",
} }
@@ -273,7 +276,19 @@ def _window_setups(
sr_variant = _sr_research_variant() sr_variant = _sr_research_variant()
detector_variant = sr_variant.removesuffix("_legacy_primary") detector_variant = sr_variant.removesuffix("_legacy_primary")
if detector_variant in {"production_control", "rr_aligned_control"}: if sr_variant == "legacy_geometry_neutral":
detected_levels = detect_sr_levels_legacy(
highs, lows, closes, volumes, neutral_strength=True
)
elif sr_variant == "legacy_pivots_only":
detected_levels = detect_sr_levels_legacy(
highs, lows, closes, volumes, include_volume_profile=False
)
elif sr_variant == "legacy_traffic_grid_only":
detected_levels = detect_sr_levels_legacy(
highs, lows, closes, volumes, include_pivots=False
)
elif detector_variant in {"production_control", "rr_aligned_control"}:
detected_levels = detect_sr_levels_legacy(highs, lows, closes, volumes) detected_levels = detect_sr_levels_legacy(highs, lows, closes, volumes)
else: else:
detector_cap = 0 if detector_variant == "gate_v2" else MAX_LEVELS detector_cap = 0 if detector_variant == "gate_v2" else MAX_LEVELS
@@ -331,7 +346,9 @@ def _window_setups(
targets = _prune_floor_pinned_targets(targets) targets = _prune_floor_pinned_targets(targets)
primary_min_rr = ( primary_min_rr = (
1.5 1.5
if sr_variant == "production_control" or sr_variant.endswith("_legacy_primary") if sr_variant == "production_control"
or sr_variant.endswith("_legacy_primary")
or sr_variant.startswith("legacy_")
else float(activation.get("min_rr", 0.0)) else float(activation.get("min_rr", 0.0))
) )
primary = _select_primary_target( primary = _select_primary_target(
+28 -14
View File
@@ -400,25 +400,36 @@ def detect_sr_levels_legacy(
closes: list[float], closes: list[float],
volumes: list[int], volumes: list[int],
tolerance: float = DEFAULT_TOLERANCE, tolerance: float = DEFAULT_TOLERANCE,
*,
include_volume_profile: bool = True,
include_pivots: bool = True,
neutral_strength: bool = False,
) -> list[dict]: ) -> list[dict]:
"""Exact research control for the deployed pre-rewrite detector.""" """Deployed detector plus source-isolation controls for local research.
The defaults remain the exact production control. Keyword-only switches
allow a research arm to remove one source or neutralize strength without
copying or subtly changing the legacy implementation.
"""
if not closes: if not closes:
return [] return []
candidates: list[tuple[float, str]] = [] candidates: list[tuple[float, str]] = []
try: if include_volume_profile:
for price in _legacy_volume_profile_nodes(highs, lows, closes, volumes): try:
candidates.append((float(price), "volume_profile")) for price in _legacy_volume_profile_nodes(highs, lows, closes, volumes):
except ValidationError: candidates.append((float(price), "volume_profile"))
pass except ValidationError:
try: pass
pivots = compute_pivot_points(highs, lows, closes) if include_pivots:
candidates.extend( try:
(float(price), "pivot_point") pivots = compute_pivot_points(highs, lows, closes)
for price in pivots.get("swing_highs", []) + pivots.get("swing_lows", []) candidates.extend(
) (float(price), "pivot_point")
except ValidationError: for price in pivots.get("swing_highs", []) + pivots.get("swing_lows", [])
pass )
except ValidationError:
pass
if not candidates: if not candidates:
return [] return []
@@ -469,6 +480,9 @@ def detect_sr_levels_legacy(
) )
_tag_levels(merged, closes[-1]) _tag_levels(merged, closes[-1])
if neutral_strength:
for level in merged:
level["strength"] = 50
merged.sort(key=lambda row: row["strength"], reverse=True) merged.sort(key=lambda row: row["strength"], reverse=True)
return merged return merged
+46
View File
@@ -452,6 +452,52 @@ 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 automatically. A lower validation Sharpe or higher drawdown remains a no-ship
result even when CAGR rises. result even when CAGR rises.
## 8. Final detector-only result: no rewritten gate arm advances
The last matrix froze production's effective gate (`primary min_rr=1.5`,
activation `min_rr=2.0`) and varied only level detection/zone policy on entries
through 2024-06-30. Corrected portfolio calendars end after the last position can
resolve; there is no flat-cash tail.
| arm | Sharpe | CAGR | MaxDD | qualified | net avg R | ex-top-5% |
|---|---:|---:|---:|---:|---:|---:|
| production control | **1.28** | **28.8%** | 21.4% | 676 | **0.230** | **0.066** |
| rewrite + legacy primary | 0.96 | 21.0% | 22.2% | 1,200 | 0.037 | -0.102 |
| soft zones + legacy primary | 1.08 | 25.1% | **17.8%** | 1,161 | 0.045 | -0.096 |
| confirmed rounds + legacy primary | 1.14 | 22.1% | 20.2% | 570 | 0.189 | 0.045 |
| gate v2 + legacy primary | 0.87 | 16.7% | 20.7% | 604 | 0.187 | 0.041 |
No arm advances to validation. The raw rewrite retains only 249 of 676 production
setups, removes 427 good setups, and adds 951 setups with negative expectancy.
Round confirmation repairs the added cohort but still removes 430 production
setups whose 30-day average (+0.680R) exceeds the additions (+0.511R). Uncapping
recovers only 16 of those missing setups. The old detector averages 43.3 gate
levels versus 15.0 rewritten and 24.0 rewritten-uncapped levels.
**Standing no-ship decision:** keep both the deployed detector and the legacy 1.5
primary-selection behavior in the trading path. The rewritten structure may only
proceed as a separately computed display model. Do not merge this research branch
into production as-is.
### Hidden-feature isolation
The remaining hypothesis is that the deployed detector accidentally measures
long-memory historical price traffic rather than genuine S/R. A dedicated matrix
holds the complete gate fixed and changes one legacy component at a time:
- `legacy_geometry_neutral`: old locations, every merged strength fixed at 50;
- `legacy_pivots_only`: unfiltered full-history pivots, no VP grid;
- `legacy_traffic_grid_only`: old range-volume price grid, no pivots.
Run on macOS:
```bash
.venv/bin/python scripts/run_sr_v2_matrix.py traffic --workers 14
```
Do not validate any traffic arm yet. First establish whether geometry, pivots, or
the range-occupancy grid reproduces production on pre-2024 training data.
**Next runs, if picked back up:** **Next runs, if picked back up:**
- A **per-name target model** for clear-air setups instead of a constant k×ATR. This - A **per-name target model** for clear-air setups instead of a constant k×ATR. This
+2
View File
@@ -53,6 +53,8 @@ def _parse_args() -> argparse.Namespace:
"soft_zones", "confirmed_rounds", "gate_v2", "soft_zones", "confirmed_rounds", "gate_v2",
"rewrite_legacy_primary", "soft_zones_legacy_primary", "rewrite_legacy_primary", "soft_zones_legacy_primary",
"confirmed_rounds_legacy_primary", "gate_v2_legacy_primary", "confirmed_rounds_legacy_primary", "gate_v2_legacy_primary",
"legacy_geometry_neutral", "legacy_pivots_only",
"legacy_traffic_grid_only",
), ),
default=None, default=None,
help="Research-only S/R detector/gate arm.", help="Research-only S/R detector/gate arm.",
+23 -6
View File
@@ -1,4 +1,4 @@
"""Run the S/R v2 training matrix or one locked validation comparison. """Run S/R detector, hidden-feature, or locked validation comparisons.
This is a cross-platform orchestrator around ``run_backtest_snapshot.py``. It This is a cross-platform orchestrator around ``run_backtest_snapshot.py``. It
contains no backtest logic; every arm still runs through the production-parity contains no backtest logic; every arm still runs through the production-parity
@@ -23,6 +23,12 @@ TRAINING_ARMS = (
"gate_v2_legacy_primary", "gate_v2_legacy_primary",
) )
LOCKABLE_ARMS = TRAINING_ARMS[1:] LOCKABLE_ARMS = TRAINING_ARMS[1:]
TRAFFIC_ARMS = (
"production_control",
"legacy_geometry_neutral",
"legacy_pivots_only",
"legacy_traffic_grid_only",
)
def _add_common(parser: argparse.ArgumentParser) -> None: def _add_common(parser: argparse.ArgumentParser) -> None:
@@ -39,6 +45,11 @@ def _args() -> argparse.Namespace:
commands = parser.add_subparsers(dest="command", required=True) commands = parser.add_subparsers(dest="command", required=True)
train = commands.add_parser("train", help="Run all arms before 2024-07-01.") train = commands.add_parser("train", help="Run all arms before 2024-07-01.")
_add_common(train) _add_common(train)
traffic = commands.add_parser(
"traffic",
help="Isolate legacy geometry, pivots, and price-traffic grid on training data.",
)
_add_common(traffic)
validate = commands.add_parser( validate = commands.add_parser(
"validate", "validate",
help="Run production control and one locked arm from 2024-07-01.", help="Run production control and one locked arm from 2024-07-01.",
@@ -75,17 +86,21 @@ def _run_arm(
) )
def _train(args: argparse.Namespace) -> None: def _train(
for arm in TRAINING_ARMS: args: argparse.Namespace,
arms: tuple[str, ...],
filename_prefix: str,
) -> None:
for arm in arms:
_run_arm( _run_arm(
arm, arm,
args.snapshot, args.snapshot,
args.workers, args.workers,
entry_flag="--entry-end", entry_flag="--entry-end",
entry_date="2024-06-30", entry_date="2024-06-30",
output=ROOT / "reports" / f"backtest-sr-v2-train-{arm}.json", output=ROOT / "reports" / f"{filename_prefix}-{arm}.json",
) )
print("Training matrix complete. Lock one arm before running validation.") print("Training matrix complete. Review results before running validation.")
def _validate(args: argparse.Namespace) -> None: def _validate(args: argparse.Namespace) -> None:
@@ -118,7 +133,9 @@ def _validate(args: argparse.Namespace) -> None:
def main() -> None: def main() -> None:
args = _args() args = _args()
if args.command == "train": if args.command == "train":
_train(args) _train(args, TRAINING_ARMS, "backtest-sr-v2-train")
elif args.command == "traffic":
_train(args, TRAFFIC_ARMS, "backtest-sr-traffic-train")
else: else:
_validate(args) _validate(args)
+25
View File
@@ -242,3 +242,28 @@ class TestDetectSrLevels:
# The research control intentionally keeps the deployed detector's much # The research control intentionally keeps the deployed detector's much
# denser output instead of borrowing the rewrite's presentation cap. # denser output instead of borrowing the rewrite's presentation cap.
assert len(levels) > MAX_LEVELS assert len(levels) > MAX_LEVELS
def test_legacy_geometry_neutral_changes_only_strength(self):
highs, lows, closes, volumes = _make_series(n=500)
control = detect_sr_levels_legacy(highs, lows, closes, volumes)
neutral = detect_sr_levels_legacy(
highs, lows, closes, volumes, neutral_strength=True
)
assert [level["price_level"] for level in neutral] == [
level["price_level"] for level in sorted(
control, key=lambda row: row["price_level"]
)
]
assert all(level["strength"] == 50 for level in neutral)
def test_legacy_source_ablation_isolates_candidates(self):
highs, lows, closes, volumes = _make_series(n=500)
pivots = detect_sr_levels_legacy(
highs, lows, closes, volumes, include_volume_profile=False
)
traffic = detect_sr_levels_legacy(
highs, lows, closes, volumes, include_pivots=False
)
assert pivots and traffic
assert all(level["sources"] == ["pivot_point"] for level in pivots)
assert all(level["sources"] == ["volume_profile"] for level in traffic)