M5 at real cost: disable XAU_PB, squash partial EMA alignment

Both validated at the measured $0.65 spread on Oanda bars (12mo halves +
PF, OOS 2024-06->2025-03, 5yr; full evidence in pair_signals/xauusd.py
M5 overrides 4-5):

- m5_pullback_enabled False on M5: loss forensics identified XAU_PB as
  the largest wrong-direction loss source; 12mo PF 1.26 (failing the
  acceptance bar) -> 1.44, max DD 23.9R -> 13.9R. PB code stays for
  future regime-gated experiments.
- New TechnicalConfig.ema_partial_neutral (default False), True on M5
  only: partial 4-EMA ribbon alignment (2-3 of 4 checks) scores NEUTRAL
  instead of a weak BUY/SELL (-0.008R/84 vs +0.230R/341 fully aligned).
  Squash mirrored in the backtester's precomputed EMA scorer in this
  same commit (live==backtest parity). 12mo +97.6R/556 PF 1.48 DD
  10.7R halves +47.8/+49.8; 5yr +127.4R PF 1.38; OOS one-trade tie.
- Rejected and logged (memory + docstring): hour-19 block (OOS fail),
  hours-14/15, trail dist 0.75, confirm-TF swaps incl M15+H4 (OOS
  ordering), divergence/candlestick weight changes, bollinger removal.
- Tests 296 -> 302: partial-alignment fixture test, factory-equivalence
  mirrors, per-TF isolation test, BC-gate fixture force-enables PB.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1eKWZsAVapJ1C4winYLqX
This commit is contained in:
Harwinder Sekhon
2026-08-04 16:54:09 -04:00
parent 90801d665d
commit 3c9bb2f017
7 changed files with 124 additions and 2 deletions
+51
View File
@@ -443,6 +443,36 @@ def _():
assert "ema_slow" in result.key_levels
@test("EMA-4 partial alignment squashes to NEUTRAL with ema_partial_neutral")
def _():
import numpy as np
# Long decline then a medium rally: price>EMA9>EMA21>EMA50 but
# EMA50<EMA200 -> alignment 2/4 = partial BUY under legacy scoring.
closes = np.concatenate([np.linspace(3000, 2400, 400),
np.linspace(2400, 2560, 55)])
idx = pd.date_range("2026-01-01", periods=len(closes), freq="5min", tz="UTC")
df = pd.DataFrame({"Open": closes, "High": closes + 1.0,
"Low": closes - 1.0, "Close": closes,
"Volume": 1000.0}, index=idx)
assert TechnicalConfig().ema_partial_neutral is False # legacy default
base = TechnicalEngine(TechnicalConfig()).analyze(
df, "XAU/USD", "5min").indicators["ema_trend"]
assert base.signal == SignalStrength.BUY and 0.2 < base.value <= 0.6, \
f"fixture must yield a partial-alignment BUY, got {base.signal} {base.value}"
ind = TechnicalEngine(TechnicalConfig(ema_partial_neutral=True)).analyze(
df, "XAU/USD", "5min").indicators["ema_trend"]
assert ind.signal == SignalStrength.NEUTRAL and ind.confidence == 40
# Full alignment is untouched by the knob.
up = np.linspace(2000, 3000, 380)
df2 = pd.DataFrame({"Open": up, "High": up + 1.0, "Low": up - 1.0,
"Close": up, "Volume": 1000.0},
index=pd.date_range("2026-01-01", periods=380,
freq="5min", tz="UTC"))
strong = TechnicalEngine(TechnicalConfig(ema_partial_neutral=True)).analyze(
df2, "XAU/USD", "5min").indicators["ema_trend"]
assert strong.signal == SignalStrength.STRONG_BUY
@test("TechnicalEngine EMA-2 mode computes EMA50/200")
def _():
config = TechnicalConfig(ema_mode="2ema")
@@ -2077,12 +2107,30 @@ def _():
] + [9]
inline_cfg.xau_strategy.cp_enabled = True
inline_cfg.xau_strategy.cp_patterns = ["inv_hs", "double_bottom", "bull_flag"]
inline_cfg.xau_strategy.m5_pullback_enabled = False
inline_cfg.technical.ema_partial_neutral = True
inline_cfg.signals.trail_arm_r = 0.60
inline_cfg.signals.trail_dist_r = 0.85
assert dataclasses.asdict(factory_cfg) == dataclasses.asdict(inline_cfg), \
f"XAU factory config differs from inline construction for {tf}"
@test("M5 factory disables XAU_PB; other TFs keep the global default")
def _():
# 2026-08-04 campaign (Oanda bars, $0.65 real spread): pullback
# entries are the largest wrong-direction loss source on M5 and
# fail at real cost; disabled in the M5 factory branch only.
from pair_signals import PAIR_PRODUCERS
m5 = PAIR_PRODUCERS[XAUUSD](Timeframe.M5, [Timeframe.M15, Timeframe.H1])
assert m5.xau_strategy.m5_pullback_enabled is False
assert m5.technical.ema_partial_neutral is True # 2026-08-04 override 5
m15 = PAIR_PRODUCERS[XAUUSD](Timeframe.M15, [Timeframe.M5, Timeframe.H1])
assert m15.xau_strategy.m5_pullback_enabled is True, \
"PB flag must stay at the global default off-M5 (inert there)"
assert m15.technical.ema_partial_neutral is False, \
"partial-alignment squash is M5-validated only"
@test("produce_signal raises for unregistered pair")
def _():
from pair_signals import produce_signal
@@ -4634,6 +4682,9 @@ def _():
def run(k):
agg = _bc_agg(k=k)
agg.config.xau_strategy.cp_enabled = True
# Force-enable PB: this test exercises gate mechanics, not the
# tuned M5 default (PB off since 2026-08-04, xauusd.py docstring).
agg.config.xau_strategy.m5_pullback_enabled = True
calls = []
agg._try_m5_pullback = lambda *a, **kw: calls.append("pb")
agg._try_chart_pattern = lambda *a, **kw: calls.append("cp")