xauusd M15: halve the RSI regime multiplier in every regime (M15 override 5, rule-1 validated)

Owner-approved 2026-09-09 after the pre-registered weight test
(backtest/research/weight_test_2026-09-09.md). M15-only
technical.regime_weights_override = the shipped table with rsi x 0.5
(0.5 trending family / 0.75 ranging / 0.5 transitioning), derived in the
factory and pinned literally by the Module 9 equivalence test; a Module 33
test locks that M5/H1/H4 carry no override.

Evidence, 2-year Oanda window 2024-09-05..2026-09-05 at $0.65: 793/+117.62R
PF 1.41 -> 931/+146.14R PF 1.43 (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
+151.90R -> +186.61R (max DD 15.5 -> 19.3R, 2026 -4R). Parity: the factory
run with no --set flag reproduces the validated candidate books byte-for-
byte (2-yr MD5 d185f099..., OOS 6f8f24b4...). M15 canonical book re-pinned
to backtest/oanda_m15_2yr_065_20260909.csv. Docs carry a one-line note.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nc9Ninzyi18QkWezrxeBvB
This commit is contained in:
Harwinder Sekhon
2026-09-09 17:36:41 -04:00
parent f6c7635c59
commit 2ac73017b8
6 changed files with 115 additions and 7 deletions
+4 -4
View File
File diff suppressed because one or more lines are too long
@@ -171,3 +171,17 @@ recent one does not, and the 3-year max drawdown is 3.8R deeper (all of it in th
(931 / +146.14R / PF 1.43 at $0.65). Not shipped in this campaign. (931 / +146.14R / PF 1.43 at $0.65). Not shipped in this campaign.
Books and logs: `backtest/research/replay_2026-09/candidates/` and `.../oos/` (gitignored). Books and logs: `backtest/research/replay_2026-09/candidates/` and `.../oos/` (gitignored).
## Shipped (owner decision 2026-09-09, same day)
M15 override 5 in `pair_signals/xauusd.py`: `technical.regime_weights_override` = the shipped
table with the RSI multiplier halved in every regime (0.5 trending family / 0.75 ranging /
0.5 transitioning), derived from the default table and pinned literally by the Module 9
factory-equivalence test; one more Module 33 test locks that M5/H1/H4 carry no override.
Parity proof — the factory run with NO `--set` flag reproduces the validated candidate books
byte-for-byte: 2-year MD5 `d185f099482a07f29502cb5a371a9bbf` (931 / +146.14R / PF 1.43) and
OOS MD5 `6f8f24b49597ced28f50a19e08c3472f` (355 / +60.67R / PF 1.49). Canonical M15 book
re-pinned to `backtest/oanda_m15_2yr_065_20260909.csv` (copy of the 2-year factory book).
Docs: `docs/user_guide.html` and `docs/beginner_guide.html` carry a one-line note (PDF builds
remain behind, as before). Suite 499 tests. Live M15 scans get a new `engine_fingerprint`
from the next deploy; signal-log comparisons across it must split at the deploy time.
+1
View File
@@ -138,6 +138,7 @@ The message says: <i>"once price hits 4,159, trail SL $33 behind best price."</i
<li><b>H1 trades are held longer</b> — often overnight, sometimes over days. That is normal. Your stop loss is working for you while you sleep.</li> <li><b>H1 trades are held longer</b> — often overnight, sometimes over days. That is normal. Your stop loss is working for you while you sleep.</li>
<li><b>H1 is BUY-only.</b> All H1 SELL signals are blocked (since 2026-09-03) because they were a losing side on this timeframe — expect to see BUY signals here, never SELL.</li> <li><b>H1 is BUY-only.</b> All H1 SELL signals are blocked (since 2026-09-03) because they were a losing side on this timeframe — expect to see BUY signals here, never SELL.</li>
<li><b>M5 also holds back some SELL signals</b> — specifically while the market is in a choppy "transitioning" phase, since those shorts tested poorly.</li> <li><b>M5 also holds back some SELL signals</b> — specifically while the market is in a choppy "transitioning" phase, since those shorts tested poorly.</li>
<li><b>M15 got a small tune-up on 2026-09-09</b> — one of its nine indicators (RSI) now counts for half as much, which means a few more M15 signals per week. Nothing changes in how you act on a signal.</li>
<li><b>Never take the same signal on two timeframes.</b> They are usually the same gold move seen at different zoom levels. Taking both doubles your risk, not your profit.</li> <li><b>Never take the same signal on two timeframes.</b> They are usually the same gold move seen at different zoom levels. Taking both doubles your risk, not your profit.</li>
</ul> </ul>
+1 -1
View File
@@ -98,7 +98,7 @@
</table> </table>
<ul> <ul>
<li><b>M5 — intraday, execution-critical.</b> Highest total edge, but stops are tight: a couple of dollars of slippage or a late entry eats a meaningful share of every trade. Suitable only if you can act within a minute or two of the alert (or automate). Trades open and close within a session; you sleep flat. SELL signals are held back while the market regime reads "transitioning" — a validated filter, not a bug if you notice fewer shorts there.</li> <li><b>M5 — intraday, execution-critical.</b> Highest total edge, but stops are tight: a couple of dollars of slippage or a late entry eats a meaningful share of every trade. Suitable only if you can act within a minute or two of the alert (or automate). Trades open and close within a session; you sleep flat. SELL signals are held back while the market regime reads "transitioning" — a validated filter, not a bug if you notice fewer shorts there.</li>
<li><b>M15 — the middle path.</b> Same frequency as M5, wider stops, up to 15 minutes to act. About half your trades will still be open overnight.</li> <li><b>M15 — the middle path.</b> Same frequency as M5, wider stops, up to 15 minutes to act. About half your trades will still be open overnight. Since 2026-09-09 the M15 scoring gives its RSI reading half the weight it used to (RSI mostly voted against the trend on this timeframe), so expect roughly 9 signals a week instead of 7–8; in the two-year test (Sep 2024 – Sep 2026) that change lifted M15 from +117.6R to +146.1R with the same win rate and a slightly better profit factor, and it also improved the separate out-of-sample period. The M15 column above predates it.</li>
<li><b>H1 — swing trading around a normal life.</b> Best quality per trade and very forgiving of slow manual execution (you have up to an hour). The costs: positions held for days, through news and overnight sessions, and a <b>lumpy</b> equity curve — one quarter of the test year was negative and most of the profit came in a single quarter. Judge it on quarters, not weeks. H1 no longer issues SELL signals at all (blocked since 2026-09-03) — it is BUY-only on this timeframe.</li> <li><b>H1 — swing trading around a normal life.</b> Best quality per trade and very forgiving of slow manual execution (you have up to an hour). The costs: positions held for days, through news and overnight sessions, and a <b>lumpy</b> equity curve — one quarter of the test year was negative and most of the profit came in a single quarter. Judge it on quarters, not weeks. H1 no longer issues SELL signals at all (blocked since 2026-09-03) — it is BUY-only on this timeframe.</li>
</ul> </ul>
<div class="callout"><b>If you trade manually from the Telegram alerts, H1 (or M15) is the honest choice.</b> M5's backtested edge assumes near-instant fills that a human copying signals rarely achieves.</div> <div class="callout"><b>If you trade manually from the Telegram alerts, H1 (or M15) is the honest choice.</b> M5's backtested edge assumes near-instant fills that a human copying signals rarely achieves.</div>
+35 -2
View File
@@ -90,6 +90,23 @@ metric. One clause waived BY OWNER DECISION 2026-07-21: 5yr max DD is
accepted in exchange for +20% total R. M5 keeps the global 0.6 arm 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). (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.
M5 override (validated 2026-07-11, full-history replay 2020-03-17 -> 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- 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 open whipsaw). Hour-9 entries lost -28.9R/143 over 6 years, negative
@@ -210,8 +227,8 @@ bollinger all look mis-weighted in-book and all replay worse when
oppose vote is a load-bearing quality bar, like ROC). oppose vote is a load-bearing quality bar, like ROC).
""" """
from typing import Optional, List from typing import Dict, Optional, List
from config import PlatformConfig, Timeframe from config import PlatformConfig, TechnicalConfig, Timeframe
from app_config import apply_pair_overrides from app_config import apply_pair_overrides
_CONFIRMATIONS = { _CONFIRMATIONS = {
@@ -222,6 +239,19 @@ _CONFIRMATIONS = {
} }
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( def make_config(
primary_tf: Timeframe, primary_tf: Timeframe,
confirmation_tfs: Optional[List[Timeframe]] = None, confirmation_tfs: Optional[List[Timeframe]] = None,
@@ -272,6 +302,9 @@ def make_config(
# Later trailing-stop arm on M15 only — see module docstring. # Later trailing-stop arm on M15 only — see module docstring.
config.signals.trail_arm_r = 0.70 config.signals.trail_arm_r = 0.70
config.signals.trail_dist_r = 0.75 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()
if primary_tf == Timeframe.H1: if primary_tf == Timeframe.H1:
# Block every H1 SELL — the SELL quality gate (6e in # Block every H1 SELL — the SELL quality gate (6e in
# signal_aggregator.py) downgrades any actionable-grade short # signal_aggregator.py) downgrades any actionable-grade short
+60
View File
@@ -2078,6 +2078,35 @@ def _():
inline_cfg.xau_strategy.sell_blocked_hours_utc = [15, 17] inline_cfg.xau_strategy.sell_blocked_hours_utc = [15, 17]
inline_cfg.signals.trail_arm_r = 0.70 inline_cfg.signals.trail_arm_r = 0.70
inline_cfg.signals.trail_dist_r = 0.75 inline_cfg.signals.trail_dist_r = 0.75
# M15 override 5 (2026-09-09): RSI x 0.5 in every regime —
# LITERAL so a silent default-table change fails loudly here.
inline_cfg.technical.regime_weights_override = {
"trending_up": {
"ema_trend": 2.0, "rsi": 0.5, "macd": 2.0,
"bollinger": 0.5, "keltner": 1.5, "roc": 1.5,
"candlestick": 0.8, "divergence": 1.0, "adx": 0.8,
},
"trending_down": {
"ema_trend": 2.0, "rsi": 0.5, "macd": 2.0,
"bollinger": 0.5, "keltner": 1.5, "roc": 1.5,
"candlestick": 0.8, "divergence": 1.0, "adx": 0.8,
},
"trending": {
"ema_trend": 2.0, "rsi": 0.5, "macd": 2.0,
"bollinger": 0.5, "keltner": 1.5, "roc": 1.5,
"candlestick": 0.8, "divergence": 1.0, "adx": 0.8,
},
"ranging": {
"ema_trend": 0.3, "rsi": 0.75, "macd": 1.5,
"bollinger": 1.5, "keltner": 2.0, "roc": 0.5,
"candlestick": 1.0, "divergence": 2.5, "adx": 0.5,
},
"transitioning": {
"ema_trend": 1.5, "rsi": 0.5, "macd": 2.5,
"bollinger": 1.0, "keltner": 2.5, "roc": 2.0,
"candlestick": 1.0, "divergence": 2.0, "adx": 1.0,
},
}
if tf == Timeframe.M5: if tf == Timeframe.M5:
# Documented per-TF overrides (see xauusd.py docstring): # Documented per-TF overrides (see xauusd.py docstring):
# hour 9 UTC blocked, hour 21 UNBLOCKED (2026-07-30 # hour 9 UTC blocked, hour 21 UNBLOCKED (2026-07-30
@@ -11861,6 +11890,37 @@ def _():
"the GPU engine must inherit the live composite, never shadow it" "the GPU engine must inherit the live composite, never shadow it"
@test("replay record follow-up: M15 factory halves RSI in every regime, other TFs carry no override")
def _():
# M15 override 5 (validated 2026-09-09): the M15 factory ships a
# regime_weights_override that is the shipped table with the RSI
# multiplier halved everywhere and every other cell untouched.
import engine as _eng
from pair_signals import PAIR_PRODUCERS
default = _eng.regime_weight_table(TechnicalConfig())
m15 = PAIR_PRODUCERS[XAUUSD](Timeframe.M15, [Timeframe.M5, Timeframe.H1])
override = m15.technical.regime_weights_override
assert set(override) == set(default), \
f"M15 override must cover exactly the shipped regimes: {sorted(override)}"
for regime, weights in default.items():
got = override[regime]
assert set(got) == set(weights), \
f"{regime}: indicator set changed: {sorted(got)}"
for name, mult in weights.items():
want = mult * 0.5 if name == "rsi" else mult
assert got[name] == want, \
f"{regime}.{name}: {got[name]} != {want} (default {mult})"
# The engine really runs with it (regime_weight_table prefers the override).
assert _eng.regime_weight_table(m15.technical) == override
# ...and it is M15-only — same per-TF pattern as every other override.
for tf, ctfs in [(Timeframe.M5, [Timeframe.M15, Timeframe.H1]),
(Timeframe.H1, [Timeframe.M15, Timeframe.H4]),
(Timeframe.H4, [Timeframe.H1, Timeframe.D1])]:
cfg = PAIR_PRODUCERS[XAUUSD](tf, ctfs)
assert cfg.technical.regime_weights_override == {}, \
f"{tf} must carry no regime weight override"
# ══════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════
# SUMMARY # SUMMARY
# ══════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════