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). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -221,6 +221,13 @@ class XauStrategyConfig:
|
|||||||
# Volatility regimes in which SELL setups are allowed (low/normal-vol
|
# Volatility regimes in which SELL setups are allowed (low/normal-vol
|
||||||
# shorts lose consistently against gold's macro uptrend).
|
# shorts lose consistently against gold's macro uptrend).
|
||||||
sell_vol_regimes: List[str] = field(default_factory=lambda: ["high", "extreme"])
|
sell_vol_regimes: List[str] = field(default_factory=lambda: ["high", "extreme"])
|
||||||
|
# Market regimes in which no XAU setup may fire (any direction).
|
||||||
|
# Empty by default; per-timeframe factories opt in — e.g. the mixed
|
||||||
|
# "trending" regime (strong ADX, EMAs disagree) on M15.
|
||||||
|
blocked_regimes: List[str] = field(default_factory=list)
|
||||||
|
# Market regimes in which XAU SELL setups may not fire. Empty by
|
||||||
|
# default; per-timeframe factories opt in.
|
||||||
|
sell_blocked_regimes: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
# ── M5-specific filters ──────────────────────────────────────
|
# ── M5-specific filters ──────────────────────────────────────
|
||||||
# M5 signals are noisy; require stronger confirmation before
|
# M5 signals are noisy; require stronger confirmation before
|
||||||
|
|||||||
@@ -14,6 +14,28 @@ window (+30.3R vs +10.6R; the 1R floor lifts targets past the min-TP
|
|||||||
spread gate, roughly doubling trades there). The same knob HURTS M5
|
spread gate, roughly doubling trades there). The same knob HURTS M5
|
||||||
(pinned 12mo book: +131.3R/699 vs +139.9R/547, PF 1.55 vs 1.78), so it
|
(pinned 12mo book: +131.3R/699 vs +139.9R/547, PF 1.55 vs 1.78), so it
|
||||||
is per-timeframe, exactly as PENDING_STEPS' decision rule anticipated.
|
is per-timeframe, exactly as PENDING_STEPS' decision rule anticipated.
|
||||||
|
|
||||||
|
M15 override 2 (validated 2026-07-11, 12mo window 2025-07-11 ->
|
||||||
|
2026-07-11 + OOS 2024-06-01 -> 2025-03-30, $0.30 spread): block the
|
||||||
|
mixed "trending" regime (ADX strong but EMAs unaligned — momentum
|
||||||
|
without directional structure). Those trades lost -6.8R/24 on 12mo,
|
||||||
|
negative in BOTH halves; blocking scores +65.7R/499 PF 1.38 DD 10.4R
|
||||||
|
vs +59.0R/521 PF 1.32 DD 12.6R baseline, better in both halves. OOS
|
||||||
|
is neutral (-1.4R = the 4 such trades that existed there; no power to
|
||||||
|
confirm or refute). M5 never trades this regime (its gates already
|
||||||
|
exclude it), so the block is M15-only. A second candidate — blocking
|
||||||
|
transitioning-regime SELLs (-4.0R/47 on 12mo) — was REJECTED: those
|
||||||
|
sells made +5.7R/32 in the 2024 OOS window (bull-market artifact).
|
||||||
|
|
||||||
|
M5 override (validated 2026-07-11, full-history replay 2020-03-17 ->
|
||||||
|
2026-07-11 + 12mo book, $0.30 spread): block hour 9 UTC (post-London-
|
||||||
|
open whipsaw). Hour-9 entries lost -28.9R/143 over 6 years, negative
|
||||||
|
in 5/7 calendar years including winning years (2022 -9.2R). Blocking:
|
||||||
|
+207.1R vs +178.3R baseline on the 6-year replay, max DD 28.7R vs
|
||||||
|
35.5R, improves 5/7 years (2023 goes -13.0R -> breakeven); the tuned
|
||||||
|
12mo book is unchanged (+139.0R vs +139.3R). M15-and-up keep hour 9:
|
||||||
|
it is POSITIVE there (M15 6yr +1.7R, M15 OOS +8.6R), so the block is
|
||||||
|
M5-only, same per-timeframe pattern as the M15 overrides.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
@@ -36,7 +58,14 @@ def make_config(
|
|||||||
config.confirmation_timeframes = (
|
config.confirmation_timeframes = (
|
||||||
confirmation_tfs or _CONFIRMATIONS.get(primary_tf, [Timeframe.M15, Timeframe.H4])
|
confirmation_tfs or _CONFIRMATIONS.get(primary_tf, [Timeframe.M15, Timeframe.H4])
|
||||||
)
|
)
|
||||||
|
if primary_tf == Timeframe.M5:
|
||||||
|
# Block hour 9 UTC on M5 only — see module docstring.
|
||||||
|
config.xau_strategy.blocked_hours_utc = (
|
||||||
|
config.xau_strategy.blocked_hours_utc + [9]
|
||||||
|
)
|
||||||
if primary_tf == Timeframe.M15:
|
if primary_tf == Timeframe.M15:
|
||||||
# 1R minimum target on M15 only — see module docstring.
|
# 1R minimum target on M15 only — see module docstring.
|
||||||
config.signals.tp_floor_rr = 1.0
|
config.signals.tp_floor_rr = 1.0
|
||||||
|
# Block the mixed "trending" regime on M15 only — see docstring.
|
||||||
|
config.xau_strategy.blocked_regimes = ["trending"]
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -779,6 +779,25 @@ class SignalAggregator:
|
|||||||
report.signal_quality = "D"
|
report.signal_quality = "D"
|
||||||
report.trade_setup = None
|
report.trade_setup = None
|
||||||
|
|
||||||
|
# ── 6g2. XAU regime gates (opt-in via per-TF factory overrides) ──
|
||||||
|
if (self.config.xau_strategy.enabled
|
||||||
|
and report.display_name == "XAU/USD"
|
||||||
|
and report.signal_quality in ("A+", "A", "B", "C")):
|
||||||
|
if report.market_regime in self.config.xau_strategy.blocked_regimes:
|
||||||
|
report.warnings.append(
|
||||||
|
f"REGIME FILTER: '{report.market_regime}' blocked for XAU"
|
||||||
|
)
|
||||||
|
report.signal_quality = "D"
|
||||||
|
report.trade_setup = None
|
||||||
|
elif (xau_is_sell_dir
|
||||||
|
and report.market_regime
|
||||||
|
in self.config.xau_strategy.sell_blocked_regimes):
|
||||||
|
report.warnings.append(
|
||||||
|
f"SELL FILTER: regime '{report.market_regime}' blocked for shorts"
|
||||||
|
)
|
||||||
|
report.signal_quality = "D"
|
||||||
|
report.trade_setup = None
|
||||||
|
|
||||||
# ── 6h. EUR/USD regime + quality + risk gate ──────────────
|
# ── 6h. EUR/USD regime + quality + risk gate ──────────────
|
||||||
if report.display_name == "EUR/USD" and self.config.eur_strategy.enabled:
|
if report.display_name == "EUR/USD" and self.config.eur_strategy.enabled:
|
||||||
eur = self.config.eur_strategy
|
eur = self.config.eur_strategy
|
||||||
|
|||||||
+28
-2
@@ -1845,9 +1845,17 @@ def _():
|
|||||||
confirmation_timeframes=ctfs,
|
confirmation_timeframes=ctfs,
|
||||||
)
|
)
|
||||||
if tf == Timeframe.M15:
|
if tf == Timeframe.M15:
|
||||||
# Documented per-TF override (see xauusd.py docstring):
|
# Documented per-TF overrides (see xauusd.py docstring):
|
||||||
# 1R minimum targets validated for M15 only.
|
# 1R minimum targets + mixed-"trending"-regime block,
|
||||||
|
# both validated for M15 only.
|
||||||
inline_cfg.signals.tp_floor_rr = 1.0
|
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), \
|
assert dataclasses.asdict(factory_cfg) == dataclasses.asdict(inline_cfg), \
|
||||||
f"XAU factory config differs from inline construction for {tf}"
|
f"XAU factory config differs from inline construction for {tf}"
|
||||||
|
|
||||||
@@ -2017,6 +2025,24 @@ def _():
|
|||||||
assert config.xau_strategy.enabled is True
|
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")
|
@test("NewsEngine._get_affected_pairs includes EUR/USD for EUR and USD events")
|
||||||
def _():
|
def _():
|
||||||
from news_engine import NewsEngine
|
from news_engine import NewsEngine
|
||||||
|
|||||||
Reference in New Issue
Block a user