Files
ForexBot2/pair_signals/xauusd.py
T
Harwinder Sekhon 9a8a2c1aa1 xauusd factory: block M5 shorts in transitioning regime, block all H1 shorts (rule-1 validated)
M5 sell_blocked_regimes=[transitioning]: 12mo +7.9R (PF 1.50->1.56, both halves up),
5yr +5.3R, OOS unchanged. H1 sell_min_quality=[]: 5yr +16.7R (PF 1.18->1.46),
OOS +11.5R (PF 1.13->1.74), 12mo -1.8R with both halves positive.
Campaign table, rejected and parked candidates: backtest/research/signal_log_campaign_2026-09.md

Claude-Session: https://claude.ai/code/session_01FC4s6VCURFte4dub4xKxYL
2026-09-03 20:47:33 -04:00

282 lines
16 KiB
Python

"""XAU/USD signal producer config.
PlatformConfig defaults ARE the tuned XAU/USD configuration, so this
factory sets the timeframes plus documented per-timeframe overrides.
It must stay equivalent to the inline PlatformConfig(symbol=XAUUSD,
...) construction (plus those overrides) used by the backtester and
Telegram bot (enforced by a test in test_all.py).
M15 override (validated 2026-07-11, window 2025-03-30 -> 2026-05-31,
$0.30 spread, trailing exits): tp_floor_rr 1.0 scores +116.6R/460
trades PF 1.83 vs +105.4R/424 PF 1.82 at the 0.5 default — better in
both train/val halves and on the 2024-06 -> 2025-03 out-of-sample
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
(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.
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).
M15 override 3 (validated 2026-07-16, 5-year replay 2021-07-17 ->
2026-07-16 under the audit-fixed backtester, $0.30 spread, trailing
exits): block SELL entries whose bar CLOSES at 15:00 or 17:00 UTC
(sell_blocked_hours_utc=[15, 17], gate 6g3 — BUYs unaffected; those
hours flank the already-blocked hour 16). The pocket lost -33.5R/165
trades, negative in 6/6 calendar years and in BOTH halves of the last
12 months (H1 -6.8R, H2 -4.2R); each hour independently is 5/6 years
negative with both halves negative (hours 14/18 fail that screen and
stay open). Cross-TF the M5 book CONFIRMS (same pocket -14.1R/59,
5/6 years negative) rather than contradicts — unlike the rejected
trending_down block. Full replay with the gate: +176.7R/1627 PF 1.31
DD 29.1R vs +144.9R/1781 PF 1.22 DD 34.4R control; better in ALL SIX
years; last-12mo halves both improve (H1 +36.3 vs +29.5, H2 +46.9 vs
+43.2; 12mo PF 1.55 vs 1.43); OOS 2024-06-01 -> 2025-03-30 improves
(+45.4R PF 1.47 DD 17.8 vs +41.9R PF 1.39 DD 21.7). SELL side goes
-1.2R -> +30.6R over 5 years. BUYs at those hours made +34.3R, hence
SELL-only rather than a blocked_hours_utc entry. Mechanism: NY-session
shorts against gold's macro uptrend (15:00 = post-US-data reversal
window, 17:00 = London close unwind).
M5 override 2 (2026-07-16, chart patterns): enable BUY-only CP on M5
(inv_hs, double_bottom, bull_flag — drop all bearish patterns). SELL CP
is consistently negative (OOS -3.21R, 5yr +3.76R PF 1.07) while BUY CP
is positive across windows: 12mo +41.8R PF 1.60 (train +9.66 PF 1.30,
val +32.17 PF 1.86), 5yr +65.2R PF 1.48, OOS +1.20R (aggregate
positive, val half -1.89R — marginal but sign-correct in aggregate).
Gold's structural uptrend makes bullish patterns fundamentally more
reliable. M15/H1 fail on all windows; M5-only enablement.
M5 override 3 (validated 2026-07-21, 5-year replay 2021-07-21 ->
2026-07-21, $0.30 spread, next-open fills): trailing distance widened
to 0.85R (trail_dist_r 0.85, arm unchanged 0.60). Same 30-cell grid
methodology as the M15 campaign, opposite finding — on M5 the arm
point barely matters; the winners need a looser leash. Plateau arm
0.5-0.7 x dist 0.85-1.0 (+227..+234R vs +218.0R baseline). Chosen
0.60/0.85 (OOS-preferred end of the plateau): +231.2R/2028 PF 1.30 WR
57.1% vs +218.0R PF 1.29 WR 58.3%; both last-12mo halves better
(+56.8/+115.1 vs +54.0/+104.6); 5yr max DD 19.2R vs 19.1R (noise);
OOS 2024-06-01 -> 2025-03-30 +14.5R DD 15.1 vs +11.1R DD 15.4.
dist 1.0 was REJECTED at the OOS gate (+10.9R < baseline +11.1R)
despite the best in-sample total (+233.3R) — the OOS ordering picks
the conservative plateau edge. XAU_PB trades are unaffected (they
carry their own validated pb_trail 0.8/0.75 per-trade overrides);
XAU_CP trades share the global params and were validated inside this
book. M15 arms at 0.70 (its own campaign, same day); the divergence
is the per-TF pattern yet again.
M15 override 4 (validated 2026-07-21, 5-year replay 2021-07-21 ->
2026-07-21, $0.30 spread, next-open fills): trailing-stop arm raised to
0.70R (trail_arm_r 0.70, dist unchanged 0.75). Full grid arm 0.3-0.9 x
dist 0.5-1.0 (30 cells): every earlier-arm cell (0.3/0.45) is WORSE —
the "arm earlier to save fading trades" hypothesis is refuted; the
plateau is arm 0.70-0.80 x dist 0.70-0.85 (all >= +190R). Chosen
plateau center 0.70/0.75: +210.7R/1629 PF 1.35 WR 57.8% vs baseline
0.60/0.75 +175.7R PF 1.30 WR 53.9%; both last-12mo halves improve
(+49.7/+50.0 vs +36.4/+46.0); per-year better 4/6 (2022 -0.9R, 2023
-1.4R, both within tolerance); OOS 2024-06-01 -> 2025-03-30 +57.4R
PF 1.57 DD 17.0R vs +45.4R PF 1.47 DD 17.8R — better on every OOS
metric. One clause waived BY OWNER DECISION 2026-07-21: 5yr max DD is
33.9R vs 29.1R baseline (+4.8R deeper worst episode, 2022-23 chop),
accepted in exchange for +20% total R. M5 keeps the global 0.6 arm
(same per-TF divergence pattern as tp_floor_rr and the hour-9 block).
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.
M5 override 2 — relaxation campaign (validated 2026-07-30, Oanda
bars, $0.30 spread, trailing exits): UNBLOCK hour 21 UTC on M5 only,
combined with the config.py M5 gate relaxations (m5_min_abs_score
30->25, m5_min_confirmation_score 10->5). The combined config beat
the pre-relaxation control in ALL 6 calendar years of the 5yr replay
2021-07-30 -> 2026-07-30 (+230.9R/2319 PF 1.26 DD 21.3 vs
+168.0R/2048 PF 1.21 DD 27.0), on 12mo (+107.4R PF 1.36, halves
+68.6/+38.8 vs +84.6R PF 1.33), on OOS 2024-06-01 -> 2025-03-30
(+26.1R vs +17.2R), and at $0.70 spread (+86.9R vs +66.4R). Hour-21
trades alone: +28.0R/22, positive every year — overturning the old
block per the full-evidence rule. Hour 21 stays BLOCKED on M15+
(12mo M15 unblock tested -0.3R). Declined-but-open from the same
campaign: an M5 hour-22 block (+4.8R on 12mo — the burst-heavy
"institutional" hour — but -3.2R on 5yr and -1.1R on OOS: regime-
specific); M15 hour-16 unblock (+0.8R/+57 trades 12mo but -18.3R
DD +34% on 5yr); every SELL-gate/vol/TP-size relaxation retested
negative (see memory notes for the full grid).
M5 override 4 — XAU_PB disabled (validated 2026-08-04, Oanda bars,
$0.65 REAL spread — first campaign validated at measured cost on the
live feed, per the 2026-08-04 re-pin baseline): m5_pullback_enabled
False on M5. Loss forensics on the Oanda books identified XAU_PB as
the largest wrong-direction loss source (12mo: -43.5R of the -156.6R
wrong-direction bucket; 5yr: -274.5R). Disabling: 12mo +96.6R/588
PF 1.44 max DD 13.9R halves +51.2/+45.4 vs +82.3R/788 PF 1.26 DD
23.9R shipped (which FAILS the PF>=1.3 clause at real cost); OOS
2024-06-01 -> 2025-03-30 +9.5R PF 1.34; 5yr +123.9R/956 PF 1.35 vs
PF 1.16 with PB. PB's 5yr net contribution (+23.6R) was almost
entirely the 2022 chop year (+36.1R vs -2.2R without) — a regime
earner that has been a drag on trend-regime bars since; its tight
stops are also the most cost-punished at $0.65. PB code and the
pb_trail_* params STAY for future regime-gated experiments (same
pattern as cp_enabled). Rejected in the same campaign (4 candidates
total, N logged): hour-19 block (+3.7R 12mo but -3.3R OOS, -1.5R
5yr — OOS gate, the classic trap); hours-14/15 block (-6.9R 12mo
once PB is removed — their raw negativity WAS the PB losses);
trail_dist_r 0.75 (-2.6R 12mo; the 0.88R average give-back is the
validated price of riding trends, exits confirmed NOT the loss
driver: exit-management accounts for 1-2% of lost R vs 50%
wrong-direction / 49% entry-timing).
M5 override 6 — SELL block in transitioning regime (validated
2026-09-03, signal-log campaign, `backtest/research/
signal_log_campaign_2026-09.md`, $0.65 round trip, same CPU harness
as its control): sell_blocked_regimes=["transitioning"]. Live
evidence: M5 SELLs in transitioning were -7.9R/12mo PF 0.21 and
-5.2R/5yr PF 0.49 in the pinned books; the live signal log showed the
same pocket -7.9R/PF 0.56 over 3 weeks. Blocking: 12mo (2025-08-14 ->
2026-08-14) 557 trades +112.21R PF 1.56 (halves +55.9/+56.4, PF
1.54/1.58) vs control 575/+104.34R PF 1.50; 5yr (2021-08-01 ->
2026-08-04) 889/+133.7R PF 1.41 vs 911/+128.4R PF 1.39; OOS
2024-06-01 -> 2025-03-30 unchanged (86/+9.45R PF 1.33 — no such
trades in that window). Removed trades: 18 in 12mo (-7.9R), 22 in
5yr (-5.3R).
H1 override — all SELL entries blocked (validated 2026-09-03, same
campaign): sell_min_quality=[] (gate 6e in signal_aggregator.py
downgrades any actionable-grade SELL whose quality is not in this
list to D, so an empty list downgrades every H1 short). H1 shorts
were a structural loser: -16.7R over the 5yr control window and
-11.5R OOS. Blocking: 12mo (2025-08-04 -> 2026-08-04) 91/+22.2R
PF 1.79 (halves +20.6/+1.6) vs control 138/+24.0R PF 1.55 (12mo cost
-1.8R); 5yr 335/+49.1R PF 1.46 vs 515/+32.4R PF 1.18; OOS 78/+16.7R
PF 1.74 vs 104/+5.1R PF 1.13. New reference books:
backtest/oanda_h1_{12mo,5yr,oos}_065_20260903.csv,
backtest/oanda_m5_12mo_065_nopb_20260903.csv,
backtest/oanda_m5_5yr_065_nopb_20260903.csv.
Rejected/parked in the same campaign (see the memo for the full
table): M15 sell_min_quality=["B"] (5yr -8.9R, max DD 35.8 -> 41.4R
— REJECTED); Bollinger weight 0 in trending regimes on M5, M15, H1
(M5 -20.4R/12mo, -15.5R/5yr; M15 -24.0R/12mo — REJECTED across the
board); divergence disabled on M15 (R improves +7.4/+11.9/+13.5R
across windows but 5yr max DD 35.8 -> 47.4R with PF flat — DECLINED,
owner call); divergence disabled on H1 (+4.6/+8.1/+2.4R, PF
1.62/1.20/1.17) — PARKED: it passes rule 1 in combination with the
H1 no-short override (b12_h1_nosell_nodiv: 12mo 100/+23.1R PF 1.75,
halves +21.8/+1.3; 5yr 364/+58.7R PF 1.52; OOS 88/+21.9R PF 1.92 —
better than the no-shorts override alone on both 5yr and OOS), but
shipping it would route through technical.disabled_indicators, which
CLAUDE.md rule 8 reserves for experiments; owner decision, recommended
vehicle is an H1-only regime_weights_override with divergence weight
0 plus a fresh validation run.
M5 override 5 — EMA partial-alignment squash (validated 2026-08-04,
Oanda bars, $0.65 real spread): ema_partial_neutral True on M5 —
partial 4-EMA ribbon alignment (2-3 of 4 checks) scores NEUTRAL
instead of a weak BUY/SELL. Motivated by the indicator audit: partial-
alignment trades averaged -0.008R/84 vs +0.230R/341 fully aligned.
Replay (knob mirrored in the backtester's precomputed EMA scorer in
the same commit — live==backtest parity): 12mo +97.6R/556 PF 1.48
DD 10.7R halves +47.8/+49.8 vs control +96.6R/588 PF 1.44 DD 13.9R;
5yr +127.4R/912 PF 1.38 DD 12.8R vs +123.9R/956 PF 1.35 DD 13.2R,
2022 improves -2.2 -> -1.1; OOS 2024-06-01 -> 2025-03-30 +9.4R
PF 1.33 vs +9.5R PF 1.34 — a one-trade tie, documented as such (the
OOS window holds up; it neither prefers nor punishes). Net effect:
32 fewer marginal trades, every risk-adjusted measure better.
Rejected in the same round (trial-logged): confirmation-TF swap
M5->[M15,H4] (12mo +104.0R PF 1.51 DD 10.3 but OOS +7.8R PF 1.27
vs control +9.5R PF 1.34 and 2022 3x worse — the H4 confirm keeps
endorsing stale direction in chop; OOS-ordering rejection, same
precedent as trail dist 1.0); confirm [H1,H4] (-31.8R 12mo);
triple-confirm [M15,H1,H4] (-4.9R); divergence weight -1/3
(-3.8R 12mo); candlestick weight +50% (-3.0R 12mo). The weight
vector keeps proving calibrated as a system: divergence/candlestick/
bollinger all look mis-weighted in-book and all replay worse when
"fixed" (bollinger weight-0: +80.9R PF 1.29 DD 23.8 — its constant
oppose vote is a load-bearing quality bar, like ROC).
"""
from typing import Optional, List
from config import PlatformConfig, Timeframe
from app_config import apply_pair_overrides
_CONFIRMATIONS = {
Timeframe.M5: [Timeframe.M15, Timeframe.H1],
Timeframe.M15: [Timeframe.M5, Timeframe.H1],
Timeframe.H1: [Timeframe.M15, Timeframe.H4],
Timeframe.H4: [Timeframe.H1, Timeframe.D1],
}
def make_config(
primary_tf: Timeframe,
confirmation_tfs: Optional[List[Timeframe]] = None,
) -> PlatformConfig:
config = PlatformConfig()
# Deployment overlay (config.json) BEFORE the validated per-TF
# deltas below, so e.g. the M5 hour-9 append lands on the JSON base.
apply_pair_overrides(config)
config.primary_timeframe = primary_tf
config.confirmation_timeframes = (
confirmation_tfs or _CONFIRMATIONS.get(primary_tf, [Timeframe.M15, Timeframe.H4])
)
if primary_tf == Timeframe.M5:
# Block hour 9 UTC on M5 only; UNBLOCK hour 21 on M5 only
# (positive every year of the 5yr replay) — see docstring.
config.xau_strategy.blocked_hours_utc = [
h for h in config.xau_strategy.blocked_hours_utc if h != 21
] + [9]
# Deliberately NOT mirrored into config.backtest.blocked_hours_utc:
# that copy is the generic non-XAU fallback carrying the JSON base
# only. Propagating a gold-validated per-TF delta there would leak
# XAU-calibrated hours to a future pair (pair-isolation rule).
# Every XAU consumer branches to the xau_strategy copy. Enforced
# by the Module 13 test "M5 factory appends hour 9 on top of a
# JSON base".
# BUY-only chart patterns on M5 — see module docstring.
config.xau_strategy.cp_enabled = True
config.xau_strategy.cp_patterns = ["inv_hs", "double_bottom", "bull_flag"]
# XAU_PB pullback entries OFF — largest wrong-direction loss
# source at real cost on Oanda bars — see module docstring.
config.xau_strategy.m5_pullback_enabled = False
# Partial EMA-ribbon alignment scores NEUTRAL on M5 — its
# trades carried no edge — see module docstring.
config.technical.ema_partial_neutral = True
# Wider trailing distance on M5 only — see module docstring.
config.signals.trail_arm_r = 0.60
config.signals.trail_dist_r = 0.85
# Block SELL entries while the regime is "transitioning" on M5
# only — see module docstring (M5 override 6).
config.xau_strategy.sell_blocked_regimes = ["transitioning"]
if primary_tf == Timeframe.M15:
# 1R minimum target on M15 only — see module docstring.
config.signals.tp_floor_rr = 1.0
# Block the mixed "trending" regime on M15 only — see docstring.
config.xau_strategy.blocked_regimes = ["trending"]
# Block SELL entries at 15:00/17:00 UTC on M15 only — see docstring.
config.xau_strategy.sell_blocked_hours_utc = [15, 17]
# Later trailing-stop arm on M15 only — see module docstring.
config.signals.trail_arm_r = 0.70
config.signals.trail_dist_r = 0.75
if primary_tf == Timeframe.H1:
# Block every H1 SELL — the SELL quality gate (6e in
# signal_aggregator.py) downgrades any actionable-grade short
# to D when its grade is not in this list; an empty list means
# no grade qualifies. H1-only — see module docstring.
config.xau_strategy.sell_min_quality = []
return config