M15 trail arm 0.70 (validated grid) + backtest/live parity hardening

Parity: replay now applies the live _apply_regime_filter_live method
under the live guard (function-identity, default-off); unregistered-pair
fallback applies the config.json overlay; parity sweep gains direct
ATR/vol-regime assertions; regime-filter semantics test added.

Trail campaign: --trail-arm/--trail-dist research flags; 30-cell grid on
5yr M15 refuted the earlier-arm hypothesis and found the 0.70-0.80 arm
plateau. Shipped M15-only trail_arm_r=0.70 (factory override):
+210.7R PF 1.35 WR 57.8% vs +175.7R PF 1.30 WR 53.9% baseline; both
halves and 2024-06->2025-03 OOS better (+57.4 vs +45.4); 5yr max-DD
clause (33.9R vs 29.1R) explicitly waived by owner. M5 unchanged.
Factory replay reproduces the grid cell trade-for-trade; 201/201 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPNF37MP9Kuv62B1VBrZ1b
This commit is contained in:
Harwinder Sekhon
2026-07-21 16:40:33 -04:00
parent 11ff49befb
commit bf8e5fbf39
5 changed files with 101 additions and 2 deletions
+58
View File
@@ -1868,6 +1868,8 @@ def _():
inline_cfg.signals.tp_floor_rr = 1.0
inline_cfg.xau_strategy.blocked_regimes = ["trending"]
inline_cfg.xau_strategy.sell_blocked_hours_utc = [15, 17]
inline_cfg.signals.trail_arm_r = 0.70
inline_cfg.signals.trail_dist_r = 0.75
if tf == Timeframe.M5:
# Documented per-TF overrides (see xauusd.py docstring):
# hour 9 UTC blocked + BUY-only chart patterns on M5 only.
@@ -2060,6 +2062,13 @@ def _():
bl = r_live.indicators["bollinger"]
bg = r_gpu.indicators["bollinger"]
assert bl.signal == bg.signal and bl.description == bg.description, f"bollinger @ {i}"
al = r_live.indicators["atr"]
ag = r_gpu.indicators["atr"]
assert abs(al.value - ag.value) < 1e-9, f"atr value @ {i}"
assert al.description == ag.description, f"atr description @ {i}"
assert r_live.volatility_regime == r_gpu.volatility_regime, f"vol regime @ {i}"
assert abs(r_live.key_levels["atr"] - r_gpu.key_levels["atr"]) < 1e-9, f"key_levels atr @ {i}"
assert abs(r_live.key_levels["atr_pct"] - r_gpu.key_levels["atr_pct"]) < 1e-9, f"key_levels atr_pct @ {i}"
if "Ribbon COMPRESSED" in el.description:
seen_compressed = True
if "Ribbon EXPANDING" in el.description:
@@ -2068,6 +2077,45 @@ def _():
assert seen_expanding, "sweep never saw a ribbon expansion (+10 conf path) — no teeth"
@test("Regime filter: live gate semantics + backtest calls the same method")
def _():
from signal_aggregator import SignalAggregator
from pair_signals import PAIR_PRODUCERS
cfg = PAIR_PRODUCERS[XAUUSD](Timeframe.M15, [Timeframe.M5, Timeframe.H1])
cfg.backtest.regime_filter = True
agg = SignalAggregator(cfg)
df = make_ohlcv(n=300, start_price=3400.0, trend=0.05, seed=3)
flat = make_ohlcv(n=300, start_price=3400.0, trend=0.0, seed=7)
adx = agg._latest_adx(flat, cfg.technical.adx_period)
assert adx is not None and adx < cfg.backtest.regime_filter_min_adx, \
f"fixture must be choppy (ADX {adx}) or the test has no teeth"
# Low higher-TF ADX → signal downgraded to NEUTRAL/F, setup killed.
report = agg.analyze_from_dataframes(XAUUSD, {Timeframe.M15: df},
context=None, use_news=False)
agg._apply_regime_filter_live(report, {Timeframe.H4: flat})
assert report.final_signal == SignalStrength.NEUTRAL
assert report.signal_quality == "F"
assert report.trade_setup is None
assert any("REGIME FILTER" in w for w in report.warnings)
# Regime TF absent from dfs → warning only, report untouched.
report2 = agg.analyze_from_dataframes(XAUUSD, {Timeframe.M15: df},
context=None, use_news=False)
quality_before = report2.signal_quality
agg._apply_regime_filter_live(report2, {})
assert report2.signal_quality == quality_before
assert any("data unavailable" in w for w in report2.warnings)
# Backtest wiring: the replay applies the SAME method under the SAME
# guard as live analyze_pair (function-identity parity).
import gpu_backtest as _gb
src = open(_gb.__file__, encoding="utf-8").read()
assert "_apply_regime_filter_live(report, slice_dfs)" in src
assert "backtest.regime_filter" in src
@test("disabled_indicators=['macd'] removes the pillar but keeps the histogram")
def _():
from engine import TechnicalEngine
@@ -2521,6 +2569,16 @@ def _():
assert args2.cp is False
@test("Backtester: --trail-arm/--trail-dist research knobs parse, default None")
def _():
from gpu_backtest import _parse_args
args = _parse_args(["--pair", "XAUUSD",
"--trail-arm", "0.45", "--trail-dist", "0.75"])
assert args.trail_arm == 0.45 and args.trail_dist == 0.75
args2 = _parse_args(["--pair", "XAUUSD"])
assert args2.trail_arm is None and args2.trail_dist is None
# ══════════════════════════════════════════════════════════════════════
# MODULE 13: app_config.py (config.json overlay)
# ══════════════════════════════════════════════════════════════════════