1793d9167a
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>
387 lines
23 KiB
Python
387 lines
23 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).
|
||
|
||
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
|