Isolate legacy range-expansion factor

This commit is contained in:
2026-07-13 08:37:55 +02:00
parent f8e1107851
commit 1daf762bda
5 changed files with 156 additions and 6 deletions
+62 -2
View File
@@ -92,6 +92,12 @@ STEP_DAYS = 5 # weekly cadence (≈ 5 trading days)
MIN_LOOKBACK = 60 # bars needed before D for indicators (EMA cross needs 51)
HORIZON = 30 # trading days to resolve an outcome (matches the evaluator)
ATR_MULTIPLIER = 1.5
RANGE_FACTOR_LOOKBACK = 504
RANGE_FACTOR_MIN_LOG = 1.0 # approximately a 2.7x high/low span
RANGE_FACTOR_VARIANTS = {
"production_range504",
"rewrite_range504_legacy_primary",
}
# Cross-sectional signal evaluation (factor IC). Each candidate signal is a
# point-in-time number computed from closes alone (sentiment/fundamentals have no
@@ -146,6 +152,7 @@ SR_RESEARCH_VARIANTS = {
"legacy_traffic_grid_only",
"legacy_range_grid_touch",
"legacy_range_grid_neutral",
*RANGE_FACTOR_VARIANTS,
}
@@ -158,6 +165,36 @@ def _sr_research_variant() -> str:
return value
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":
return "rewrite"
return sr_variant.removesuffix("_legacy_primary")
def _range_504_log(highs: list[float], lows: list[float]) -> float:
"""Multiplicative high/low range over the last two trading years."""
window_highs = highs[-RANGE_FACTOR_LOOKBACK:]
window_lows = lows[-RANGE_FACTOR_LOOKBACK:]
if not window_highs or not window_lows:
return 0.0
high = max(window_highs)
low = min(window_lows)
if high <= 0 or low <= 0 or high < low:
return 0.0
return math.log(high / low)
def _range_factor_allows(sr_variant: str, range_504_log: float) -> bool:
"""Apply the explicit range factor only in its diagnostic arms."""
return (
sr_variant not in RANGE_FACTOR_VARIANTS
or range_504_log >= RANGE_FACTOR_MIN_LOG
)
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"}:
@@ -288,7 +325,8 @@ def _window_setups(
return []
sr_variant = _sr_research_variant()
detector_variant = sr_variant.removesuffix("_legacy_primary")
detector_variant = _sr_detector_variant(sr_variant)
range_504_log = _range_504_log(highs, lows)
if sr_variant == "legacy_geometry_neutral":
detected_levels = detect_sr_levels_legacy(
highs, lows, closes, volumes, neutral_strength=True
@@ -370,7 +408,7 @@ def _window_setups(
targets = _prune_floor_pinned_targets(targets)
primary_min_rr = (
1.5
if sr_variant == "production_control"
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))
@@ -419,6 +457,10 @@ def _window_setups(
# week are known. run_backtest ranks momentum and finalizes `qualified`.
core_config = {**activation, "min_momentum_percentile": 0.0}
meets_core = setup_qualifies(setup_ns, core_config)
meets_core = meets_core and _range_factor_allows(
sr_variant,
range_504_log,
)
best_prob = best_target_probability(setup_ns)
out.append({
"direction": direction,
@@ -445,6 +487,9 @@ def _window_setups(
),
"raw_level_count": len(sr_levels),
"gate_level_count": len(gate_levels),
"range_504_log": range_504_log,
"range_504_ratio": math.exp(range_504_log),
"range_factor_pass": range_504_log >= RANGE_FACTOR_MIN_LOG,
})
return out
@@ -608,6 +653,9 @@ def _replay_ticker(
"primary_distance_atr": s["primary_distance_atr"],
"raw_level_count": s["raw_level_count"],
"gate_level_count": s["gate_level_count"],
"range_504_log": s["range_504_log"],
"range_504_ratio": s["range_504_ratio"],
"range_factor_pass": s["range_factor_pass"],
"outcome": outcome,
"target_hit": target_hit,
"realized_r": realized_r,
@@ -677,6 +725,7 @@ def _sr_variant_diagnostics(candidates: list[dict]) -> dict:
rejections: list[int] = []
raw_counts: list[int] = []
gate_counts: list[int] = []
range_logs: list[float] = []
for cand in candidates:
sources = list(cand.get("primary_sources") or [])
for source in sources:
@@ -688,6 +737,7 @@ def _sr_variant_diagnostics(candidates: list[dict]) -> dict:
rejections.append(int(cand.get("primary_rejection_count", 0) or 0))
raw_counts.append(int(cand.get("raw_level_count", 0) or 0))
gate_counts.append(int(cand.get("gate_level_count", 0) or 0))
range_logs.append(float(cand.get("range_504_log", 0.0) or 0.0))
def avg(values: list[float] | list[int]) -> float | None:
return round(sum(values) / len(values), 3) if values else None
@@ -703,6 +753,10 @@ def _sr_variant_diagnostics(candidates: list[dict]) -> dict:
"avg_primary_rejection_count": avg(rejections),
"avg_raw_level_count": avg(raw_counts),
"avg_gate_level_count": avg(gate_counts),
"avg_range_504_log": avg(range_logs),
"range_factor_pass": sum(
1 for value in range_logs if value >= RANGE_FACTOR_MIN_LOG
),
}
@@ -738,6 +792,9 @@ def _sr_candidate_audit(candidates: list[dict], min_percentile: float) -> list[d
"primary_distance_atr": round(float(cand.get("primary_distance_atr", 0.0)), 6),
"raw_level_count": int(cand.get("raw_level_count", 0) or 0),
"gate_level_count": int(cand.get("gate_level_count", 0) or 0),
"range_504_log": round(float(cand.get("range_504_log", 0.0)), 6),
"range_504_ratio": round(float(cand.get("range_504_ratio", 1.0)), 6),
"range_factor_pass": bool(cand.get("range_factor_pass")),
"outcome": cand.get("outcome"),
"net_r": round(float(cand.get("realized_r", 0.0)) - _cost_r(cand), 6),
"hold30_r": round(float((cand.get("time_r") or {}).get(30, 0.0)), 6),
@@ -3089,6 +3146,9 @@ async def run_backtest(
"min_lookback": MIN_LOOKBACK,
"cost_per_side_pct": round(COST_PER_SIDE * 100, 3),
"sr_variant": _sr_research_variant(),
"range_factor_lookback": RANGE_FACTOR_LOOKBACK,
"range_factor_min_log": RANGE_FACTOR_MIN_LOG,
"range_factor_min_ratio": round(math.exp(RANGE_FACTOR_MIN_LOG), 4),
"entry_start": (
_backtest_entry_bounds()[0].isoformat()
if _backtest_entry_bounds()[0] is not None else None
+47 -4
View File
@@ -541,10 +541,53 @@ Run only these new arms on macOS:
--only-arm legacy_range_grid_neutral --workers 14
```
The touch arm should closely reproduce `legacy_traffic_grid_only`; that is the
volume-removal parity check. The touch-versus-neutral comparison then attributes
any remaining difference to occupancy strength. Freeze the winner before running
the post-2024-06-30 validation command; do not tune the bin count on training data.
The touch arm reproduced `legacy_traffic_grid_only` exactly: all 121,464
candidates, 504 qualified setups, cohort membership, expectancy, and portfolio
metrics match. Volume contributes nothing. Neutral strength won the training
portfolio comparison (Sharpe 1.98 versus 1.72), but failed the locked validation:
| validation arm | Sharpe | CAGR | MaxDD | net avg R | ex-top-5% |
|---|---:|---:|---:|---:|---:|
| production control | **2.78** | **73.3%** | **11.7%** | 0.174 | 0.022 |
| neutral range grid | 1.85 | 43.2% | 15.2% | **0.178** | **0.039** |
The neutral grid is a no-ship. The validation failure prompted a causal audit of
the control rather than another detector sweep. One relationship survives both
periods: dense legacy ladders are a proxy for a wide multiplicative price range.
| control cohort | training ex-top-5% | validation ex-top-5% |
|---|---:|---:|
| at least 70 legacy levels | +0.165R | +0.185R |
| fewer than 70 levels | +0.004R | -0.379R |
Level count is not independently useful after controlling for the last 504
trading days' range. For `log(max(high) / min(low)) >= 1.0315` (about a 2.8x
high/low ratio), the overlap cohort returns +0.292R training and +0.322R
validation ex-top-5%. High density without high range returns -0.096R and
+0.029R. Correlation between the explicit range and legacy level count is 0.864
training and 0.822 validation.
This isolates the hidden feature as a two-year realized price-excursion factor,
accidentally encoded by how many full-history pivots survive a 0.5% merge. It is
not evidence that the arbitrary lines are structural. A rounded threshold of
`log range >= 1.0` remains positive across a 0.9/1.0/1.1 sensitivity plateau.
Two diagnostic-only arms now test whether the explicit scalar replaces the side
effect:
- `production_range504`: deployed targets plus the explicit range gate;
- `rewrite_range504_legacy_primary`: clean targets, frozen primary selection,
plus the identical range gate.
Run on pre-2024 training data only:
```bash
.venv/bin/python scripts/run_sr_v2_matrix.py factor --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.
**Next runs, if picked back up:**
+1
View File
@@ -56,6 +56,7 @@ def _parse_args() -> argparse.Namespace:
"legacy_geometry_neutral", "legacy_pivots_only",
"legacy_traffic_grid_only", "legacy_range_grid_touch",
"legacy_range_grid_neutral",
"production_range504", "rewrite_range504_legacy_primary",
),
default=None,
help="Research-only S/R detector/gate arm.",
+18
View File
@@ -34,6 +34,10 @@ TRAFFIC_ARMS = (
"legacy_pivots_only",
*RANGE_GRID_ARMS,
)
RANGE_FACTOR_ARMS = (
"production_range504",
"rewrite_range504_legacy_primary",
)
def _add_common(parser: argparse.ArgumentParser) -> None:
@@ -61,6 +65,17 @@ def _args() -> argparse.Namespace:
default=None,
help="Rerun one hidden-feature arm without repeating the full matrix.",
)
factor = commands.add_parser(
"factor",
help="Test the explicit 504-day range factor with old and clean detectors.",
)
_add_common(factor)
factor.add_argument(
"--only-arm",
choices=RANGE_FACTOR_ARMS,
default=None,
help="Run one range-factor arm without repeating the pair.",
)
validate = commands.add_parser(
"validate",
help="Run production control and one locked arm from 2024-07-01.",
@@ -148,6 +163,9 @@ def main() -> None:
elif args.command == "traffic":
arms = (args.only_arm,) if args.only_arm else TRAFFIC_ARMS
_train(args, arms, "backtest-sr-traffic-train")
elif args.command == "factor":
arms = (args.only_arm,) if args.only_arm else RANGE_FACTOR_ARMS
_train(args, arms, "backtest-sr-range-factor-train")
else:
_validate(args)
+28
View File
@@ -763,6 +763,8 @@ def test_sr_research_variant_is_explicit_and_validated(monkeypatch):
"production_control",
"legacy_range_grid_touch",
"legacy_range_grid_neutral",
"production_range504",
"rewrite_range504_legacy_primary",
):
monkeypatch.setenv("BACKTEST_SR_VARIANT", variant)
assert bt._sr_research_variant() == variant
@@ -771,6 +773,29 @@ def test_sr_research_variant_is_explicit_and_validated(monkeypatch):
bt._sr_research_variant()
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("soft_zones_legacy_primary") == "soft_zones"
def test_range_504_log_uses_only_the_bounded_window():
highs = [1_000.0, *([100.0] * bt.RANGE_FACTOR_LOOKBACK)]
lows = [10.0, *([50.0] * bt.RANGE_FACTOR_LOOKBACK)]
assert bt._range_504_log(highs, lows) == pytest.approx(math.log(2.0))
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 bt._range_factor_allows(
"production_range504",
bt.RANGE_FACTOR_MIN_LOG,
)
assert bt._range_factor_allows("production_control", below)
@pytest.mark.parametrize(
("variant", "neutral_strength"),
[
@@ -863,6 +888,9 @@ def test_replay_ticker_candidates_carry_gate_fields():
for c in cands:
assert c.get("action") is not None
assert "risk_level" in c
assert c["range_504_log"] >= 0.0
assert c["range_504_ratio"] >= 1.0
assert isinstance(c["range_factor_pass"], bool)
async def _seed_oscillating_ticker(session, symbol: str, n: int = 160) -> None: