Document S/R findings and isolate legacy gate features
This commit is contained in:
@@ -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_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_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 |
|
||||
|
||||
@@ -141,6 +141,9 @@ SR_RESEARCH_VARIANTS = {
|
||||
"soft_zones_legacy_primary",
|
||||
"confirmed_rounds_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()
|
||||
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)
|
||||
else:
|
||||
detector_cap = 0 if detector_variant == "gate_v2" else MAX_LEVELS
|
||||
@@ -331,7 +346,9 @@ def _window_setups(
|
||||
targets = _prune_floor_pinned_targets(targets)
|
||||
primary_min_rr = (
|
||||
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))
|
||||
)
|
||||
primary = _select_primary_target(
|
||||
|
||||
@@ -400,17 +400,28 @@ def detect_sr_levels_legacy(
|
||||
closes: list[float],
|
||||
volumes: list[int],
|
||||
tolerance: float = DEFAULT_TOLERANCE,
|
||||
*,
|
||||
include_volume_profile: bool = True,
|
||||
include_pivots: bool = True,
|
||||
neutral_strength: bool = False,
|
||||
) -> 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:
|
||||
return []
|
||||
|
||||
candidates: list[tuple[float, str]] = []
|
||||
if include_volume_profile:
|
||||
try:
|
||||
for price in _legacy_volume_profile_nodes(highs, lows, closes, volumes):
|
||||
candidates.append((float(price), "volume_profile"))
|
||||
except ValidationError:
|
||||
pass
|
||||
if include_pivots:
|
||||
try:
|
||||
pivots = compute_pivot_points(highs, lows, closes)
|
||||
candidates.extend(
|
||||
@@ -469,6 +480,9 @@ def detect_sr_levels_legacy(
|
||||
)
|
||||
|
||||
_tag_levels(merged, closes[-1])
|
||||
if neutral_strength:
|
||||
for level in merged:
|
||||
level["strength"] = 50
|
||||
merged.sort(key=lambda row: row["strength"], reverse=True)
|
||||
return merged
|
||||
|
||||
|
||||
@@ -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
|
||||
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:**
|
||||
|
||||
- A **per-name target model** for clear-air setups instead of a constant k×ATR. This
|
||||
|
||||
@@ -53,6 +53,8 @@ def _parse_args() -> argparse.Namespace:
|
||||
"soft_zones", "confirmed_rounds", "gate_v2",
|
||||
"rewrite_legacy_primary", "soft_zones_legacy_primary",
|
||||
"confirmed_rounds_legacy_primary", "gate_v2_legacy_primary",
|
||||
"legacy_geometry_neutral", "legacy_pivots_only",
|
||||
"legacy_traffic_grid_only",
|
||||
),
|
||||
default=None,
|
||||
help="Research-only S/R detector/gate arm.",
|
||||
|
||||
@@ -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
|
||||
contains no backtest logic; every arm still runs through the production-parity
|
||||
@@ -23,6 +23,12 @@ TRAINING_ARMS = (
|
||||
"gate_v2_legacy_primary",
|
||||
)
|
||||
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:
|
||||
@@ -39,6 +45,11 @@ def _args() -> argparse.Namespace:
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
train = commands.add_parser("train", help="Run all arms before 2024-07-01.")
|
||||
_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",
|
||||
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:
|
||||
for arm in TRAINING_ARMS:
|
||||
def _train(
|
||||
args: argparse.Namespace,
|
||||
arms: tuple[str, ...],
|
||||
filename_prefix: str,
|
||||
) -> None:
|
||||
for arm in arms:
|
||||
_run_arm(
|
||||
arm,
|
||||
args.snapshot,
|
||||
args.workers,
|
||||
entry_flag="--entry-end",
|
||||
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:
|
||||
@@ -118,7 +133,9 @@ def _validate(args: argparse.Namespace) -> None:
|
||||
def main() -> None:
|
||||
args = _args()
|
||||
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:
|
||||
_validate(args)
|
||||
|
||||
|
||||
@@ -242,3 +242,28 @@ class TestDetectSrLevels:
|
||||
# The research control intentionally keeps the deployed detector's much
|
||||
# denser output instead of borrowing the rewrite's presentation cap.
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user