Files
ForexBot2/pair_signals/xauusd.py
T
Harwinder Sekhon 1793d9167a Daily-trend veto on M15 + lot-size / already-open lines (campaign 2026-09-30), and the 2026-10-01 whole-codebase bug review
Two bodies of work that touch the same files, so one commit.

Direction and setup-size campaign (2026-09-30 → 10-01, owner's RISK-FIRST rule,
written before the runs):
- SIGNAL: M15 daily-trend veto (factory override 7, xau_strategy.daily_trend_veto
  "both"). A finished setup against a clear daily trend (closed D1 bars, Sunday
  stubs dropped; close vs its 50-bar EMA AND vs the close 60 kept days earlier)
  is removed as the last step of the core. One definition
  (signal_aggregator.daily_trend_series) for live, the bar replay and the twin's
  host side; D1 is a required frame of the M15 scan; mirrored in
  cuda_signal_kernels.py K2. W3 824 / +205.12R / PF 1.73 / max DD 12.4R vs
  1105 / +195.79R / 1.49 / 20.9R; 2019 → 2026 1473 / +223.33R / 1.42 / 41.5R vs
  2091 / +192.53R / 1.24 / 64.8R; unseen holdout −4.62R vs −6.63R.
- FAILED, not shipped: the veto on M5 (W3 drawdown 10.1R → 15.2R) and on H1
  (−9.7R), buy-only and sell-only on M15, the wide-stop cap
  (xau_strategy.wide_stop_cap_atr stays 0.0, a research knob).
- New M15 pins: W2 dfc647ee…, OOS e87b6de1…, W3 ad3b0dbd…. M5 / H1 / H4 pins
  unchanged. --set xau_strategy.daily_trend_veto=off reproduces the previous M15
  pins byte for byte. Twin and reference script MD5-equal on every window;
  recorder and feature-export parity re-proved; base-rate table rebuilt.
- DISPLAY (books byte-identical): exposure.py — the lot size for
  trade_size.risk_per_trade (new config.json block, off by default, private to
  the owner) and the count of alerted same-direction plans still open
  (signals.alerted_at, read-only open_alerted_setups); the daily trend in words
  on Telegram, the CLI and the dashboard; signals.daily_trend logged.
- providers/oanda: a date window with no bars is an empty page, not an error
  (a D1 pad starting on a Saturday could not be fetched).
- Reports only, nothing shipped: seven context screens (no state nominated) and
  eight outside data sources (none adds a forecast to gold's own daily trend).
- Independent verification (report section 5.13): every figure reproduced on
  both scripts, scoring identical to the previous commit on all four
  timeframes, no look-ahead. Caveats recorded there: this is a risk filter, not
  a more accurate direction call; nine tenths of the 2019 → 2026 gain is 2022;
  the neighbour clause is thin (a simple 100-day average misses the R line by
  1.2R); "never adds a trade" is an observation, not a guarantee.

Bug review of the whole codebase (2026-10-01). Signal path clean. 17 defects
elsewhere, plus 4 found by an independent review of the fixes; all fixed, each
with a test; no trade book changed:
- telegram_bot: one _deliver_alert for the candle scan and the level break. The
  row is stamped alerted_at BEFORE the send, on a one-worker alert-db pool, and
  un-stamped when no chat received it or the task is cancelled mid-stamp, so
  two alerts at one candle close count each other and no SQLite write runs on
  the event loop. The 5-bar cooldown counts bars of the scan's own frame
  (== gpu_backtest._apply_cooldown; clock time released the 22:00 UTC bar after
  the settlement pause) and is restored from signals.db at start-up. The
  lot-size line goes to the owner's own chat only (a group the owner ran
  /autoscan in used to receive it). /status gains a news-model line and escapes
  failure details.
- signal_log: mark_alerted returns the stamp it wrote; unmark_alerted;
  last_alerted_bars (read-only).
- advisory_outcome: candle continuity is judged bar by bar inside the walk; a
  hole after a trade's exit no longer voids it. The stored-candle replay
  resolves 108 of 111 logged setups instead of 99, all equal to the live
  resolver's R.
- providers/oanda: an empty date window is accepted only when it is 5 days or
  shorter and no malformed candle was skipped; a longer one is retried, then
  refused, instead of being cached as history with a hole.
- news_engine: blank RSS titles skipped; one unreadable headline no longer
  costs the local model its whole batch (checked on the real bundle); model
  health recorded; no DeepSeek audit call in a process that does not archive;
  event-direction cache keyed per release; peek_week_events never fetches.
- llm_client: chat_json never raises and never logs the API key (requests
  quotes the Authorization value in InvalidHeader; error bodies are redacted).
- signal_aggregator: the scan's news block is guarded; the replay path
  (use_news=False) assigns exactly the values it did before.
- agent_panel: PANEL_PROMPT_VERSION v2.2 — same prompt text, corrected input
  (event timing over the cached calendar week, headlines newest first).
  scripts/panel_replay.py replays the logged payload per persona;
  scripts/label_scan_outcomes.py counts with an ISO cutoff.
- test_all.py: no ALTER TABLE … DROP COLUMN (the live box has SQLite 3.34);
  tests that write Parquet print a notice and return without pyarrow.

Verification after the last code change: suite 632 passed, 0 failed (also with
both Parquet engines blocked and FB2_BACKEND=cpu, the live box's view); the
five reference MD5s reproduce on both backtesters, 0 cache fallbacks; Module 37
green; static Python 3.9 / SQLite 3.34 check clean on every touched file.

Evidence: backtest/research/daily_trend_campaign_2026-09-30.md,
context_screens_2026-10.md, new_inputs_2026-10.md; CLAUDE.md sections 2, 4, 6,
8, 9, 10 and 11; pair_signals/xauusd.py docstring (M15 override 7).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-01 10:28:28 -04:00

387 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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).
M15 override 7 — daily-trend veto (validated 2026-09-30,
backtest/research/daily_trend_campaign_2026-09-30.md, pre-registered
RISK-FIRST rule approved by the owner that day, CUDA twin runs paired
with gpu_backtest.py MD5s, $0.65): daily_trend_veto="both". The engine
sees at most the H1 chart. The daily trend is +1 when the last CLOSED
daily bar closes above its 50-day EMA AND above the close 60 daily
bars earlier, -1 when below both, else 0 (Oanda's Sunday stub bars are
dropped; signal_aggregator.daily_trend_series). After every setup
archetype has run, a setup AGAINST a non-zero state is dropped (grade
D, no setup, warning "DAILY TREND FILTER: ..."). Candidate vs control:
3-year W3 2023-09-05 -> 2026-09-05 824/+205.12R PF 1.73 DD 12.4R vs
1105/+195.79R PF 1.49 DD 20.9R (halves +76.7/+128.4 vs +76.9/+118.9;
at $0.30 +221.18R PF 1.81 DD 11.4R vs +216.53R PF 1.55 DD 19.6R);
7.6-year LONG 2019-01-01 -> 2026-08-07 1473/+223.33R PF 1.42 DD 41.5R
vs 2091/+192.53R PF 1.24 DD 64.8R (2022 +3.2R vs -24.6R; 2021 NOT
improved, -27.3R vs -25.6R; 2023 worse, +1.8R vs +11.7R); the unseen
HOLDOUT 2026-09-05 -> 2026-10-01 18/-4.62R vs 24/-6.63R; 3 of 4
neighbouring definitions pass (90-day change, 120-day change, 100-day
average; the 60-day change alone misses the 97% line by 1.7R). Trade
diff W3: 281 removed (-9.3R: BUY 106/+7.6R, SELL 175/-16.9R), 0 added;
the removed trades are break-even after costs (-0.03R each) against
+0.25R for the kept ones, so the veto drops the no-edge quarter of the
book rather than a pocket of big losers. ACCEPTED COST (owner decision
2026-09-30): R given up while gold only rises — 2-year W2 597/+155.89R
PF 1.78 DD 11.4R vs 813/+154.15R PF 1.53 DD 15.6R with the first half
+61.8R vs +70.1R, and the old OOS window 2024-06-01 -> 2025-03-30
258/+63.78R PF 1.80 DD 9.1R vs 313/+68.50R PF 1.67 DD 12.4R. The
2026-09-26 rule template (no half and no OOS window may lose) would
have rejected it; the risk-first rule (3-year R >= 97%, PF +0.10, DD
not deeper, long window better on R, DD and 2021+2022, holdout, both
costs, plateau) is the owner's deliberate choice. M15 ONLY: the same
veto FAILED on M5 ("both": W3 DD 10.1 -> 15.2R and the long window
worse on R and DD; "buy": passes W3, loses the long window, no
plateau) and on H1 (W3 +60.3R vs +70.0R). "buy"-only and "sell"-only
FAILED on M15 as well ("sell" has the best W3 total, +212.7R, and a
LONG DD of 76.4R vs 64.8R: in falling years the buys against the daily
trend are the losers). The live wrapper and both backtesters load D1
as an extra REQUIRED frame while the veto is on.
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"]
# Daily-trend veto on M15 only: a finished setup against a clear
# daily trend (price vs its 50-day average AND the 60-day change)
# is dropped. The live wrapper and both backtesters load the D1
# frame for it — see module docstring (M15 override 7, validated
# 2026-09-30). M5, H1 and H4 FAILED the same rule and stay "off".
config.xau_strategy.daily_trend_veto = "both"
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