"""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). M15 override 5 — RSI regime multiplier x 0.5 in every regime (validated 2026-09-09, 2-year Oanda window 2024-09-05 -> 2026-09-05 at $0.65, pre-registered rule in backtest/research/ weight_test_2026-09-09.md): 793/+117.62R PF 1.41 DD 14.8R -> 931/+146.14R PF 1.43 DD 16.1R, fixed halves +27.9/+0.6R vs control; OOS 2024-06-01 -> 2025-03-30 300/+40.59R PF 1.38 -> 355/+60.67R PF 1.49; 3-year 1092/+151.90R PF 1.38 -> 1273/+186.61R PF 1.39 (max DD 15.5 -> 19.3R, all in the 2023-24 year; 2026 -4R is the one calendar year that does not improve). Mechanism: RSI is neutral on ~73% of M15 entries and votes against the trend when it fires; halving it lets more trend-following entries clear the score band (210 added trades +26.8R, mostly B-grade BUYs; 72 removed -7.1R). Candidate came from the recorded replay screen (replay_recording_2026-09-09.md); the same screen's M5 picks (MACD-down) all FAILED the replay, and ema_trend x 2 / divergence -> 0 on M15 failed too. Canonical M15 book re-pinned to backtest/oanda_m15_2yr_065_20260909.csv. M15 override 6 — grade-A SELLs blocked (validated 2026-09-26, backtest/research/rr_and_sell_campaign_2026-09-26.md, pre-registered rule, CUDA twin runs paired with gpu_backtest.py MD5s, $0.65): sell_min_quality=["A+", "B"] on M15, so gate 6e downgrades every grade-A short to D (A+ and B shorts still trade). The bucket was the one cycle-2 candidate from the 2026-09-10 screen: M15 grade-A SELLs 230 / -14.4R on the 3-year window, negative in every 12-month slice and in the OOS window. Candidate vs control: 2-year W2 2024-09-05 -> 2026-09-05 813/+154.15R PF 1.53 DD 15.6R (halves +70.1/+84.0) vs 931/+146.14R PF 1.43 DD 16.1R (+66.1/+80.0); OOS 2024-06-01 -> 2025-03-30 313/+68.50R PF 1.67 DD 12.4R vs 355/+60.67R PF 1.49 DD 17.1R; 3-year W3 2023-09-05 -> 2026-09-05 1105/+195.79R PF 1.49 DD 20.9R vs 1273/+186.61R PF 1.39 DD 19.3R (+8%, inside the 10% clause), better in 2024, 2025 and 2026, -2.4R in the 4 months of 2023; at $0.30 +167.53R vs +161.44R (W2). Trade diff on W2: 162 removed (-8.1R, all grade-A SELLs), 44 added (-0.1R, cooldown displacement). Counter-fact on record: M15 A-grade shorts paid in the 2021-22 bear phase (m15_5yr_cuda_2026-09-18.md), outside the owner's 3-year window. Tested and NOT shipped in the same campaign: signals.min_rr_ratio 1.0 on M15 (trade-identical, -2.0R W2, -2.8R W3: the swing-anchored GENERIC stop is not better than the 2 x ATR stop on the same grade-A signals); the combination with this block (-0.7R vs the block alone). 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 Dict, Optional, List from config import PlatformConfig, TechnicalConfig, 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 _m15_regime_weights() -> Dict[str, Dict[str, float]]: """Shipped regime weight table with the RSI multiplier halved in every regime — the M15-only override validated 2026-09-09 (see the module docstring, M15 override 5). Derived from the default table so the intent ("RSI x 0.5, everything else as shipped") stays explicit; the Module 9 equivalence test pins the resulting numbers.""" from engine import regime_weight_table # local import: keep the factory import-light table = regime_weight_table(TechnicalConfig()) # a fresh TechnicalConfig has no override -> the shipped table return {regime: {name: (mult * 0.5 if name == "rsi" else mult) for name, mult in weights.items()} for regime, weights in table.items()} 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 # RSI regime multiplier × 0.5 in every regime on M15 only — see # module docstring (M15 override 5, validated 2026-09-09). config.technical.regime_weights_override = _m15_regime_weights() # Grade-A SELLs blocked on M15 only (gate 6e downgrades an # actionable-grade short whose grade is not listed) — see module # docstring (M15 override 6, validated 2026-09-26). config.xau_strategy.sell_min_quality = ["A+", "B"] 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