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_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 |
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -400,17 +400,28 @@ 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]] = []
|
||||||
|
if include_volume_profile:
|
||||||
try:
|
try:
|
||||||
for price in _legacy_volume_profile_nodes(highs, lows, closes, volumes):
|
for price in _legacy_volume_profile_nodes(highs, lows, closes, volumes):
|
||||||
candidates.append((float(price), "volume_profile"))
|
candidates.append((float(price), "volume_profile"))
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
pass
|
pass
|
||||||
|
if include_pivots:
|
||||||
try:
|
try:
|
||||||
pivots = compute_pivot_points(highs, lows, closes)
|
pivots = compute_pivot_points(highs, lows, closes)
|
||||||
candidates.extend(
|
candidates.extend(
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user