XAU: two validated per-TF gates — M15 mixed-regime block, M5 hour-9 block

M15 mixed-"trending" regime block (ADX strong, EMAs unaligned =
momentum without direction): -6.8R/24 trades on 12mo, negative both
halves; blocking scores +65.7R/499 PF 1.38 DD 10.4R vs +59.0R/521
PF 1.32 DD 12.6R, better both halves, OOS 2024-06->2025-03 neutral
(-1.4R = the 4 such trades there). M5 never trades this regime.
New XauStrategyConfig.blocked_regimes / sell_blocked_regimes knobs
(default empty) + aggregator gate 6g2; M15 factory opts in.

M5 hour-9 UTC block (post-London-open whipsaw): -28.9R/143 over the
6-year book, negative 5/7 calendar years. Full-history replay with
block: +207.1R vs +178.3R, max DD 28.7R vs 35.5R, 2023 -13.0R ->
breakeven; tuned 12mo book unchanged (139.0 vs 139.3). Hour 9 is
positive on M15 (+1.7R 6yr, +8.6R OOS) so the block is factory
M5-only.

Rejected after validation (documented in xauusd.py + session memory):
transitioning-SELL block (bull-market artifact, +5.7R in 2024 OOS),
M15 trending_down block (M5 cross-TF contradiction), losing-streak
circuit breakers, 30d efficiency-ratio trend gate, and all entry-level
indicator filters (winners == losers at entry on every feature).

Full-history context (Twelve Data 5min starts 2020-03-16): M5 6.3yr
+207R PF ~1.3 post-fix with losing year 2021; M15 +152.6R PF 1.19
DD 49.7R — edge is regime-dependent, chop years are structural.

Tests: 178 passed (new gate test + factory-equivalence updates).
This commit is contained in:
Harwinder Sekhon
2026-07-11 21:38:42 -04:00
parent a2bf80054c
commit 6f69b9dd90
4 changed files with 83 additions and 2 deletions
+28 -2
View File
@@ -1845,9 +1845,17 @@ def _():
confirmation_timeframes=ctfs,
)
if tf == Timeframe.M15:
# Documented per-TF override (see xauusd.py docstring):
# 1R minimum targets validated for M15 only.
# Documented per-TF overrides (see xauusd.py docstring):
# 1R minimum targets + mixed-"trending"-regime block,
# both validated for M15 only.
inline_cfg.signals.tp_floor_rr = 1.0
inline_cfg.xau_strategy.blocked_regimes = ["trending"]
if tf == Timeframe.M5:
# Documented per-TF override (see xauusd.py docstring):
# hour 9 UTC blocked on M5 only.
inline_cfg.xau_strategy.blocked_hours_utc = (
inline_cfg.xau_strategy.blocked_hours_utc + [9]
)
assert dataclasses.asdict(factory_cfg) == dataclasses.asdict(inline_cfg), \
f"XAU factory config differs from inline construction for {tf}"
@@ -2017,6 +2025,24 @@ def _():
assert config.xau_strategy.enabled is True
@test("XAU regime filter: blocked regime never yields actionable quality")
def _():
from pair_signals.xauusd import make_config
config = make_config(Timeframe.M15)
config.backtest.session_filter = False
config.xau_strategy.blocked_regimes = [
"trending", "trending_up", "trending_down", "ranging", "transitioning"]
agg = SignalAggregator(config)
df = make_ohlcv(n=300, start_price=3300.0, trend=0.5, seed=7)
dfs = {Timeframe.M15: df, Timeframe.M5: df, Timeframe.H1: df}
report = agg.analyze_from_dataframes(symbol=XAUUSD, dfs=dfs, use_news=False)
assert report.signal_quality not in ("A+", "A", "B", "C"), \
f"blocked regime must not stay actionable, got {report.signal_quality}"
# The M15 factory opts into the mixed-"trending" block; M5 must not.
assert make_config(Timeframe.M15).xau_strategy.blocked_regimes == ["trending"]
assert make_config(Timeframe.M5).xau_strategy.blocked_regimes == []
@test("NewsEngine._get_affected_pairs includes EUR/USD for EUR and USD events")
def _():
from news_engine import NewsEngine