backtest/research/cycle_2026-09-10.md: pre-registered rules, HEAD baseline on the 3-year window (three pinned MD5s reproduced), cost ladder and concentration facts, bucket screen (one cycle-2 candidate: M15 grade-A SELLs), C1 H1 divergence->0 REJECTED, C2 M15 RSI-multiplier ladder = stable plateau, seeding-depth measurement and the two shipped parity fixes. CLAUDE.md: live fetch depth, backtester pad, 501 tests, the 2026-09-10 state entry; corrected the docs note (no PDF builds exist) and recorded pyarrow as a dev-venv-only dependency. Claude-Session: https://claude.ai/code/session_01NQDjW8rXYrJR4JDWMWKUij
20 KiB
Improvement cycle 1 — 2026-09-10 (HEAD baseline, robustness challenge, seeding parity)
Purpose
First cycle under the owner's standing brief (evidence-driven incremental improvement of the existing XAU/USD system). No new strategy idea is tested. The cycle (1) re-establishes the baseline at HEAD and proves the harness reproduces the pinned books, (2) challenges the current edge — cost sensitivity, concentration, and the parameter neighbourhood of the M15 RSI × 0.5 change shipped 2026-09-09, (3) reruns the one parked candidate with prior rule-1 evidence (H1 divergence off) through the production vehicle it now has, and (4) measures and fixes a live-vs-backtest indicator-seeding gap (H4 fetches 380 bars, D1 300, while the backtester seeds from its whole cached history). Everything under "Pre-registered" was written before the corresponding run.
Harness
venv/bin/python backtest/gpu_backtest.py --pair XAUUSD --spread 0.65 --workers 12 --primary-tf {5min|15min|1h|4h}, pair-factory config (no --quality/--strategies/ --confirm-tfs). Windows: W3 2023-09-05→2026-09-05 (halves at 2025-03-05; last-12-months
slice from 2025-09-05 reported descriptively), W2 2024-09-05→2026-09-05 (halves at
2025-09-05), OOS 2024-06-01→2025-03-30. Code: git 2ac7301 (branch sql-integration).
Backend: NumPy CPU (CuPy imports but has no CUDA toolkit on this box). Bars: cached
backtest/cache/oanda_XAU_USD_{5min,15min,1h,4h}_20230705_20260905.parquet,
_1d_20221110_20260905, _{5min,15min,1h}_20240705_20260905, _{…}_20240331_20250330.
Outputs: backtest/research/cycle_2026-09-10/ (gitignored). Every candidate is compared
with a CONTROL produced on this harness in this session. --set technical.regime_weights_override=<JSON> replaces the WHOLE table, so each candidate
passes the complete table with one indicator's multipliers changed.
Pre-registered rules (written 2026-09-10 before any run)
Step 0 — reproducibility gate
The HEAD factory run must reproduce the ledger's MD5s: M5 W2
29120a1ee32979431d062f210d41a9d1, M15 W2 d185f099482a07f29502cb5a371a9bbf, M15 OOS
6f8f24b49597ced28f50a19e08c3472f. A mismatch stops the cycle until explained.
Step 1 — diagnostics (descriptive; flags fixed in advance)
- Cost ladder $0.65 / 0.80 / 1.00 / 1.30 on W3 per TF. Red flag for a TF: at $0.80 PF < 1.3 or either W3 half ≤ 0.
- Concentration/stability: share of total R in the top 5% of trades, % of calendar months positive, longest losing streak, max DD and R per calendar year. No action threshold.
- Bucket screen over the recorded W3 trades (direction, quality, regime, strategy,
entry bar-close hour, weekday,
us_holiday,ev_entry_within_30m_hi_usd,ev_hi_usd_during_hold, year/quarter; plus M15/H4 SELL-in-transitioning and holiday trades). A bucket becomes a cycle-2 candidate ONLY if n ≥ 30, R < 0 in each of the three 12-month slices AND in OOS, and a mechanism can be stated. No gate is built this cycle.
C1 — H1 divergence multiplier → 0 in every regime (H1-only)
Vehicle: --set technical.regime_weights_override=<default table with divergence 0.0 in all five regimes>, primary 1h, W3 + OOS, control = the Step-0 HEAD run.
PASS only if ALL: (a) ΔR ≥ +5R on W3; (b) ΔR > 0 in both W3 halves; (c) PF ≥ 1.3 and not
more than 0.05 below control; (d) max DD ≤ control + 2R; (e) trade count within ±25%;
(f) OOS ΔR ≥ 0. Last-12-months slice reported, not a clause. No rescue, no regime-family
fine-tuning after seeing the result. A pass is a shipping PROPOSAL for the owner.
C2 — M15 RSI multiplier neighbourhood (stability of the live change)
Ladder k ∈ {0, 0.25, 0.5 (HEAD), 0.75, 1.0 (pre-2026-09-09), 1.5}, full table with rsi × k, on W2, OOS and W3. Reading, with G(k) = R(k) − R(1.0) on W2: stable plateau if G(0.25) ≥ 0.5·G(0.5) and G(0.75) ≥ 0.5·G(0.5) and the OOS deltas at 0.25 and 0.75 are ≥ 0; spike if either neighbour retains < 50% of G(0.5). Outcome is a report to the owner; no different k is proposed whatever the result.
Step 3 — seeding-depth parity
3a: on the deepest cached H4/D1 frames (H1 as control), sampled bars over the last two
years, compare TechnicalEngine.analyze() on the trailing N bars with the full-history
result; MIN_SEED_BARS = smallest N with zero indicator-signal flips on both H4 and D1.
3b: live per-TF fetch floor raised to that depth (no book changes; the backtester is the
reference). 3c: backtester slow-TF pad raised to the same depth, shipped only if the M5
and M15 W3 books stay byte-identical and the H1/H4 differences are confined to the
window-start seeding zone; the H1/H4 re-pin is the owner's decision.
Results
(filled in as runs complete)
Step 0 — HEAD baseline (W3, $0.65, factory config; recorded with --record)
| TF | trades | total R | WR | PF | max DD | half 1 (→2025-03-05) | half 2 | last 12 mo | SELL bucket | book MD5 |
|---|---|---|---|---|---|---|---|---|---|---|
| M5 | 870 | +153.64 | 53.3% | 1.50 | 10.1 | 136 / +16.73 / PF 1.36 | 734 / +136.91 / 1.52 | 593 / +119.65 / 1.57 | 100 / +8.95 / 1.25 | c2722f9830528658b53854f6bae61b8c |
| M15 | 1273 | +186.61 | 59.4% | 1.39 | 19.3 | 556 / +75.87 / 1.37 | 717 / +110.74 / 1.41 | 474 / +81.04 / 1.47 | 404 / −15.18 / 0.91 | b2cfa9e2e6cd3997f4d5ca9b49cab90e |
| H1 | 253 | +70.02 | 64.4% | 1.98 | 4.4 | 115 / +26.26 / 1.79 | 138 / +43.76 / 2.15 | 89 / +24.62 / 1.96 | 0 (all shorts blocked) | b99111132d9ecb80c2998ed5aee2f3ea |
| H4 | 114 | +21.68 | 57.9% | 1.59 | 5.4 | 52 / +4.91 / 1.26 | 62 / +16.77 / 1.94 | 46 / +12.34 / 1.79 | 20 / −0.97 / 0.89 | 4508e7a3402e008ba1d93805094cd981 |
These equal the numbers CLAUDE.md records for HEAD (M5 870/+153.6/1.50, M15 1273/+186.61/1.39,
H1 253/+70.0/1.98, H4 114/+21.7/1.59). Engine / gate fingerprints from the manifests:
M5 f28b1a8c409d/bc0991492340, M15 536f84340ea0/b2920991cdb8 (new since the RSI
override), H1 6efb6d365f01/0b67d738dc15, H4 ed96f258b10e/0c3b51f063f9.
replay_scorecard.py --check on the four recorded runs: see step0.out.
Per calendar year (R / PF): M5 2023 +2.4/1.47, 2024 +1.0/1.02 (102 trades — a flat
year), 2025 +40.5/1.34, 2026 +109.7/1.77; M15 +13.2/1.40, +52.9/1.36, +77.3/1.43,
+43.2/1.37; H1 +5.5/2.60, +14.7/1.58, +33.7/2.11, +16.1/2.35; H4 +1.2/1.28, +4.6/1.39,
+7.7/1.74, +8.2/1.78.
Step 3a — seeding-depth measurement (seed_depth.out)
Deepest Oanda caches (H4 12,419 bars, D1 2,617, H1 45,914; 2018→2026-08-06), bars sampled
every 12th (H4, 267 bars) / 2nd (D1, 313) / 48th (H1, 247) over the last two years;
TechnicalEngine.analyze() on the trailing N bars vs on the full history; identical
results with raw frames and with frames cleaned by validate_and_clean (the frame-wide
σ of the outlier filter did not add a single flip).
| TF | N = live depth today | bars with ≥1 indicator flip | which | max composite Δ | N = 600 … 5000 |
|---|---|---|---|---|---|
| H4 | 380 | 2 / 267 | ema_trend | 4.66 | 0 flips at every depth |
| D1 | 300 | 1 / 313 | ema_trend | 9.47 | 0 flips at every depth |
| H1 | 1460 (control) | 0 / 247 | — | 0.00 | 0 |
Only the 200-bar EMA trend reading is affected, as the adjust=False seeding predicts;
composite-signal and regime never flipped in the sample. Pre-registered pick = smallest
zero-flip depth = 600; shipped floor = 1000 (seed weight e^-10, a ×40 margin on a
sample of a few hundred bars; costs nothing — one request either way). The 2026-08-04
audit's "~2% of H4 bars" was measured on a different sample; this sample says 0.75%
(H4) and 0.3% (D1) of bars, with composite errors up to 9.5 points on the ones that flip.
Step 1 — concentration, stability, bucket screen (W3 recorded books, diag_w3_head.txt)
| TF | top 5% of trades carry | months positive | longest losing streak |
|---|---|---|---|
| M5 | 94% of total R (44 trades, +144R of +154R) | 62% (23/37) | 8 |
| M15 | 96% (64 trades, +179R of +187R) | 68% (25/37) | 9 |
| H1 | 48% (13 trades) | 71% (25/35) | 4 |
| H4 | 80% (6 trades) | 50% (15/30) | 6 |
The intraday edge is a fat right tail: trailing exits let a few dozen runners pay for everything else (M5 2024 was flat over 102 trades). H1 is the only timeframe whose edge is spread across its book.
Bucket screen (pre-registered criterion: n ≥ 30 and R < 0 in each 12-month slice y1/y2/y3
AND in the OOS slice of the same book). Exactly one bucket qualifies (besides the trivial
exit_reason = SL):
- M15 SELL grade A: 230 trades, −14.4R, PF 0.86, y1 −7.4 (67) / y2 −0.4 (80) /
y3 −6.5 (83) / OOS −6.9 (62). Context: M15 SELL overall 404 / −15.2R / PF 0.91 (SELL
max DD 36R on its own curve); SELL|B 160 / −5.5R (y3 +4.6 — not qualifying);
SELL|A+ 14 / +4.7R; SELL|trending_down 301 / −10.9R (y2 +0.9 — not qualifying).
This is the same bucket the 2026-09-03 campaign tested as
sell_min_quality=["B"](allow only B-grade M15 shorts) and REJECTED: 12mo +0.7R, 5-yr −8.9R with max DD 35.8 → 41.4R, OOS +8.3R — A-grade M15 shorts paid in 2021–2023. Under the owner's ≤3-year window they lose in every slice. Recorded as the ONE cycle-2 candidate from this screen (M15sell_min_qualityexcluding "A" via the factory), to be run against a control with the 3-yr DD clause; the 5-yr verdict is the fact on the other side of the scale. Not built this cycle (rule: no gate from the screen). - Not qualifying / no evidence: holiday-day trades are POSITIVE on M15 (27 / +10.6R) and
small everywhere; entries within 30 min after a high-impact USD release are positive
(M15 114 / +9.3R); M15 SELL-in-transitioning (the gate-6c bucket) 75 / −2.2R with y2
and y3 positive; M15
vol_regime = high291 / +4.1R / PF 1.03 (y3 +12.6); hour 7 UTC −4.0R (OOS +2.4). M5/H1/H4 produced no qualifying bucket.
Step 0 — reproducibility gate: PASS
| book | MD5 at HEAD | ledger MD5 | result |
|---|---|---|---|
| M5 W2 | 29120a1ee32979431d062f210d41a9d1 |
same | equal |
| M15 W2 | d185f099482a07f29502cb5a371a9bbf |
same | equal |
| M15 OOS | 6f8f24b49597ced28f50a19e08c3472f |
same | equal |
| H1 OOS (new control, no prior MD5) | c02eebc9066916d7637bbf5796993f87 |
— | recorded |
replay_scorecard.py --check: all eight invariants hold on every recorded W3 run.
Step 3b — live floor shipped (commit 9942736) and verified on the live feed
BaseDataProvider.MIN_LIVE_BARS = 1000, applied in a new pure helper
_live_outputsize(timeframe, lookback_days); requested depths now M5 5000, M15 5000,
H1 1460 (unchanged), H4 1000 (was 380), D1 1000 (was 300). The backtester never calls
fetch_ohlcv, so no book changes. Tests: new Module 16 depth test, Module 21 cache test
updated from 300 to 1000; suite 500 passed.
Live verification 2026-09-10 (real Oanda fetch, same latest closed bar, engine on the
frame cleaned exactly as live does):
- H4 (bar 2026-09-10 04:00 UTC): 380 / 1000 / 5000 bars → identical readings (composite +15.03, ranging) — no flip on today's bar.
- D1 (bar 2026-09-09): 300 bars → ema_trend SELL, composite −9.53; 1000 and 5000 bars → ema_trend NEUTRAL, composite −8.87. The old depth was mis-reading the daily trend today; the new floor agrees with the converged value.
Step 1 — cost ladder (W3, same trade lists; --spread only re-prices; mechanics check: the H1 $1.00 total predicted from the $0.65 book (−0.35 $/trade ÷ risk) equals the rerun to the cent)
| TF | $0.65 R / PF / DD | $0.80 R / PF / DD (halves) | $1.00 R / PF / DD | $1.30 R / PF / DD | red flag ($0.80: PF < 1.3 or a half ≤ 0)? |
|---|---|---|---|---|---|
| M5 | +153.6 / 1.50 / 10.1 | +145.6 / 1.46 / 11.0 (+14.9 / +130.7) | +134.9 / 1.42 / 12.2 | +118.8 / 1.36 / 14.1 | no |
| M15 | +186.6 / 1.39 / 19.3 | +176.4 / 1.37 / 19.9 (+70.0 / +106.4) | +162.8 / 1.34 / 20.8 | +142.4 / 1.29 / 22.6 | no (PF dips under 1.3 only at $1.30) |
| H1 | +70.0 / 1.98 / 4.4 | +68.9 / 1.96 / 4.5 (+25.6 / +43.2) | +67.3 / 1.93 / 4.5 | +65.0 / 1.89 / 4.7 | no |
| H4 | +21.7 / 1.59 / 5.4 | +21.3 / 1.57 / 5.5 (+4.7 / +16.6) | +20.8 / 1.56 / 5.7 | +20.1 / 1.53 / 6.0 | no |
Doubling the assumed round-trip cost ($0.65 → $1.30) costs M5 23% and M15 24% of total R and H1/H4 about 7%; the H1 book is the least cost-sensitive because its stops are the widest in dollars. The M5 first half is thin at every price (+8.7R at $1.30 over 136 trades), which is the 2024 flat year again, not a cost effect.
C1 — H1 divergence multiplier → 0 in every regime: FAIL (a, b, c) — closed
| book | trades | total R | ΔR | half 1 | half 2 | PF | max DD | last 12 mo | OOS |
|---|---|---|---|---|---|---|---|---|---|
| H1 control (HEAD, W3) | 253 | +70.02 | +26.26 | +43.76 | 1.98 | 4.4 | +24.62 (89) | 78 / +16.68 / PF 1.74 | |
| H1 divergence 0 (W3) | 280 | +71.01 | +0.99 | +28.70 (+2.4) | +42.30 (−1.5) | 1.89 | 4.8 | +23.09 (102) | 88 / +21.91 / PF 1.92 (+5.2) |
Clauses: (a) ΔR +1.0 < +5 FAIL; (b) second half negative FAIL; (c) PF 1.98 → 1.89, drop
0.09 > 0.05 FAIL; (d) DD +0.4 pass; (e) trades +10.7% pass; (f) OOS +5.2 pass. Calendar
years: 2024 +14.7 → +19.1, 2025 +33.7 → +34.1, 2026 +16.1 → +12.4 — the gain sits in
2024, the most recent year gives it back. The OOS candidate book (88 / +21.91 / PF 1.92)
is exactly the 2026-09-03 campaign's b12_h1_nosell_nodiv_oos under
disabled_indicators — so a 0.0 regime multiplier is mechanically the same as disabling
the indicator for the composite (engine skips only base weight 0, but an effective weight
of 0 adds nothing to score, weight sum or confidence); the only residual difference is
display (reasons text still lists the vote). The parked candidate's earlier evidence
(5-yr +9.6R over no-shorts) was carried by 2021–2023; on the owner's 3-year window it is
noise with a worse profit factor. Not shipped; do not re-propose without new years.
C2 — M15 RSI multiplier neighbourhood: STABLE PLATEAU (the 2026-09-09 ship stands)
Full regime table with the RSI multiplier × k (k = 1.0 is the pre-2026-09-09 table,
k = 0.5 is HEAD; every other point is a --set run on the same harness).
| k | W2 trades / R / PF / DD (halves) | G(k) = R − R(1.0) on W2 | OOS trades / R / PF / DD | ΔOOS | W3 trades / R / PF / DD (halves) |
|---|---|---|---|---|---|
| 0 (RSI off) | 1091 / +145.46 / 1.35 / 18.5 (+74.1 / +71.3) | +27.8 | 418 / +51.89 / 1.34 / 19.5 | +11.3 | 1490 / +188.14 / 1.33 / 22.7 (+69.3 / +118.8) |
| 0.25 | 1008 / +139.93 / 1.37 / 20.1 (+66.1 / +73.9) | +22.3 (78% of G(0.5)) | 390 / +59.55 / 1.44 / 17.3 | +19.0 | 1374 / +187.58 / 1.36 / 21.4 (+77.7 / +109.9) |
| 0.5 (HEAD) | 931 / +146.14 / 1.43 / 16.1 (+66.1 / +80.0) | +28.5 | 355 / +60.67 / 1.49 / 17.1 | +20.1 | 1273 / +186.61 / 1.39 / 19.3 (+75.9 / +110.7) |
| 0.75 | 854 / +137.83 / 1.45 / 14.9 (+47.6 / +90.3) | +20.2 (71%) | 326 / +55.56 / 1.49 / 15.6 | +15.0 | 1176 / +178.81 / 1.42 / 17.2 (+66.9 / +111.9) |
| 1.0 (old) | 793 / +117.62 / 1.41 / 14.8 (+38.2 / +79.4) | 0 | 300 / +40.59 / 1.38 / 15.5 | 0 | 1092 / +151.90 / 1.38 / 15.5 (+55.7 / +96.2) |
| 1.5 | 676 / +110.17 / 1.46 / 14.2 (+39.4 / +70.7) | −7.5 | 266 / +37.27 / 1.39 / 18.8 | −3.3 | 949 / +136.20 / 1.39 / 18.8 (+42.6 / +93.6) |
Reading (rule fixed in advance): G(0.25) = 78% and G(0.75) = 71% of G(0.5), both ≥ 50%; OOS deltas +19.0 and +15.0, both ≥ 0 → stable plateau, not a spike. The response is monotone in k on every window (R falls as RSI's weight rises; trade count falls ~90 per 0.25 step), PF peaks in the 0.5–0.75 band, and drawdown grows as k → 0 (RSI off has the same R as HEAD with PF 1.35 and DD 22.7 on W3). The shipped 0.5 sits inside a broad region, on its PF/DD-favourable side. Under this rule no other k is proposed.
Step 3c — backtester history pad targets the same 1000-bar seed (_history_pad_days)
Change: the per-TF warm-up pad now targets BaseDataProvider.MIN_LIVE_BARS (1000 bars,
converted to calendar days with 7/5) instead of 210 bars, so a window's first bars are
seeded like the rest of the window and like live. Intraday pads are unchanged (62 days
already hold ≥ 1488 bars), so M5/M15/H1 keep their cache windows; H4 now pads 234 days
(was 62 → 372 bars) and D1 1400 days (was 299 → ~210 bars for a 200-bar EMA). New caches
oanda_XAU_USD_4h_20230114_20260905.parquet (5,826 bars) and
_1d_20191105_20260905.parquet (2,124 bars); the first H4 chunk (2023-01-14, a Saturday)
is legitimately empty. The raw-path fetch for trade resolution now reuses the primary
timeframe's own padded start (one cache file per TF, as before). Test: Module 21
"history pad seeds slow TFs like live"; suite 501 passed.
W3 books before → after, trade-by-trade (bookdiff.py with the recorded runs):
| TF | trades / R before | trades / R after | trades added / removed / R changed | what actually differs |
|---|---|---|---|---|
| H1 | 253 / +70.02 | 253 / +70.02 | 0 / 0 / 0 | ONE confirmation reading: 2023-09-20 18:00 (15 days into the window), H4 ema_trend STRONG_BUY → BUY (value 1.0 → 0.5, confidence 90 → 65); that trade's blended score moves 0.21; nothing else in 5,761 recorded indicator rows |
| H4 | 114 / +21.68 | 114 / +21.68 | 0 / 0 / 0 | no signal flips; 20 D1 rows (ADX/RSI/MACD Wilder recursions) differ at the 1e-6…1e-9 level, all Oct–Dec 2023; exported floats differ by ≤ 1.7e-10 (score ≤ 1.4e-9, R ≤ 1e-15) |
Both differences sit in the window-start seeding zone, exactly where the deeper seed was
meant to act, and neither moves a trade. The canonical H1 book therefore needs NO re-pin
(trade content identical); only the CSV bytes differ (H1 3b45d12bade823178b267c13bc698458
vs b99111132d9ecb80c2998ed5aee2f3ea, H4 e75adae13c839b15736f69c0114e6aa5 vs
4508e7a3402e008ba1d93805094cd981), which the parity recipe must state.
M5 and M15 on the patched harness: byte-identical to Step 0 (M5
c2722f9830528658b53854f6bae61b8c, M15 b2cfa9e2e6cd3997f4d5ca9b49cab90e) — the pad
change did not leak into the intraday books. Shipped (commit below); no re-pin needed.
Verdicts
| item | decision | one-line reason |
|---|---|---|
| Step 0 reproducibility | PASS | three ledger MD5s reproduced at HEAD; scorecard invariants hold |
| Cost ladder | no red flag | every TF keeps PF ≥ 1.3 with positive halves at $0.80; edge survives $1.00 on all four |
| Concentration | fact recorded | M5/M15: top 5% of trades carry 94–96% of R; H1 48% |
| Bucket screen | one cycle-2 candidate | M15 grade-A SELLs (230 / −14.4R, negative in y1, y2, y3 and OOS); 5-yr history says the opposite — run it against a control with the DD clause before believing it |
| C1 H1 divergence → 0 | REJECT | ΔR +1.0 (< +5), second half −1.5R, PF 1.98 → 1.89; OOS-only gain |
| C2 M15 RSI ladder | stable plateau | neighbours keep 78% / 71% of the shipped gain, OOS positive at both; ship stands |
| 3a/3b live seeding floor | ACCEPT, shipped (9942736) |
measured flips at 380/300 bars, none at ≥ 600; live D1 flipped on 2026-09-10 at the old depth; books untouched |
| 3c backtester pad | ACCEPT, shipped | no trade moved in any book; M5/M15 byte-identical; H1/H4 differ only in window-start numerics |
What NOT to re-propose (added to the ledger's closed list)
- H1 divergence weight 0 (any vehicle) without new years of data.
- M15 RSI multiplier other than 0.5 on the strength of a single-point rerun; the ladder is on record.
- Cost-driven gates: the books are not cost-fragile at $0.80–$1.00.
Shipped
9942736providers:MIN_LIVE_BARS = 1000live floor +_live_outputsize(Module 16 test; Module 21 expectation 300 → 1000). Suite 500.- backtester:
_history_pad_days(62 / 234 / 1400 days for intraday / H4 / D1), raw-path fetch reuses the primary's start (Module 21 test). Suite 501. Live H1/H4 scans and the books now share the same seeding depth; pinned H1/H4 CSVs are trade-identical but no longer byte-reproducible (float noise in exported indicator columns — same status the M5/M15 pins have had since 2026-09-07). - No signal-core change, no docs change (nothing user-visible moved), both news gating flags untouched.