backtest: full-GPU research twin (cudaBasedBacktesting.py + cuda_signal_kernels.py); books byte-identical to gpu_backtest.py (2026-09-18)

Owner decisions: research only (live scans stay on the CPU code), gpu_backtest.py
stays the base reference, every research flag supported, the one exemption from
instructions item 6 recorded in backtest/instructions_for_backtest.txt.

- backtest/cuda_signal_kernels.py: the CUDA decision stage. K1 = engine.py + the
  GPUTechnicalEngine overrides (one thread per bar), K2 = analyze_from_dataframes
  end to end incl. gates 6b-6i, the four setup generators and the regime filter
  (one thread per primary bar), K3 = position_sizing.build_features, K4 cooldown,
  K5 prefix median, K6 recorder bar rows. Config packer with generated #defines;
  leaf module; --fmad=false; Python evaluation order kept (ternary min/max,
  pairwise sums, Neumaier sum(), exact round(x, 2)).
- backtest/cudaBasedBacktesting.py: same CLI, cache files and writers as the
  reference; the host turns kernel codes into the exact strings; no CPU path.
- test_all.py Module 37 (16 tests): every stage pinned against the live code;
  suite 552 / 552 on the GPU, without CUDA and with FB2_BACKEND=cpu.
- Proof: five reference MD5s, four 6-month acceptance books (M5/M15, $0.65/$0.30),
  M15 OOS --export-features and --record --record-bars tables, ten research-flag
  variants, and the owner's five-year M5 and M15 books, all byte-identical to
  gpu_backtest.py. Wall time 0.8-6.6 s per book against 13-353 s.
- CLAUDE.md sections 1, 3, 6, 7, 9, 10, 11 and the instructions addendum: any
  signal change is now a parity event for the twin.
This commit is contained in:
Harwinder Sekhon
2026-09-18 16:11:47 -04:00
parent 02589cbb88
commit 32ad73c59a
8 changed files with 5602 additions and 6 deletions
+43 -6
View File
@@ -23,7 +23,7 @@ cp config.json.example config.json # then fill i
venv/bin/pip install cupy-cuda12x "nvidia-cuda-runtime-cu12==12.9.*" # optional, dev box: backtester GPU backend venv/bin/pip install cupy-cuda12x "nvidia-cuda-runtime-cu12==12.9.*" # optional, dev box: backtester GPU backend
# (CuPy from pip needs that header wheel to compile kernels) # (CuPy from pip needs that header wheel to compile kernels)
venv/bin/python test_all.py # QA suite, no network; prints its own count venv/bin/python test_all.py # QA suite, no network; prints its own count
# (536 tests / 35 modules as of 2026-09-18) # (552 tests / 36 modules as of 2026-09-18)
venv/bin/python main.py --pair XAUUSD --timeframe H1 --detail # one signal report in the terminal venv/bin/python main.py --pair XAUUSD --timeframe H1 --detail # one signal report in the terminal
# --json full dump, --panel adds the advisory LLM panel, # --json full dump, --panel adds the advisory LLM panel,
# --timeframe M5 | M15 | H1 | H4 # --timeframe M5 | M15 | H1 | H4
@@ -35,6 +35,9 @@ venv/bin/python dashboard_publisher.py --once # one scan +
venv/bin/python backtest/gpu_backtest.py --pair XAUUSD --days 365 venv/bin/python backtest/gpu_backtest.py --pair XAUUSD --days 365
venv/bin/python backtest/gpu_backtest.py --pair XAUUSD --start 2024-09-05 --end 2026-09-05 \ venv/bin/python backtest/gpu_backtest.py --pair XAUUSD --start 2024-09-05 --end 2026-09-05 \
--primary-tf 15min --spread 0.65 --workers 12 --export-csv book.csv --primary-tf 15min --spread 0.65 --workers 12 --export-csv book.csv
venv/bin/python backtest/cudaBasedBacktesting.py --pair XAUUSD --start 2024-09-05 --end 2026-09-05 \
--primary-tf 15min --spread 0.65 --export-csv book.csv # research twin: same flags, every stage on
# the GPU, seconds per book, same MD5 (section 9)
``` ```
Backtest flags worth knowing: `--spread` (round-trip cost in dollars; the default is 0.30, the Backtest flags worth knowing: `--spread` (round-trip cost in dollars; the default is 0.30, the
@@ -273,6 +276,8 @@ Arrows show who imports whom (generated from the code on 2026-09-15).
|---|---| |---|---|
| `backtest/gpu_backtest.py` | The backtester. The indicator precompute over the full history and the resolution of every trade run on the GPU when CUDA works (`--backend`, `Backend:` log line names the device or the fallback reason); the per-bar replay through `analyze_from_dataframes` is the live Python signal code in forked CPU workers by design and is 80–95% of wall time | | `backtest/gpu_backtest.py` | The backtester. The indicator precompute over the full history and the resolution of every trade run on the GPU when CUDA works (`--backend`, `Backend:` log line names the device or the fallback reason); the per-bar replay through `analyze_from_dataframes` is the live Python signal code in forked CPU workers by design and is 80–95% of wall time |
| `backtest/gpu_kernels.py` | CuPy backend state (lazy probe, never silent, sets `CUDA_PATH` to the pip header wheel) and the three CUDA kernels: EWM batch, pandas rolling mean/var replica, trade resolver. Each is a bit-for-bit twin of the NumPy/pandas code kept in `gpu_backtest.py` (`--fmad=false`, one thread per series or trade); the twins are pinned by Module 36 and by the reference MD5s | | `backtest/gpu_kernels.py` | CuPy backend state (lazy probe, never silent, sets `CUDA_PATH` to the pip header wheel) and the three CUDA kernels: EWM batch, pandas rolling mean/var replica, trade resolver. Each is a bit-for-bit twin of the NumPy/pandas code kept in `gpu_backtest.py` (`--fmad=false`, one thread per series or trade); the twins are pinned by Module 36 and by the reference MD5s |
| `backtest/cudaBasedBacktesting.py` | Research-only full-GPU twin of the backtester (2026-09-18): same CLI, same cache files, same 63-column book byte for byte, but the per-bar decision runs in CUDA kernels instead of the live Python code, so a book takes seconds (M5 two-year 2.4 s against 115 s). Reuses the loader, the precompute, the resolver, the metrics and the writers from `gpu_backtest.py`; the host only turns the kernels' numeric codes into the exact strings. No CPU path: `--backend cpu` is refused. Its numbers count as evidence only while the MD5 parity with `gpu_backtest.py` holds (section 7) |
| `backtest/cuda_signal_kernels.py` | The CUDA decision stage: K1 (`engine.py` plus the `GPUTechnicalEngine` overrides, one thread per bar), K2 (`analyze_from_dataframes` end to end with the gates, the setup generators, XAU_PB, XAU_CP, the regime filter and the replay's per-bar rules, one thread per primary bar), K3 (`position_sizing.build_features`), K4 (cooldown), K5 (prefix median), K6 (recorder bar rows); the config packer and the name lists the `#define`s are generated from. A leaf module that never imports the live code; pinned by Module 37 |
| `backtest/replay_record.py` | Opt-in `--record` recorder: why every accepted trade was taken, plus calendar events. Book-neutral by contract | | `backtest/replay_record.py` | Opt-in `--record` recorder: why every accepted trade was taken, plus calendar events. Book-neutral by contract |
| `backtest/replay_scorecard.py` | Read-only reader of recorded runs (`DIR --check` verifies its invariants) | | `backtest/replay_scorecard.py` | Read-only reader of recorded runs (`DIR --check` verifies its invariants) |
| `scripts/ff_calendar_scraper.py` | Builds `backtest/cache/calendar.parquet` (Forex Factory events) for the recorder | | `scripts/ff_calendar_scraper.py` | Builds `backtest/cache/calendar.parquet` (Forex Factory events) for the recorder |
@@ -287,7 +292,7 @@ Arrows show who imports whom (generated from the code on 2026-09-15).
| `app_config.py` | Loads `config.json` once per process; env var → config.json → refuse; validates keys | | `app_config.py` | Loads `config.json` once per process; env var → config.json → refuse; validates keys |
| `config.json.example` | The schema of `config.json` (which is gitignored) | | `config.json.example` | The schema of `config.json` (which is gitignored) |
| `deploy/README-dashboard.md`, `deploy/gold-dashboard.service` | Live-server checklist and systemd unit for the dashboard daemon | | `deploy/README-dashboard.md`, `deploy/gold-dashboard.service` | Live-server checklist and systemd unit for the dashboard daemon |
| `test_all.py` | The QA suite (35 modules; Module 36 is the GPU backend parity module) | | `test_all.py` | The QA suite (36 modules; Module 36 is the GPU backend parity module, Module 37 the CUDA decision-stage parity module) |
--- ---
@@ -365,9 +370,12 @@ day, 30 kept), `logs/dashboard.log`. Read the databases with `mode=ro`, never `i
## 6. Testing ## 6. Testing
- `venv/bin/python test_all.py` runs everything with mocked network. Expect - `venv/bin/python test_all.py` runs everything with mocked network. Expect
`RESULTS: 536 passed, 0 failed, 536 total` (2026-09-18). On a box with a visible CUDA device `RESULTS: 552 passed, 0 failed, 552 total` (2026-09-18). On a box with a visible CUDA device
Module 36 fails if the GPU backend falls back (the reason is in the message); with Module 36 fails if the GPU backend falls back (the reason is in the message); with
`FB2_BACKEND=cpu` or no CUDA the GPU-path tests print a notice and the NumPy twins are tested. The suite's hermetic app config points `FB2_BACKEND=cpu` or no CUDA the GPU-path tests print a notice and the NumPy twins are tested.
Module 37 (the CUDA decision stage) adds about 85 s on the GPU because its references replay
two synthetic fixtures through the live signal code bar by bar; without CUDA each of its device
tests prints a notice and only the arithmetic rule, packing, flag and source-guard tests run. The suite's hermetic app config points
both database writers at a suite-private temp dir; before 2026-09-17 the RSS fixture tests wrote both database writers at a suite-private temp dir; before 2026-09-17 the RSS fixture tests wrote
fixture headlines into the real `logs/newsdata.db`, and a Module 32 test now pins the isolation. One Twelve Data interval-map test fixture headlines into the real `logs/newsdata.db`, and a Module 32 test now pins the isolation. One Twelve Data interval-map test
reaches the network and can time out on a slow connection; a rerun clears it. reaches the network and can time out on a slow connection; a rerun clears it.
@@ -416,7 +424,13 @@ Process rules that go with them: every backtest report follows
`backtest/instructions_for_backtest.txt`; every candidate is compared with a control run on the `backtest/instructions_for_backtest.txt`; every candidate is compared with a control run on the
same code and cache, never with an old book directly; the `Backend:` line (since 2026-09-18 it same code and cache, never with an old book directly; the `Backend:` line (since 2026-09-18 it
names the CUDA device or the fallback reason) and the "GPU cache fallbacks: 0" line are part of names the CUDA device or the fallback reason) and the "GPU cache fallbacks: 0" line are part of
the evidence; keep `docs/` in sync when behaviour changes. the evidence; keep `docs/` in sync when behaviour changes. Any change to `engine.py`,
`signal_aggregator.py`, `chart_patterns.py`, `position_sizing.py`, the recorder's row layout or
the `GPUTechnicalEngine` overrides is also a **parity event for the CUDA research twin**
(`backtest/cudaBasedBacktesting.py`): mirror it in `backtest/cuda_signal_kernels.py` in the same
commit, keep Module 37 green, and regenerate at least the M15 OOS book with both scripts and show
equal MD5s before a number from the twin is used as evidence. Until then its books are not
evidence.
--- ---
@@ -480,12 +494,20 @@ H1 12-month (2025-08-04 → 2026-08-04) 91 / +22.2R / PF 1.79. All five MD5s abo
reproduced on 2026-09-15 after the cache prune and on 2026-09-18 on both backends reproduced on 2026-09-15 after the cache prune and on 2026-09-18 on both backends
(`backtest/research/gpu_backend_2026-09-18.md`). (`backtest/research/gpu_backend_2026-09-18.md`).
The CUDA research twin (`backtest/cudaBasedBacktesting.py`, section 3) reproduced all five MD5s
above on 2026-09-18 in 0.8–2.4 s per book, plus the four 6-month acceptance books (2026-03-18 →
2026-09-18, M5 and M15 at $0.65 and $0.30: `86111c36…`, `e70d0252…`, `b8c98083…`, `e66b5fbd…`,
each equal to the reference script's), the M15 OOS `--export-features` CSV (`6239d197…`) and
`--record --record-bars` tables, and ten research-flag variants
(`backtest/research/cuda_backtester_2026-09-18.md`).
Kept cache windows (`backtest/cache/oanda_XAU_USD_<tf>_<paddedStart>_<end>.parquet`; the padded Kept cache windows (`backtest/cache/oanda_XAU_USD_<tf>_<paddedStart>_<end>.parquet`; the padded
start is the window start minus 62 days for M5/M15/H1, 234 for H4 and 1,400 for D1, so the D1 start is the window start minus 62 days for M5/M15/H1, 234 for H4 and 1,400 for D1, so the D1
and H4 files of one run have different start dates than its M5 file): 3-year `20230705_20260905`, and H4 files of one run have different start dates than its M5 file): 3-year `20230705_20260905`,
2-year `20240705_20260905`, OOS `20240331_20250330`, M5 12-month `20250613_20260814`, M15/H1 2-year `20240705_20260905`, OOS `20240331_20250330`, M5 12-month `20250613_20260814`, M15/H1
12-month `20250603_20260804`, 5-year `20210531_20260804`, 7.6-year `20181031_20260807`, the 12-month `20250603_20260804`, 5-year `20210531_20260804`, 7.6-year `20181031_20260807`, the
2026-08-14 parity window `20250814_20260814`, their D1/H4 pads, and `calendar.parquet`. 2026-08-14 parity window `20250814_20260814`, the 6-month CUDA acceptance window
`20260115_20260918` (M5/M15/H1), their D1/H4 pads, and `calendar.parquet`.
The research recorder (`--record DIR [--record-bars]`) writes Parquet files describing every The research recorder (`--record DIR [--record-bars]`) writes Parquet files describing every
accepted trade (indicator votes on all three timeframes, penalties, setup, outcome, calendar accepted trade (indicator votes on all three timeframes, penalties, setup, outcome, calendar
@@ -541,6 +563,20 @@ MD5s. Scorecards from it are in-book statistics, never decisions.
- The replay workers are forked after the CUDA context exists and never touch the device (every - The replay workers are forked after the CUDA context exists and never touch the device (every
cache array is host NumPy, asserted before the fork). Python 3.12 may warn about forking a cache array is host NumPy, asserted before the fork). Python 3.12 may warn about forking a
multi-threaded process under `-X dev`; that is expected. multi-threaded process under `-X dev`; that is expected.
- `backtest/cudaBasedBacktesting.py` is a CUDA re-implementation of the decision stage, exempt
from "replays go through the real signal code" only while its book MD5 equals
`gpu_backtest.py`'s. A signal change that is not mirrored in `cuda_signal_kernels.py` makes the
twin drift silently: no test compares it with the live code except Module 37 and the paired
MD5 runs. It has no CPU fallback, refuses `--backend cpu`, and every description, warning and
reason string is produced on the host from the same f-string templates as the live code; the
kernels emit codes and numbers only.
- The kernels keep Python's evaluation order on purpose: `min`/`max` as argument-order ternaries
(never `fmin`/`fmax`), numpy's pairwise summation for `mean`/`nanmean`, pandas' online rolling
and EWM recurrences, Python 3.12's Neumaier-compensated `sum()` for the multi-timeframe mean,
`round(x, 2)` through an error-free product, `atr_percentile = count / 50 * 100` (not
`count * 2`: 15/50·100 = 30.000000000000004 flips "low" to "normal"). A cleaner formulation
flips a threshold somewhere in 500k bars. The as-of alignment compares int64 nanoseconds; a
mixed-unit index once returned slice length 0 for every confirmation frame.
- The dashboard's due-timeframe check uses the raw wake time; flooring `wake − buffer` skipped - The dashboard's due-timeframe check uses the raw wake time; flooring `wake − buffer` skipped
every H1 and H4 scan once (2026-08-02). every H1 and H4 scan once (2026-08-02).
- The agent panel's price context reads the frames `analyze_pair` stashes on `report._frames`. - The agent panel's price context reads the frames `analyze_pair` stashes on `report._frames`.
@@ -561,6 +597,7 @@ One line per validated change or verdict, newest first. Full evidence in the nam
| Date | Decision | Where the evidence is | | Date | Decision | Where the evidence is |
|---|---|---| |---|---|---|
| 2026-09-18 | Full-GPU research twin SHIPPED: `backtest/cudaBasedBacktesting.py` + `backtest/cuda_signal_kernels.py` (owner decisions: research only, `gpu_backtest.py` stays the base reference, every research flag supported, the one exemption from instructions item 6 recorded there). Proof: the five reference MD5s, four 6-month acceptance books (2026-03-18 → 2026-09-18, M5 and M15, $0.65 and $0.30), the M15 OOS `--export-features` CSV and `--record --record-bars` tables, and ten research-flag variants, all byte-identical to the reference script; Module 37 (16 tests) pins every stage against the live code. Books take 0.8–2.4 s instead of 13–115 s. Rule added: any signal change is a parity event for the twin | `backtest/research/cuda_backtester_2026-09-18.md` |
| 2026-09-18 | GPU backend made real: CuPy had never compiled a kernel on the dev box (`CUDA_PATH` missing for the pip layout), so every book to date was NumPy. SHIPPED: `backtest/gpu_kernels.py` (lazy, loud backend probe; `nvidia-cuda-runtime-cu12` header wheel; CUDA kernels for the EWM batch, the pandas rolling mean/var replica and the trade resolver, each a bit-for-bit twin of the CPU code), `--backend auto/gpu/cpu` + `FB2_BACKEND`, Module 36. All five reference MD5s reproduced on the GPU and with `--backend cpu`; precompute GPU == CPU on all 42 cached windows (2.34 M bars). Wall time roughly unchanged: the replay is CPU by design | `backtest/research/gpu_backend_2026-09-18.md` | | 2026-09-18 | GPU backend made real: CuPy had never compiled a kernel on the dev box (`CUDA_PATH` missing for the pip layout), so every book to date was NumPy. SHIPPED: `backtest/gpu_kernels.py` (lazy, loud backend probe; `nvidia-cuda-runtime-cu12` header wheel; CUDA kernels for the EWM batch, the pandas rolling mean/var replica and the trade resolver, each a bit-for-bit twin of the CPU code), `--backend auto/gpu/cpu` + `FB2_BACKEND`, Module 36. All five reference MD5s reproduced on the GPU and with `--backend cpu`; precompute GPU == CPU on all 42 cached windows (2.34 M bars). Wall time roughly unchanged: the replay is CPU by design | `backtest/research/gpu_backend_2026-09-18.md` |
| 2026-09-17 | Local news scorer evaluated (the owner's scikit TF-IDF model in `~/Development/ML-NEWS`, trained on 606 DeepSeek gold labels): golden set 39/78 vs DeepSeek 78/78 vs keywords 24/78; out-of-sample agreement with DeepSeek 64% (DeepSeek agrees with itself 90%); cleaning / negation marking / label weighting move nothing; backfilling the 961 missing gold labels lifts macro-headline agreement 38% → 52% but only reshuffles golden-set errors. Verdict: a fallback, not a replacement. SHIPPED to grow a complete corpus: `xau_score` archived for every headline, no-currency headlines scored and archived (never shown), every new headline sent in its cycle (25 per call). Dashboard process made database-read-only (`set_db_writes_for_process`); the bot is the single writer. Books unaffected (news never enters signals) | `~/Development/ML-NEWS/eval/RESULTS_2026-09-17.md` (evaluation scripts live outside the repo by owner instruction) | | 2026-09-17 | Local news scorer evaluated (the owner's scikit TF-IDF model in `~/Development/ML-NEWS`, trained on 606 DeepSeek gold labels): golden set 39/78 vs DeepSeek 78/78 vs keywords 24/78; out-of-sample agreement with DeepSeek 64% (DeepSeek agrees with itself 90%); cleaning / negation marking / label weighting move nothing; backfilling the 961 missing gold labels lifts macro-headline agreement 38% → 52% but only reshuffles golden-set errors. Verdict: a fallback, not a replacement. SHIPPED to grow a complete corpus: `xau_score` archived for every headline, no-currency headlines scored and archived (never shown), every new headline sent in its cycle (25 per call). Dashboard process made database-read-only (`set_db_writes_for_process`); the bot is the single writer. Books unaffected (news never enters signals) | `~/Development/ML-NEWS/eval/RESULTS_2026-09-17.md` (evaluation scripts live outside the repo by owner instruction) |
| 2026-09-17 | Live review of two weeks of `signals.db`: live == backtest bar for bar (26/29 trades matched, R within 0.14); the raw advisory −7.15R is cluster re-entries the validated books never take (cooldown view −1.6R, backtester +1.1R on the window, an ordinary fortnight); composite score inversely related to the 24 h move on all timeframes (two-week bear reading, test in-book with `--record-bars`); big-candle gate re-tested and REJECTED again (removes +32R / +60R of net-positive trades). Agent panel v2 SHIPPED: probabilities anchored to book base rates, code-computed decision context, no news doctrine, mean aggregator (`verdict_source: "mean"`), prompt version logged, offline replay harness; owner decision the same day: only the macro persona runs live (`personas: ["macro"]`, 1 call per panel; technical / risk / contrarian carried no information in either evaluation, kept for research); backtester `strategy` tag fixed (label-only, books re-pinned). Delivery findings: dashboard daemon on stale code since Sep 10, bot outage Sep 9 | `backtest/research/signal_log_review_2026-09-17.md`, `backtest/research/panel_v2_2026-09-17.md` | | 2026-09-17 | Live review of two weeks of `signals.db`: live == backtest bar for bar (26/29 trades matched, R within 0.14); the raw advisory −7.15R is cluster re-entries the validated books never take (cooldown view −1.6R, backtester +1.1R on the window, an ordinary fortnight); composite score inversely related to the 24 h move on all timeframes (two-week bear reading, test in-book with `--record-bars`); big-candle gate re-tested and REJECTED again (removes +32R / +60R of net-positive trades). Agent panel v2 SHIPPED: probabilities anchored to book base rates, code-computed decision context, no news doctrine, mean aggregator (`verdict_source: "mean"`), prompt version logged, offline replay harness; owner decision the same day: only the macro persona runs live (`personas: ["macro"]`, 1 call per panel; technical / risk / contrarian carried no information in either evaluation, kept for research); backtester `strategy` tag fixed (label-only, books re-pinned). Delivery findings: dashboard daemon on stale code since Sep 10, bot outage Sep 9 | `backtest/research/signal_log_review_2026-09-17.md`, `backtest/research/panel_v2_2026-09-17.md` |
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+19
View File
@@ -46,3 +46,22 @@ re-implementation.
- Any change to a kernel or to its CPU twin is a parity event: re-run Module 36, regenerate the - Any change to a kernel or to its CPU twin is a parity event: re-run Module 36, regenerate the
five reference books on BOTH backends and show the ten MD5s unchanged. Never replace a kernel five reference books on BOTH backends and show the ten MD5s unchanged. Never replace a kernel
with a differently ordered algorithm (parallel scan, two-pass variance) even if it is faster. with a differently ordered algorithm (parallel scan, two-pass variance) even if it is faster.
--- 2026-09-18: CUDA research twin (backtest/cudaBasedBacktesting.py) ---
- Exemption from "replays go through the real signal code" above, granted by the owner for this
ONE research script: its per-bar decision is a CUDA re-implementation of engine.py,
signal_aggregator.py and chart_patterns.py (backtest/cuda_signal_kernels.py). gpu_backtest.py
stays the base reference and live scans never use the twin.
- Its numbers count as evidence only while parity holds. After ANY change to engine.py,
signal_aggregator.py, chart_patterns.py, position_sizing.py, the recorder's row layout or the
GPUTechnicalEngine overrides: mirror the change in cuda_signal_kernels.py in the same commit,
keep test_all.py Module 37 green, and regenerate at least the M15 OOS book (2024-06-01 ->
2025-03-30, $0.65) with BOTH scripts and show equal MD5s before quoting a CUDA-script result.
- A report built from the twin quotes both scripts' "Backend:" lines (the twin's necessarily
starts with "CuPy GPU (") and the paired MD5 of at least one book from the reference script on
the same code and cache. Whole-window and half-window figures, costs and breakdowns are then
read from the twin's book exactly as from the reference's; the CSVs are identical.
- The twin has no CPU path (--backend cpu / FB2_BACKEND=cpu are refused), --workers is ignored,
and it refuses configurations beyond its compile-time limits (more than 4 confirmation
timeframes, divergence lookback >= 64, swing lookback >= 256, cp_lookback >= 128) with a
message that points back to gpu_backtest.py.
@@ -0,0 +1,215 @@
# Full-GPU research twin of the backtester — 2026-09-18
Behaviour-neutral by construction and by proof. `backtest/cudaBasedBacktesting.py` runs every stage
of a backtest on the GPU, including the per-bar decision that `gpu_backtest.py` still runs through
the live Python signal code. Nothing in the signal logic, the weights, the gates or the exit rules
moved; the proof is that both scripts export byte-identical books (same CSV MD5) on every window
and every research flag tried, listed in section 3.
## 1. Owner decisions
- Research and backtesting only. Live scans stay on the CPU signal code; the Telegram bot, the CLI
and the dashboard never import the new files (Module 37 sweeps the live roots).
- `gpu_backtest.py` stays in the tree as the **base reference**. The twin must reproduce its
exported book byte for byte for the same arguments; the MD5 of the CSV is the acceptance gate.
- `--record`, `--record-bars`, `--export-features` and `--fixed-rr-tp` all work on the GPU path.
- Acceptance: 6-month M5 and M15 books, window 2026-03-18 → 2026-09-18, both scripts, identical
CSV MD5 at $0.65 (headline) and $0.30 (comparability). That window was not cached; the first
control run fetched M5/M15/H1 from Oanda (padded 62 days, `oanda_XAU_USD_<tf>_20260115_20260918`)
and both scripts read the same files afterwards.
- `backtest/instructions_for_backtest.txt` item 6 ("replays go through the real signal code, never
a re-implementation") is waived for this ONE research script; the dated addendum in that file
records the exemption and the new obligation: any signal change is a parity event, re-prove the
MD5 with both scripts before a number from the twin counts as evidence.
## 2. What shipped
| File | Role |
|---|---|
| `backtest/cudaBasedBacktesting.py` (1,028 lines) | Same CLI as the reference (it reuses `_parse_args`), same cache files, same report and CSV writers. Reuses by import everything that is not the replay: the loader and its cleaning, `precompute_indicators` (GPU), the trade resolver (GPU), `_reanchor_entries_to_raw`, `_apply_fixed_rr_tp`, `compute_metrics`, `print_report`, `trades_to_dataframe`, `replay_record.write_run`. Replaces `_replay_span` + the worker pool + `_apply_cooldown` with kernel launches and host builders that turn the kernels' numeric codes into the exact strings (indicator descriptions, warnings, reasons) with the same f-string templates as `engine.py` / `signal_aggregator.py`, then call the real `SignalAggregator._clean_accepted_cp_warnings` and `_drop_stale_spread_gate_vetoes`. No CPU path: `--backend cpu`, `FB2_BACKEND=cpu` or a failed probe exit with a message that points to `gpu_backtest.py`; configurations beyond the compile-time limits (more than 4 confirmation frames, divergence lookback ≥ 64, swing lookback ≥ 256, `cp_lookback` ≥ 128, a big-candle window over 500 bars) are refused the same way. |
| `backtest/cuda_signal_kernels.py` (2,914 lines, most of it CUDA C in strings) | K1 `fb2_analyze_tf` (one thread per bar of one timeframe; the nine `GPUTechnicalEngine` overrides plus the live engine code they do not cover: divergence, structure S/R, candlestick, regime, regime-aware RSI, composite, the `len < 30` empty analysis, `ema_mode="2ema"`). K2 `fb2_aggregate` (one thread per primary bar; `analyze_from_dataframes` end to end: the multi-timeframe merge, bonuses, penalties in the post-sort decay order with the 30 cap, exhaustion block, signal, grade, gates 6b–6i, the XAU_ATR / GENERIC / XAU_PB / XAU_CP generators, the NEUTRAL-at-grade-C direction, `_apply_regime_filter_live`, plus the replay's quality / strategy filters, next-open entry and distance-preserving re-anchor). K3 `fb2_features` (the 65 `position_sizing.build_features` columns per accepted trade). K4 cooldown (single thread, the only cross-bar rule). K5 prefix median (the snapshot's `spread_estimate`). K6 recorder bar rows (the 160-column float32 row). The config packer (`PARAMS_F` / `PARAMS_I` name lists → generated `#define`s, so a name cannot drift), the classification table of every config field the kernels do not read, and thin test launches of the device helpers. Compiled with `--fmad=false`; imports only numpy, `gpu_kernels`, `config`, `chart_patterns` (pattern priority order) and `position_sizing` constants. |
| `test_all.py` Module 37 (16 tests) | Section 4. |
| `backtest/instructions_for_backtest.txt` | The 2026-09-18 CUDA-twin addendum. |
What stays on the host, on purpose: loading and cleaning the parquet cache; the calendar and
session flags (pandas time-zone arithmetic over the whole index: NY weekend, UTC weekday, blocked
close hours, and the three clocks the gates use: bar label for gate 6b, bar close for gate 6g3 and
XAU_CP, decision time for the replay skip); the text; the writers.
Rules that keep the twins bit-identical (they cost most of the debugging time and are now pinned
by tests): doubles everywhere; Python `min()` / `max()` as argument-order ternaries, never
`fmin` / `fmax`; every sum in Python's iteration order; numpy's pairwise summation (8 accumulators,
128-blocks) for `np.mean` / `np.nanmean`; Python 3.12's Neumaier-compensated builtin `sum()` for
the multi-timeframe score mean; pandas' online rolling mean and EWM recurrences copied from
`gpu_kernels.py` for the restarted tails the features use; `round(x, 2)` reproduced exactly with an
error-free product (the host additionally re-checks Python's `round()` on the device doubles of
every accepted trade); `atr_percentile = count / 50 * 100` (not `count * 2`: 15/50·100 =
30.000000000000004 flips "low" to "normal"); the as-of alignment compares int64 nanoseconds.
## 3. Evidence
Environment: NVIDIA GeForce RTX 4070 SUPER, CuPy 14.0.1 (`cupy-cuda12x` with the
`nvidia-cuda-runtime-cu12` header wheel), NumPy 2.2.6, pandas 3.0.2, Python 3.12, 16 CPUs. Code:
this working tree on branch `sql-integration` on top of `bac11d8`. `Backend:` lines verbatim:
reference `CuPy GPU (NVIDIA GeForce RTX 4070 SUPER, cupy 14.0.1)`, twin
`CuPy GPU (NVIDIA GeForce RTX 4070 SUPER, cupy 14.0.1)`; both log `GPU cache fallbacks: 0` (the
twin's line adds "no per-bar recompute path in cudaBasedBacktesting").
### 3.1 Acceptance: 6 months, both scripts, both costs
Harness: `venv/bin/python backtest/gpu_backtest.py --pair XAUUSD --primary-tf <tf> --start
2026-03-18 --end 2026-09-18 --spread <sp> --workers 12 --export-csv <out>` for the control and
`venv/bin/python backtest/cudaBasedBacktesting.py` with the same arguments (no `--workers`) for the
twin; factory config, no overrides.
| Primary | Cost | Control MD5 | Twin MD5 | Equal | Control wall | Twin wall |
|---|---|---|---|---|---|---|
| M5 | $0.65 | `86111c36c1a07e543a65f0cd09d56ebf` | `86111c36c1a07e543a65f0cd09d56ebf` | ✓ | 51.8 s (replay 43.8 s) | 1.27 s |
| M5 | $0.30 | `e70d025205844ecedc2b9e9c08f38f77` | `e70d025205844ecedc2b9e9c08f38f77` | ✓ | 47.1 s (replay 45.9 s) | 1.65 s |
| M15 | $0.65 | `b8c980833f1a32bd59930bd037c75b89` | `b8c980833f1a32bd59930bd037c75b89` | ✓ | 20.4 s (replay 18.7 s) | 1.24 s |
| M15 | $0.30 | `e66b5fbdaace1d3fda4a76a0e2d47d69` | `e66b5fbdaace1d3fda4a76a0e2d47d69` | ✓ | 14.8 s (replay 13.5 s) | 1.50 s |
Because the CSVs are identical the figures below are the same from either script (`NO_DATA` rows
excluded; halves split at 2026-06-18). They are reported because the instructions require them,
not as a validation: nothing changed, and the window is 6 months (rule 1 needs 12).
| Primary | Cost | Segment | Trades | Total R | Win rate | PF | Max DD (R) |
|---|---|---|---|---|---|---|---|
| M5 | $0.65 | whole | 283 | +41.67 | 56.5% | 1.42 | 10.1 |
| M5 | $0.65 | first half | 146 | +29.58 | 58.9% | 1.64 | 6.6 |
| M5 | $0.65 | second half | 137 | +12.09 | 54.0% | 1.23 | 8.3 |
| M5 | $0.30 | whole | 283 | +46.38 | 57.2% | 1.48 | 8.4 |
| M5 | $0.30 | first half | 146 | +32.07 | 60.3% | 1.71 | 6.4 |
| M5 | $0.30 | second half | 137 | +14.30 | 54.0% | 1.28 | 7.4 |
| M15 | $0.65 | whole | 238 | +16.35 | 60.1% | 1.19 | 14.9 |
| M15 | $0.65 | first half | 120 | +14.09 | 63.3% | 1.37 | 7.6 |
| M15 | $0.65 | second half | 118 | +2.26 | 56.8% | 1.05 | 14.4 |
| M15 | $0.30 | whole | 238 | +19.20 | 60.9% | 1.23 | 13.8 |
| M15 | $0.30 | first half | 120 | +15.37 | 65.0% | 1.41 | 7.1 |
| M15 | $0.30 | second half | 118 | +3.82 | 56.8% | 1.08 | 13.8 |
By generator at $0.65: M5 GENERIC 163 trades +42.4R, XAU_CP 104 −2.7R, XAU_ATR 16 +2.0R (BUY 244
+25.7R, SELL 39 +16.0R); M15 GENERIC 149 +26.3R, XAU_ATR 89 −10.0R (BUY 144 +9.7R, SELL 94
+6.7R). The M15 second half is the same weak stretch the 2026-09-17 live review saw; no decision
follows from a 6-month window.
### 3.2 The five reference books (kept cache, `--spread 0.65`, factory config)
| Book | Window | Twin MD5 | Pinned (CLAUDE.md §9) | Twin wall |
|---|---|---|---|---|
| M5 W2 | 2024-09-05 → 2026-09-05 | `49f8e602fc6ae769713c94ef7b257810` | ✓ | 2.4 s (reference 114.8 s) |
| M15 W2 | 2024-09-05 → 2026-09-05 | `b4aa07e33e17e23774c486a6900d673b` | ✓ | 2.3 s (37.2 s) |
| M15 OOS | 2024-06-01 → 2025-03-30 | `9fd624fb387c36052122bd1830cf9c32` | ✓ | 1.5 s (15.6 s) |
| H1 W3 | 2023-09-05 → 2026-09-05 | `7b848c917dcb5c371ecbb9f8f7292278` | ✓ | 1.3 s (13.0 s) |
| H4 W3 | 2023-09-05 → 2026-09-05 | `186e677fbbd53cca735c94094c23ce05` | ✓ | 0.8 s (21.2 s) |
Reference wall times are the GPU-backend rows of `gpu_backend_2026-09-18.md`. On the twin the
decision stage itself is milliseconds (M5 W2: K1 218,088 bars of three timeframes in 0.020 s, K2
141,819 primary bars in 0.002 s); what remains is loading and cleaning the parquet files, the
indicator precompute (1.3 s on the two-year M5 window), the host formatting of the accepted trades
and the CSV write.
### 3.3 Research flags and knobs (M15 OOS window unless stated; both scripts, `--spread 0.65`)
| Variant | Rows | Twin MD5 (prefix) | Equal |
|---|---|---|---|
| `--export-features` (130 columns) | 355 | `6239d197` | ✓ |
| `--record DIR --record-bars` | 355 | book `9fd624fb`; `trades` (355 × 138), `indicators` (8,120 × 15), `events` (6,902 × 14), `bars` (12,315 × 172) parquet tables equal via `DataFrame.equals`; manifests differ only in `cli`, `created_at`, `replay_seconds`, `workers` | ✓ |
| `--fixed-rr-tp 2.0` | 355 | `95a7d2ab` | ✓ |
| `--quality A+,A` | 249 | `1883c346` | ✓ |
| `--strategies XAU` | 3 | `c07a4025` | ✓ |
| `--cp` (all six chart patterns on M15) | 396 | `7ff51617` | ✓ |
| `--big-candle 3.0` | 104 | `76f76f3b` | ✓ |
| `--set backtest.regime_filter=true` (H4 ADX gate, a fourth frame) | 179 | `0a65af6b` | ✓ |
| `--set technical.ema_mode=2ema` | 368 | `faaa7e86` | ✓ |
| M5, `--set xau_strategy.m5_pullback_enabled=true --quality A+,A,B,C,D` | 279 | `1811117a` | ✓ |
| `--trail-arm 0.5 --trail-dist 0.9 --cooldown-bars 3 --max-hold-bars 150` | 414 | `9665d753` | ✓ |
| H1, `--swing-sr-lookback 80 --warmup-bars 300` | 75 | `f8ef2b9f` | ✓ |
One variant could not run on either script: H1 with `--confirm-tfs 15min,4h,1d` on the OOS
window needs a D1 file padded 1,400 days (2020-08-01) that is not cached, and Oanda returned an
empty page for that start; both scripts fail identically in `fetch_range`. Not a parity issue.
### 3.4 Test suite, three ways (final tree)
| Run | Result | Wall | Module 37 note |
|---|---|---|---|
| default (GPU) | `RESULTS: 552 passed, 0 failed, 552 total` | 116 s | every device test exercised |
| `CUDA_VISIBLE_DEVICES=""` | `RESULTS: 552 passed, 0 failed, 552 total` | 30 s | `CUDA unavailable here: NumPy CPU fallback (CUDARuntimeError: cudaErrorNoDevice: no CUDA-capable device is detected)` printed; device tests print a notice |
| `FB2_BACKEND=cpu` | `RESULTS: 552 passed, 0 failed, 552 total` | 31 s | `NumPy CPU fallback (forced: --backend cpu / FB2_BACKEND=cpu)` printed; the twin's refusal of a cpu-forced backend is exercised for real |
536 tests before this change, 16 added by Module 37.
## 4. Module 37 (what each test pins)
1. `fb2_round2` / `round1` against Python `round(x, 2)` / `round(x, 1)` on 400k doubles including
every `.xx5` half in ±300 and signed zero; without CUDA the rule is checked in exact arithmetic.
2. Parameter packing round-trips every decision leaf of the four factories (M5 blocked hours with
9 and without 21, M15 SELL hours 15/17 and the RSI × 0.5 table, H1 empty SELL quality, M15
`tp_floor_rr` 1.0, the six-regime × nine-indicator table, `FB2_EMA_LO` read once with the same
fallback); every field of the four config dataclasses is either packed or classified host-only,
so a new field fails the suite until it is placed; the `#define`s are generated from the lists.
3. K1 against `GPUTechnicalEngine.analyze` on every bar of a 600-bar fixture (doji, hammer,
shooting star, engulfing, marubozu, a flat stretch, plateaus) for 5min/15min/1h/4h under their
factories, partial-EMA off, `2ema`, classic RSI, disabled indicators and a 150-bar frame: valid
flag, composite, regime, volatility, every indicator's value / signal / confidence / description
/ effective weight / weighted score, key levels, pattern signals.
4. The arithmetic twins: numpy pairwise sums and `Series.mean` for n = 1…600, `nanmean` with NaNs,
EWM and rolling means restarted on tails, Wilder ATR on tails, `_ext_vs_sma`, the prefix median
with ties, CPython float floor division; and the rolling helper text is the prefix of
`gpu_kernels._ROLL_SRC`.
5. `align_cut` against the replay's `searchsorted` rule on gapped, late-starting M15/H1/H4/D1
frames for M5 and H1 primaries (`cut == 0` included).
6. `host_flags` against `_is_xau_blocked_by_calendar` plus the session block bar by bar across
both 2026 DST shifts, an H4 primary (label hour ≠ close hour), the day filter, and each filter
switched off.
7. K2 against `analyze_from_dataframes` on every analysed bar: the M5 factory over a 21-day
synthetic fixture (4,046 bars: all six grades, GENERIC and XAU_CP setups, both directions, the
mixed `trending` regime with both score signs, 23 distinct warnings); the M5 research knobs
(all six chart patterns, big-candle 3.0, the H4 regime filter, `FB2_EMA_LO=0.9`, grades to D;
every second bar); the M15 factory (1,182 bars, 62 candidates, XAU_ATR and GENERIC, the SELL,
exhaustion-block and spread gates); the H1 factory (shorts blocked) and the H4 factory with a
D1 confirmation shorter than 30 bars on a 30-day fixture. Compared per bar: score, confidence,
signal, grade, agreement, exhaustion count, momentum fading, penalty total and tags, the full
warning and cleaned-warning lists, both reason lists, price, spread estimate, regime,
volatility, the setup (strategy, direction, entry, stop, targets, rounded RR, pattern) and the
replay's candidate fields.
8. The device chart-pattern detector against `detect_chart_pattern` with all six patterns on the
fixture and on its mirror image (bear patterns), zero ATR included; the device big-candle
detector against `_detect_big_candle` (index and ratio) with injected abnormal bars.
9. K4 against `_apply_cooldown` (cooldown 0 / 1 / 5 / 17, empty, a dense run).
10. `Trade` rows, the 65 features, the recorder snapshots and the per-bar float32 matrix against a
real `_replay_span` over the whole M5 fixture with the pullback path re-enabled (XAU_PB, XAU_CP
and GENERIC trades; `dataclasses.asdict` field by field, exact floats, tz-aware entry times, the
CSV frame, dtypes and bytes with the research columns).
11. End to end: `cudaBasedBacktesting.main` against `gpu_backtest.main` on the fixture written to a
temporary cache directory (M15, two weeks): plain book MD5 equal; `--fixed-rr-tp 2.0
--export-features --record DIR --record-bars` MD5 equal and the four parquet tables equal.
12. Refusals: `--backend cpu`, a cpu-forced backend and a failed probe exit before the cache is
touched; each compile-time limit is refused with a message naming the knob and pointing to
`gpu_backtest.py`, the largest supported values pass; the parser is the reference script's;
`require_gpu_or_exit("gpu")` and the `Backend:` text on a working box.
13. Source guards: the script never calls `analyze_from_dataframes` or `_replay_span`, never
imports cupy or a live writer; the kernel module stays a leaf (no `engine`,
`signal_aggregator`, `gpu_backtest`, `replay_record`, live writer or ML import at module level),
pins `options=KERNEL_OPTIONS` (`--fmad=false`) and contains no `fmin(` / `fmax(`; the shared
code tables equal the recorder's; the live roots import neither new module.
On the GPU the module takes about 85 s, almost all of it the CPU references (about 5 ms per bar
through the live signal code). Without CUDA the device tests print a notice and the module takes
about a second.
## 5. Honest notes
- The twin is a second implementation of the decision stage. The MD5 gate proves equality on the
windows and flags tried, not in general; a future signal change that is not mirrored in the
kernels will make the twin drift silently. That is why the instructions addendum and CLAUDE.md
section 7 make every signal change a parity event, and why the reference script stays.
- Speed comes from the batch, not from the per-bar work: the decision kernels run one thread per
bar over the whole window at once. The remaining wall time (about a second per book) is parquet
loading, cleaning, the precompute and the host formatting of the accepted trades.
- `--workers` is accepted and ignored (logged); the twin never forks.
- Nothing in `engine.py`, `signal_aggregator.py`, `chart_patterns.py`, `position_sizing.py`,
`replay_record.py`, `gpu_backtest.py` or `gpu_kernels.py` changed. `git diff --stat` touches
`test_all.py`, `CLAUDE.md` and `backtest/instructions_for_backtest.txt` and adds the two new
modules and this note.
@@ -0,0 +1,149 @@
# M15 five-year book on the CUDA research twin — 2026-09-18
Owner request: a five-year M15 backtest with `backtest/cudaBasedBacktesting.py`. This window goes
past the three-year cap the owner set on 2026-09-05 (CLAUDE.md rule 1); it was run as an explicit
owner-requested exception. It is a read-out of the shipped configuration on a longer window, not
a candidate change: nothing was tuned, nothing ships from it.
## 1. Runs
Window 2021-08-01 → 2026-08-04 (the kept 5-year cache, padded files
`oanda_XAU_USD_{5min,15min,1h}_20210531_20260804.parquet`; 0 cache fallbacks; 122,489 M15 bars,
118,389 replayed; skipped 43,245 `SESSION_WINDOW`, 247 `WEEKEND_FILTER`). Factory config, no
overrides.
```
venv/bin/python backtest/cudaBasedBacktesting.py --pair XAUUSD --primary-tf 15min --start 2021-08-01 --end 2026-08-04 --spread 0.65 --export-csv cuda_m15_5yr_0.65.csv
venv/bin/python backtest/cudaBasedBacktesting.py --pair XAUUSD --primary-tf 15min --start 2021-08-01 --end 2026-08-04 --spread 0.30 --export-csv cuda_m15_5yr_0.30.csv
venv/bin/python backtest/gpu_backtest.py --pair XAUUSD --primary-tf 15min --start 2021-08-01 --end 2026-08-04 --spread 0.65 --workers 12 --export-csv ctrl_m15_5yr_0.65.csv # paired reference
```
| Run | Backend line (verbatim) | Wall | Book MD5 |
|---|---|---|---|
| twin $0.65 | `CuPy GPU (NVIDIA GeForce RTX 4070 SUPER, cupy 14.0.1)` | 6.6 s (K1 520,523 bars 0.046 s, K2 118,389 bars 0.001 s, trade sim 0.80 s) | `d5e432bdb3dc9923e00594125b4867dd` |
| twin $0.30 | `CuPy GPU (NVIDIA GeForce RTX 4070 SUPER, cupy 14.0.1)` | 5.3 s | `4ac5c0307123557c0740e57f5b91ff72` |
| reference $0.65 | `CuPy GPU (NVIDIA GeForce RTX 4070 SUPER, cupy 14.0.1)`, `GPU cache fallbacks: 0` | 93.7 s (replay 89.2 s, 12 workers) | `d5e432bdb3dc9923e00594125b4867dd` — identical to the twin's (`cmp` equal) |
The paired reference run is the parity evidence the 2026-09-18 addendum in
`backtest/instructions_for_backtest.txt` requires before a number from the twin is quoted.
## 2. Results
1,822 trades at both costs (1,087 `TRAIL`, 666 `SL`, 69 `TIMEOUT`, no `NO_DATA`). Halves split at
2024-02-02.
| Cost | Segment | Trades | Total R | Win rate | PF | Max DD (R) |
|---|---|---|---|---|---|---|
| $0.65 | whole 2021-08-01 → 2026-08-04 | 1822 | +157.43 | 56.8% | 1.22 | 43.6 |
| $0.65 | first half 2021-08-01 → 2024-02-01 | 704 | −9.61 | 52.4% | 0.97 | 43.6 |
| $0.65 | second half 2024-02-02 → 2026-08-04 | 1118 | +167.04 | 59.5% | 1.40 | 17.1 |
| $0.30 | whole | 1822 | +196.77 | 57.7% | 1.28 | 36.3 |
| $0.30 | first half | 704 | +9.38 | 54.0% | 1.03 | 36.3 |
| $0.30 | second half | 1118 | +187.39 | 60.1% | 1.46 | 14.9 |
Read against rule 1 for the record: the five-year window does **not** pass it (the first half is
negative at $0.65 and the whole-window PF is 1.22). The shipped M15 configuration was validated on
2024-09-05 → 2026-09-05 (+146.1R, PF 1.43) and the 2024-06 → 2025-03 out-of-sample window
(+60.7R, PF 1.49); the extra 2021–2023 years are where the R is missing, which is the same picture
the Oanda re-pin of 2026-08-04 gave ("all timeframes earn only 2024+") and the reason for the
three-year cap.
### Per year
| Year | Trades | $0.65 R | WR | PF | Max DD | $0.30 R | PF |
|---|---|---|---|---|---|---|---|
| 2021 (Aug–Dec) | 97 | +1.17 | 47.4% | 1.03 | 15.5 | +3.85 | 1.10 |
| 2022 | 318 | −15.37 | 51.9% | 0.89 | 39.1 | −6.73 | 0.95 |
| 2023 | 264 | +10.91 | 55.7% | 1.10 | 15.7 | +17.86 | 1.17 |
| 2024 | 397 | +52.86 | 58.7% | 1.36 | 17.1 | +62.53 | 1.44 |
| 2025 | 480 | +77.35 | 60.0% | 1.43 | 14.8 | +85.69 | 1.48 |
| 2026 (Jan–Aug 4) | 266 | +30.51 | 58.3% | 1.30 | 13.2 | +33.57 | 1.34 |
The 43.6R drawdown is entirely 2021-Q4 → 2022-Q3 (quarterly R: −13.5, +13.5, −7.6, −22.6); from
2024 on the worst quarter is −3.4R (2024-Q2) and the worst since is −8.3R for the five weeks of
2026-Q3 in the window.
### Breakdowns ($0.65)
| Grade | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| A+ | 36 | +5.48 | 63.9% | 1.41 | 3.3 |
| A | 970 | +92.30 | 56.5% | 1.24 | 21.3 |
| B | 728 | +41.16 | 56.5% | 1.14 | 31.3 |
| C | 88 | +18.49 | 59.1% | 1.55 | 4.8 |
| Generator | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| GENERIC | 1658 | +153.28 | 57.0% | 1.24 | 43.6 |
| XAU_ATR | 164 | +4.15 | 54.3% | 1.06 | 17.0 |
XAU_ATR produces no trade before 2024 (5 in 2024, 48 in 2025, 111 in 2026). The gold ATR setup
is gated by dollar-denominated floors (`min_atr_threshold`, the $8 stop floor, the 30 × $0.30
target floor); at 2021–2023 gold prices and volatility they blocked the generator and the GENERIC
swing setup took every A-grade signal. The pre-2024 book therefore runs under an effectively
different setup mix. Observation only; no proposal.
| Direction | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| BUY | 1212 | +147.91 | 58.3% | 1.32 | 48.3 |
| SELL | 610 | +9.52 | 53.8% | 1.04 | 36.2 |
Direction by year (trades / R): BUY 2021 58 / −9.8, 2022 215 / −28.9, 2023 166 / +10.9, 2024
273 / +56.8, 2025 344 / +95.2, 2026 156 / +23.7; SELL 2021 39 / +10.9, 2022 103 / +13.6, 2023
98 / 0.0, 2024 124 / −3.9, 2025 136 / −17.9, 2026 110 / +6.8. Shorts carried the book in the
2021–2022 bear phase and lost in the 2024–2025 rally; the parked cycle-2 candidate (block M15
grade-A SELLs, `cycle_2026-09-10.md`) is about the recent years only and this table does not
change its status.
| Regime | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| ranging | 168 | +45.30 | 61.3% | 1.73 | 7.7 |
| transitioning | 400 | +51.25 | 58.2% | 1.33 | 7.0 |
| trending_up | 788 | +55.43 | 56.9% | 1.18 | 37.3 |
| trending_down | 466 | +5.44 | 53.6% | 1.03 | 26.0 |
(The mixed `trending` regime is blocked on M15, so it has no row.)
| Volatility | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| low | 193 | +34.75 | 60.1% | 1.48 | 11.2 |
| normal | 251 | +41.65 | 61.0% | 1.50 | 20.6 |
| high | 393 | +2.77 | 53.7% | 1.02 | 19.2 |
| extreme | 985 | +78.27 | 56.2% | 1.20 | 32.0 |
| Entry hour UTC | Trades | Total R | WR | PF | | Entry hour UTC | Trades | Total R | WR | PF |
|---|---|---|---|---|---|---|---|---|---|---|
| 03 | 98 | +8.18 | 60.2% | 1.24 | | 14 | 381 | +24.60 | 55.4% | 1.16 |
| 04 | 38 | +4.62 | 55.3% | 1.29 | | 15 | 146 | +17.60 | 57.5% | 1.30 |
| 05 | 77 | +16.50 | 61.0% | 1.58 | | 17 | 79 | +27.77 | 68.4% | 2.20 |
| 06 | 117 | +27.74 | 66.7% | 1.85 | | 18 | 112 | −6.65 | 50.9% | 0.87 |
| 07 | 116 | −9.29 | 50.0% | 0.83 | | 19 | 80 | +10.95 | 62.5% | 1.40 |
| 08 | 158 | +1.62 | 53.2% | 1.03 | | 20 | 55 | +11.68 | 61.8% | 1.66 |
| 09 | 145 | −7.31 | 49.7% | 0.89 | | 22 | 40 | +12.48 | 67.5% | 2.01 |
| 12 | 180 | +16.94 | 54.4% | 1.22 | | | | | | |
(Entry hour is the open of the bar after the signal bar; the blocked close hours 0, 1, 2, 10, 11,
13, 16, 21 leave the gaps.)
| Weekday | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| Mon | 309 | +25.97 | 57.3% | 1.21 | 15.8 |
| Tue | 394 | +40.58 | 60.2% | 1.29 | 17.4 |
| Wed | 386 | −18.07 | 50.5% | 0.90 | 25.4 |
| Thu | 356 | +50.99 | 56.5% | 1.37 | 22.3 |
| Fri | 358 | +47.13 | 58.4% | 1.35 | 19.3 |
| Sun | 19 | +10.82 | 78.9% | 3.64 | 2.1 |
Tail: the top 5% of trades (91) carry +249.8R of the +157.4R total; median R +0.113; largest win
+9.07R, largest loss −2.04R; mean hold 70.5 bars. Same fat-right-tail shape as the 3-year cycle-1
reading (94–96% of R in the top 5% there).
## 3. What to do with this
Nothing. The numbers describe the shipped configuration on a window the owner does not use for
decisions; the per-hour, per-weekday and per-direction tables are in-book statistics on a
five-year mix of setup gates and must not be turned into filters (rule 1 needs a control-vs-
candidate run on the validated windows, and the 2026-08-04 age/stretch and 2026-09-06 sizing
verdicts already refute the obvious ones). If the owner wants the pre-2024 weakness explained,
the honest next step is a `--record --record-bars` run on this window with the twin (seconds) and
a scorecard on the dollar-floor gates by year, not a parameter change.
+156
View File
@@ -0,0 +1,156 @@
# M5 five-year book on the CUDA research twin — 2026-09-18
Owner request, the companion of `m15_5yr_cuda_2026-09-18.md`: a five-year M5 backtest with
`backtest/cudaBasedBacktesting.py`. Same owner-requested exception to the three-year cap
(CLAUDE.md rule 1), same status: a read-out of the shipped configuration on a longer window,
nothing tuned, nothing ships.
## 1. Runs
Window 2021-08-01 → 2026-08-04 (kept 5-year cache, padded files
`oanda_XAU_USD_{5min,15min,1h}_20210531_20260804.parquet`; 0 cache fallbacks; 367,394 M5 bars,
355,096 replayed; skipped 139,064 `SESSION_WINDOW`, 247 `WEEKEND_FILTER`). Factory config (M5:
hour 9 blocked and 21 open, chart-pattern entries on, pullbacks off), no overrides.
```
venv/bin/python backtest/cudaBasedBacktesting.py --pair XAUUSD --primary-tf 5min --start 2021-08-01 --end 2026-08-04 --spread 0.65 --export-csv cuda_m5_5yr_0.65.csv
venv/bin/python backtest/cudaBasedBacktesting.py --pair XAUUSD --primary-tf 5min --start 2021-08-01 --end 2026-08-04 --spread 0.30 --export-csv cuda_m5_5yr_0.30.csv
venv/bin/python backtest/gpu_backtest.py --pair XAUUSD --primary-tf 5min --start 2021-08-01 --end 2026-08-04 --spread 0.65 --workers 12 --export-csv ctrl_m5_5yr_0.65.csv # paired reference
```
| Run | Backend line (verbatim) | Wall | Book MD5 |
|---|---|---|---|
| twin $0.65 | `CuPy GPU (NVIDIA GeForce RTX 4070 SUPER, cupy 14.0.1)` | 5.2 s (K1 520,523 bars 0.045 s, K2 355,096 bars 0.004 s, trade sim 0.38 s) | `0429885713ca34653a39754c013db501` |
| twin $0.30 | `CuPy GPU (NVIDIA GeForce RTX 4070 SUPER, cupy 14.0.1)` | 5.6 s | `87bfdba14090459f715e1ee1f751a6cb` |
| reference $0.65 | `CuPy GPU (NVIDIA GeForce RTX 4070 SUPER, cupy 14.0.1)`, `GPU cache fallbacks: 0` | 353.2 s (replay 348.7 s, 12 workers, 1,018 bars/s) | `0429885713ca34653a39754c013db501` — identical to the twin's (`cmp` equal) |
## 2. Results
905 trades at both costs (561 `TRAIL`, 290 `SL`, 54 `TIMEOUT`, no `NO_DATA`). Halves split at
2024-02-02.
| Cost | Segment | Trades | Total R | Win rate | PF | Max DD (R) |
|---|---|---|---|---|---|---|
| $0.65 | whole 2021-08-01 → 2026-08-04 | 905 | +127.08 | 51.4% | 1.39 | 13.5 |
| $0.65 | first half 2021-08-01 → 2024-02-01 | 129 | −8.98 | 43.4% | 0.82 | 12.4 |
| $0.65 | second half 2024-02-02 → 2026-08-04 | 776 | +136.06 | 52.7% | 1.49 | 9.6 |
| $0.30 | whole | 905 | +147.83 | 52.5% | 1.46 | 10.6 |
| $0.30 | first half | 129 | −5.20 | 45.0% | 0.89 | 10.6 |
| $0.30 | second half | 776 | +153.03 | 53.7% | 1.57 | 8.9 |
Against rule 1 for the record: the five-year window fails it (first half negative), on a first
half of only 129 trades. The shipped M5 configuration was validated on 2024-09-05 → 2026-09-05
(+146.1R, PF 1.52, 781 trades) and the 12-month window (+108.8R, PF 1.54).
### Per year
| Year | Trades | $0.65 R | WR | PF | Max DD | $0.30 R | PF |
|---|---|---|---|---|---|---|---|
| 2021 (Aug–Dec) | 12 | −1.99 | 41.7% | 0.59 | 2.5 | −1.60 | 0.65 |
| 2022 | 50 | −2.18 | 44.0% | 0.90 | 11.1 | −0.71 | 0.97 |
| 2023 | 61 | −2.75 | 42.6% | 0.87 | 10.3 | −1.00 | 0.95 |
| 2024 | 102 | +0.97 | 49.0% | 1.02 | 8.2 | +4.30 | 1.11 |
| 2025 | 332 | +40.54 | 50.6% | 1.34 | 9.6 | +48.63 | 1.42 |
| 2026 (Jan–Aug 4) | 348 | +92.49 | 55.7% | 1.77 | 9.2 | +98.21 | 1.83 |
Two things the M15 window did not show as sharply:
1. **M5 barely trades before 2025.** 225 trades in the first 3.4 years against 680 in the last
1.6. Trade generation on M5 is governed by dollar-denominated floors: target 1 must clear
30 × $0.30 = $9 (`SPREAD GATE`), the generic and chart-pattern stops are floored at $8, and a
chart pattern needs a $9 height. At 2021–2023 prices and volatility an M5 ATR of $1.5–3 puts a
1.5 × ATR target far below the floor, so the spread gate vetoed almost every setup and only
high-volatility bursts survived. The pre-2025 M5 book is therefore a burst-only sample, not the
shipped strategy under normal conditions.
2. **One quarter carries the window.** 2026-Q1 made +90.2R of the +127.1R total (71%); 2025 Q1–Q2
another +32.3R. Quarterly R outside those three quarters ranges −6.5 … +5.8. This is the
fat-right-tail shape of every M5 reading (cycle 1: the top 5% of trades = 94–96% of R; here the
top 5%, 45 trades, carry +145.8R of +127.1R) compounded by a single very favourable quarter.
### Breakdowns ($0.65)
| Grade | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| A+ | 1 | −0.03 | 0.0% | 0.00 | 0.0 |
| A | 247 | +53.72 | 55.1% | 1.69 | 7.2 |
| B | 637 | +73.66 | 50.2% | 1.30 | 16.4 |
| C | 20 | −0.26 | 45.0% | 0.97 | 3.2 |
| Generator | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| GENERIC | 447 | +70.36 | 52.8% | 1.46 | 8.1 |
| XAU_CP | 427 | +48.52 | 49.9% | 1.29 | 14.4 |
| XAU_ATR | 31 | +8.20 | 51.6% | 1.89 | 3.8 |
Generator by year (trades / R): GENERIC 2021 4 / +0.4, 2022 23 / +4.4, 2023 32 / −2.9, 2024
37 / +6.6, 2025 170 / +18.1, 2026 181 / +43.8; XAU_CP 2021 8 / −2.4, 2022 27 / −6.5, 2023
29 / +0.2, 2024 65 / −5.7, 2025 159 / +21.6, 2026 139 / +41.3; XAU_ATR appears only in 2025 (3)
and 2026 (28), the same dollar-floor effect as on M15. The chart-pattern path is negative through
2024 (−14.4R over 129 trades) and positive since (+62.9R over 298); its validation
(2026-08-04, +38.9R of the 12-month book) covered the recent regime only.
| Direction | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| BUY | 808 | +119.68 | 51.4% | 1.41 | 15.2 |
| SELL | 97 | +7.40 | 51.5% | 1.21 | 11.4 |
Shorts are nearly absent before 2025 (9 trades in 2021–2024) under the M5 SELL gates; 2025 32 /
−2.8R, 2026 56 / +12.0R.
| Regime | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| trending_up | 481 | +75.44 | 53.4% | 1.44 | 11.9 |
| ranging | 174 | +24.78 | 50.0% | 1.36 | 8.3 |
| trending_down | 94 | +6.61 | 51.1% | 1.19 | 12.4 |
| transitioning | 84 | +19.59 | 46.4% | 1.69 | 6.9 |
| trending (mixed) | 72 | +0.67 | 47.2% | 1.02 | 10.5 |
| Volatility | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| low | 250 | +62.92 | 51.6% | 1.70 | 12.4 |
| normal | 169 | +0.69 | 47.9% | 1.01 | 12.5 |
| extreme | 486 | +63.47 | 52.5% | 1.38 | 13.3 |
(`high` has no row: the M5 chop-transition gate blocks it.)
| Entry hour UTC | Trades | Total R | WR | PF | | Entry hour UTC | Trades | Total R | WR | PF |
|---|---|---|---|---|---|---|---|---|---|---|
| 03 | 52 | +8.49 | 50.0% | 1.41 | | 15 | 121 | +12.81 | 53.7% | 1.30 |
| 04 | 31 | +3.22 | 61.3% | 1.27 | | 17 | 47 | −2.61 | 48.9% | 0.86 |
| 05 | 50 | +7.15 | 52.0% | 1.32 | | 18 | 51 | +13.17 | 58.8% | 1.76 |
| 06 | 56 | +24.31 | 58.9% | 2.47 | | 19 | 51 | +2.96 | 49.0% | 1.18 |
| 07 | 52 | +2.50 | 53.8% | 1.14 | | 20 | 43 | +31.45 | 46.5% | 2.80 |
| 08 | 63 | −4.88 | 41.3% | 0.81 | | 21 | 12 | +16.02 | 75.0% | 6.16 |
| 12 | 69 | +7.01 | 53.6% | 1.28 | | 22 | 37 | +4.14 | 45.9% | 1.32 |
| 14 | 170 | +1.37 | 47.6% | 1.02 | | | | | | |
(Entry hour is the open of the bar after the signal bar; M5 blocks close hours 0, 1, 2, 9, 10,
11, 13, 16 and leaves 21 open.)
| Weekday | Trades | Total R | WR | PF | Max DD |
|---|---|---|---|---|---|
| Mon | 172 | +20.80 | 55.8% | 1.36 | 12.3 |
| Tue | 180 | +24.22 | 52.8% | 1.36 | 13.2 |
| Wed | 200 | +30.84 | 48.5% | 1.43 | 11.4 |
| Thu | 167 | +5.27 | 48.5% | 1.08 | 13.3 |
| Fri | 177 | +41.41 | 50.3% | 1.62 | 9.2 |
| Sun | 9 | +4.55 | 77.8% | 3.22 | 1.0 |
Tail: median R +0.031; largest win +5.53R, largest loss −1.84R; mean hold 76.6 bars.
## 3. What to do with this
Nothing ships. Two honest readings for the owner:
- The five-year M5 figure (+127R) is not "five years of edge"; it is the validated 2024-09 →
2026-09 book plus three and a half years in which the dollar floors kept M5 almost out of the
market and the few trades that got through lost a little. The three-year cap already keeps
decisions inside the period where the gates behave as designed.
- The concentration in 2026-Q1 (71% of the window's R) is a fact about the shipped M5 edge, not
about the window length. It is consistent with every earlier M5 reading and is the reason the
advisory outcomes over a quiet fortnight can look nothing like the book.
If the owner wants the floor effect measured rather than inferred, a `--record --record-bars`
run of this window on the twin (seconds) with a by-year count of `SPREAD GATE` and `LOW ATR
FILTER` warnings per analysed bar would settle it. Changing the floors is a rule-1 event in its
own right (`round_trip_cost` drives the live target floor, CLAUDE.md rule 1) and is not proposed.
+1078
View File
File diff suppressed because it is too large Load Diff