Files
ForexBot2/backtest/research/sizing_campaign_2026-09.md
T
Harwinder Sekhon 6bf58fd9d4 research: 3-year stake-sizing campaign + violent-move studies (nothing changes signals)
Walk-forward "second opinion" stake multiplier (meta-labelling on 65 decision-time
features, 3yr Oanda books @ $0.65) FAILED the pre-registered rule on M5 (clean null)
and M15 (lift inside the label-shuffle null, explained by BUY-vs-SELL). Nothing that
changes numbers ships; the 12-month M5 book re-run is byte-identical to the pinned one.

Behaviour-neutral plumbing:
- position_sizing.build_features (65 features; multiplier path returns 1.0, no artifact)
- SignalReport.penalty_tags / penalty_total / momentum_fading (record-keeping)
- TradeSetup.stake_multiplier = None (reserved)
- gpu_backtest --export-features (default export stays the pinned 63 columns; extras
  forced to the tail even when the first trade lacks an indicator)

Research tools (backtest/research): book_report, burst_map, burst_precursors,
open_trade_bursts, calendar_impact, burst_indicator_map, sizing_model, watch_rule_eval.
Findings recorded in backtest/research/sizing_campaign_2026-09.md: M5 stretch measures
are positive in both halves; every open-trade protection against violent bars loses on
both TFs; high-impact USD releases cause 20-31% of violent bars but trades held through
them do better; violent bars come when price is already at an extreme, not from calm.

Module 29 tests (13). Suite: 435 passed.

Claude-Session: https://claude.ai/code/session_01RWV2yLt3YXHHGVYHSQU2EE
2026-09-05 21:19:24 -04:00

349 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Stake-sizing campaign — 2026-09-05 (3-year Oanda window, $0.65)
## Purpose and owner decisions
Owner asked (2026-09-05): predict direction and choose order size better; protect against
trend exhaustion at signal time; map hours/days where institutions move gold violently;
backtests ≤ 3 years. Decisions taken in planning:
- Order size = a per-signal RELATIVE stake ("Stake: 0.7× normal"), not account-based units.
- Exhaustion protection = RISK LESS, DON'T SKIP (every hard exhaustion block tested since
2026-07 removed net-positive trades — the winners are the stretched runs).
- Model only M5 and M15; H1/H4 measured descriptively.
- scikit-learn / lightgbm allowed in ./venv for research only; the shipped artefact is
numpy-only JSON. Neither library enters requirements.txt.
- No tuned threshold, gate or exit changes (rule 1). Sizing is a post-decision overlay computed
by `position_sizing.build_features` / `stake_multiplier` INSIDE `analyze_from_dataframes`, so
live and backtest use the same call on the same objects (rule 4).
## Harness
`backtest/gpu_backtest.py --pair XAUUSD --start 2023-09-05 --end 2026-09-05 --spread 0.65
--workers 12`, pair-factory config (no --set/--quality/--strategies/--confirm-tfs), NumPy CPU
backend (CuPy cannot locate CUDA_PATH on this box; the 2026-09-03 pinned books were also CPU).
Cache: `oanda_XAU_USD_{5min,15min,1h,4h}_20230705_20260905.parquet`, `…_1d_20221110_20260905`.
Feature books add `--export-features` (65 `f_*` columns + `stake_multiplier` + `r_sized`; the
default export stays the pinned 63-column shape). Outputs in `backtest/research/sizing_2026-09/`
(gitignored).
## Baseline (control books, unmodified backtester, 2026-09-05 23:36–23:41 UTC)
| TF | trades | total R | WR | PF | max DD |
|---|---|---|---|---|---|
| M5 | 857 | +161.32 | 53.7% | 1.53 | 9.85 |
| M15 | 1086 | +148.69 | 59.1% | 1.37 | 15.47 |
| H1 | 255 | +70.52 | 64.3% | 1.98 | 4.43 |
| H4 | 113 | +22.68 | 58.4% | 1.63 | 5.39 |
### Behaviour-neutrality checks (2026-09-05 23:57 UTC)
- 12-month M5 canonical window (2025-08-14 → 2026-08-14, $0.65) re-run on the modified tree:
`book_diff` vs `backtest/oanda_m5_12mo_065_nopb_20260903.csv` = ΔR +0.0, Δtrades +0, and the
two CSVs have the SAME MD5 (`a28fed4becd056466c26d6a947e1a0e5`). Record-keeping fields,
`SizingConfig`, and the `--export-features` hook change nothing when the flag is off.
- Feature books: the 63 shared columns are value-identical to the control books on every TF
(`assert_frame_equal`). A column-ORDER quirk was found and fixed the same day: pandas orders
export columns by first appearance, so when the first trade of a book lacks an indicator
(the 3-year M5 book's first trade had no candlestick pattern) that indicator's columns were
appended after the extras. `trades_to_dataframe` now forces the extras to the tail; test
extended with that exact case (435 tests green).
## Pre-registered acceptance rule (written 2026-09-05, BEFORE any model fit)
A per-timeframe stake-multiplier model may ship only if, evaluated on OUT-OF-SAMPLE quarters
only (all calendar quarters after a 12-month burn-in, each predicted by a model trained
exclusively on trades whose entry time is ≥ 300 primary bars before that quarter's start,
expanding window, standardisation fit on train only), ALL of the following hold:
1. Sized total R > flat total R in BOTH halves of the OOS period (split at the median OOS
entry time).
2. Sized total R > flat total R over the full OOS period.
3. Sized max drawdown (0-prepended cumulative sized-R curve, as `compute_metrics`) ≤ flat max
drawdown × 1.10.
4. Sized OOS book has PF ≥ 1.3 and positive total R in both halves (the rule-1 floor).
5. Real OOS lift > the 95th percentile of the lift distribution from 50 label-shuffle refits
(R permuted within training windows only; test labels and walk-forward geometry unchanged).
6. Mean multiplier over OOS trades within 1.00 ± 0.05; every multiplier in [0.5, 1.5].
Plus the mechanical check: the `--sizing` full-window replay reproduces the flat book's
entry/exit/`r_multiple` columns exactly (sizing changes no entries).
Model family and size map (fixed): primary = ridge regression on winsorised R (clip to
[−1.5, +4]); secondary = L2 logistic on R>0 (ships only if the primary fails and it passes);
simplicity baseline = one-feature model on `f_is_buy` (the shipped model must beat its OOS
lift, else the simpler one ships if it passes); upper bound = lightgbm on all `f_*` (never
ships; if its OOS lift is inside the shuffle null, stop); ablation = exhaustion-block-only model.
Map: `m = 0.5 + 1.0 × clip((z − q10)/(q90 − q10), 0, 1)` with q10/q90 from TRAIN predictions,
times a normaliser making the mean train multiplier 1.0, clipped to [0.5, 1.5], rounded to 0.05.
`SHIPPED_FEATURES` (≤ 12 per TF, ≥ 2 from the exhaustion block) are chosen from the Stage-B
sign-stable screen on the 3-year feature books and recorded below BEFORE the first fit; any
later change restarts the shuffle control. If any clause fails on a timeframe, that timeframe
ships nothing. No widening the clip, changing the split, dropping the purge, re-picking
features or adding quarters to rescue a result.
## Pre-registered feature lists
Derived MECHANICALLY (script in the session log, saved as `sizing_2026-09/preregistered_features.json`):
sign-stable `f_*` features from the Stage-B screen (top-minus-bottom quintile mean-R gap has the
same sign in both halves, both halves ≥ 10 trades per bucket), zero-variance columns removed,
ranked by min(|gap_h1|, |gap_h2|), top 12; the ≥ 2-from-the-exhaustion-block clause was already
satisfied on both TFs. Written 2026-09-06 00:05 UTC, before any Stage-D fit.
- **M5 (29 sign-stable candidates):** f_sl_atr, f_confidence, f_rsi, f_mtf_c1, f_swing_dist_atr,
f_ret_48_atr, f_tag_no_pullback, f_rsi_dir, f_first_friday, f_mtf_agreement,
f_exhaustion_count, f_ret_24_atr. Exhaustion-block members: f_swing_dist_atr,
f_tag_no_pullback, f_exhaustion_count (all three run POSITIVE in both halves).
- **M15 (37 sign-stable candidates):** f_rsi, f_bb_pos, f_day_of_month, f_tag_ema200_near,
f_atr_pct, f_is_buy, f_confidence, f_close_pos_in_bar, f_tag_weak_pullback, f_tag_exhaustion,
f_penalty_total, f_ret_6_atr. Exhaustion-block members: f_tag_exhaustion, f_penalty_total
(both NEGATIVE in both halves; f_tag_weak_pullback and f_tag_ema200_near also negative).
Exhaustion-only ablation list (fixed by the plan): f_swing_dist_atr, f_ema21_dist_atr,
f_ema200_dist_atr, f_exhaustion_count, f_penalty_total, f_tag_bb_overextended,
f_tag_rsi_extended, f_tag_no_pullback, f_tag_exhaustion.
## Stage B — book breakdown (3-year feature books)
Tool: `backtest/research/book_report.py` (outputs in `sizing_2026-09/report_{5min,15min,1h,4h}/`).
Halves = median TRADE split (M5 cut 2025-12-12 because trade density doubled in the last year;
M15 cut 2025-04-25; H1 2025-04-09).
**M5 (857 trades, +161.3R, PF 1.53, DD 9.85; halves +51.0R PF 1.33 / +110.3R PF 1.74; max 11
wins / 8 losses in a row; longest flat stretch 88 trades).** Mix: GENERIC 469, XAU_CP 388, BUY
765 / SELL 92 (SELLs +11.8R, positive both halves). Grade: A +0.26R/trade (227) > B +0.17 (608)
> C −0.01 (21). Session (decision hour): NY 17–22 best (+0.31R/trade, PF 1.88), overlap 13–16
weakest (+0.08, PF 1.22) — both stable. Weekday: Wed and Fri best and stable; Mon/Tue flip sign.
Feature screen: 29 sign-stable `f_*` features. **Every stretch measure is POSITIVE**: wider
stop in ATR (+0.22/+0.23 gap), higher RSI (+0.20/+0.19), farther from the last swing
(+0.18/+0.27), 48-bar run-up (+0.18/+0.20), the `no_pullback` tag (+0.17/+0.28), more exhaustion
tags (+0.12/+0.24). Only ATR-relative-to-500-bar-mean is negative (−0.11/−0.12: entries during
a volatility spike do worse). "Risk less when stretched" has NO support on M5 — a model that
honours the data will size stretched M5 trades UP, not down.
**M15 (1086 trades, +148.7R, PF 1.37, DD 15.47; halves +65.8R / +82.9R; max 12 wins / 8 losses;
flat stretch 131 trades).** All GENERIC; BUY 739 (+158.4R, PF 1.62) vs SELL 347 (−9.7R, PF 0.93;
−19.2R in half 1, +9.4R in half 2). Grade NOT monotone: C +0.45R/trade (82, PF 2.41) > A +0.14
(481) > B +0.08 (516) — stable in both halves (matches the 2026-08-04 quant review). Session:
Asian and NY ≈ +0.18–0.20R/trade, London and overlap ≈ +0.09–0.10 (all stable). Weekday:
Wednesday flat/negative in both halves (229 trades, −0.6R); Thu/Fri flip sign between halves.
Feature screen: 37 sign-stable features. Here the exhaustion block runs NEGATIVE: `exhaustion`
tag (−0.19/−0.10), `weak_pullback` tag (−0.24/−0.11), `ema200_near` tag (−0.17/−0.32), penalty
total (−0.16/−0.10), ATR percentile (−0.20/−0.15); while raw RSI (+0.31/+0.24) and Bollinger
position (+0.36/+0.22) are positive. So on M15 "stretched by the aggregator's own penalty
logic" is bad while "strong momentum reading" is good — the sizing model gets both.
**H1 (255 trades, +70.5R, PF 1.98, DD 4.43; halves +28.7R PF 1.79 / +41.9R PF 2.17)** and
**H4 (113 trades, +22.7R, PF 1.63, DD 5.39)** are descriptive only. H1 is all-BUY by
construction (shorts blocked 2026-09-03); Mon/Thu strongest, Friday the only losing weekday
(−1.4R/50, flips between halves); 03:00 UTC decision hour is the best single hour (+20.0R/40,
stable) and 04:00–05:00 negative.
**Labelling quirk (pre-existing, not fixed):** 33 M5 trades carry the backtester `strategy`
tag GENERIC while their setup strategy is XAU_ATR (`f_strat_atr = 1`); the CSV tag and the
setup's own string disagree for the ATR path. Labels only — R is unaffected.
**Stop-trigger check: PASSED** (both TFs have many sign-stable features) → proceed to Stage D.
## Stage C — institutional burst / time map
Tool: `backtest/research/burst_map.py` on the 3-year raw bars (`burst_5min/`, `burst_15min/`
under `sizing_2026-09/`). Burst = bar range ≥ K × prior Wilder ATR(14). Base rate at K=3:
0.86 % of M5 bars (1840), 1.19 % of M15 bars (846).
**Where the violent bars cluster (bar-CLOSE hour, UTC, K=3 burst % of bars):**
- 12:00–13:00 (US data window): M5 2.4 % / 2.3 %, M15 3.7 % / 5.0 % — 3–4× the base rate;
Friday 12:00–13:00 is the densest weekday cell on M5 (3.4 % / 3.1 %). Largest mean 1-hour
dollar moves are at close-hours 13–15 (M5: $7.8 / $9.4 / $8.5 vs ≈$5 elsewhere).
- 01:00 (Asia open): M5 2.9 %, M15 3.3 %. Also 05:00 (Tokyo/London hand-over) ≈1.2 % on M5.
- 22:00–23:00 (post-settlement thin book): ≈1.3–1.4 % on M5; Sunday reopen 22–23 h is the most
burst-prone slot of all (2.4–3.0 % of bars) but the calendar already blocks Sunday entries
before 18:00 NY.
- Quietest hours: 03:00–04:00 and 16:00–17:00 (0.14–0.3 %).
**Post-burst drift (in the burst's own direction, ATR units):** ≈ 0 at every horizon on both
TFs (M5 6/12/24/48-bar means −0.01/−0.01/+0.09/+0.05; M15 −0.08/+0.14/+0.18/+0.17; % positive
46–51 %). London-session bursts on M5 show a mild short-horizon reversal (42 % positive at 6
bars). No continuation or reversal edge worth a rule.
**Do bursts hurt the system's entries? No.** Trades DECIDED within 12 bars after a K=3 burst:
M5 149 trades, mean +0.199R, PF 1.62 vs +0.186R / PF 1.52 for the rest; M15 242 trades,
+0.160R / PF 1.47 vs +0.130R / PF 1.34. At K=4/5 the after-burst group is better still
(M5 +0.276R PF 1.98; M15 +0.259R PF 1.80). This re-confirms the 2026-07-30 big-candle-gate
rejection with 3 years of data: expansion bars are part of the entry signal, not a danger.
(Group (b) "burst while open" is survivorship-shaped — winners live longer — and is not evidence.)
**The 40 largest 1-hour moves** are almost all one episode (2026-01-28 → 2026-02-06, incl. a
−$394 hour at 15:45 UTC on 2026-01-29, and 2026-03-23), not recurring clock slots; only 3 of 40
fall in the 12:30 data slot and 1 in the Wednesday 18:00 central-bank slot (slot labels only —
no economic calendar file exists in the repo). Violent moves are episodic regime events; the
recurring, predictable clustering is the 12:00–15:00 UTC US-data window and the 01:00 Asia open.
**Use in this campaign:** `f_bars_since_burst` and the hour/session features are already in the
feature set, so Stage D can weigh them as a stake input; no entry gate is proposed (rule 1).
## Stage D — walk-forward results (2026-09-06 00:20 UTC)
Driver: `backtest/research/sizing_model.py` (numpy closed-form ridge + Newton logistic; lightgbm
4.7 + scikit-learn 1.9 installed in ./venv for the upper bound only; NOT in requirements.txt).
Outputs: `sizing_2026-09/sm_{m5,m15}_{ridge,logistic}/` (summary.txt, oos_trades.csv,
quarters.csv, coefs_by_quarter.csv, shuffle_lifts.csv). Validation of the driver: planted
synthetic effect recovered (lift +116.7R, shuffle rank 100 %, all clauses PASS); pure-noise
book correctly FAILS (rank 32 %); ridge matches `lstsq` to 2e-16; logistic gradient < 3e-15.
**M5 (5 OOS quarters 2025Q3→2026Q3, 619 trades — 2024Q4–2025Q2 skipped by the pre-registered
min-train rule because only 96/119/142 trades precede them; 2026Q1 alone is +92R of the +125R
flat OOS):**
| model | flat R | sized R | lift | PF | DD | mean m | shuffle p95 | rank | verdict |
|---|---|---|---|---|---|---|---|---|---|
| ridge (primary) | +125.41 | +121.98 | −3.43 | 1.63 | 10.69 | 0.891 | +13.69 | 48 % | FAIL (1,2,5,6) |
| logistic | +125.41 | +128.40 | +2.99 | 1.66 | 8.70 | 0.900 | +14.39 | 62 % | FAIL |
| isbuy baseline | +125.41 | +123.22 | −2.20 | 1.55 | 11.92 | 1.000 | +7.05 | 4 % | FAIL |
| exhaustion-only | +125.41 | +118.03 | −7.38 | 1.59 | 10.88 | 0.918 | +14.36 | 28 % | FAIL |
| lightgbm (all 65) | +125.41 | +116.05 | −9.36 | 1.51 | 11.64 | 1.007 | +7.60 | 10 % | FAIL |
Ridge weights (standardised, 5 quarters): `f_tag_no_pullback` +0.018 sign-stable 5/5 — the
"no pullback" flag INCREASES the stake; `f_swing_dist_atr` and `f_exhaustion_count` ≈ 0 and
sign-unstable. Exhaustion-only sizing LOSES 7.4R. **A clean null: nothing in the 65 features
predicts M5 trade outcomes beyond noise, linear or non-linear.**
**M15 (8 OOS quarters 2024Q4→2026Q3, 762 trades; flat +115.93R PF 1.42 DD 13.73):**
| model | sized R | lift | PF | DD | mean m | shuffle p95 | rank | clauses |
|---|---|---|---|---|---|---|---|---|
| ridge (primary) | +142.04 | +26.11 | 1.53 | 14.46 | 0.980 | +28.50 | 88 % | 1,2,3,4,6 PASS · **5 FAIL** |
| logistic | +144.78 | +28.84 | 1.53 | 13.97 | 1.002 | +31.89 | 92 % | 1,2,3,4,6 PASS · **5 FAIL** |
| isbuy baseline | +139.38 | +23.45 | 1.50 | 16.44 | 1.007 | +24.47 | 58 % | FAIL (3, 5) |
| exhaustion-only | +123.83 | +7.90 | 1.45 | 14.85 | 1.010 | +19.77 | 70 % | FAIL |
| lightgbm (all 65) | +123.87 | +7.94 | 1.44 | 14.74 | 0.996 | +17.66 | 60 % | FAIL |
Ridge weights sign-stable in 8/8 quarters: `f_tag_exhaustion` −0.022, `f_tag_ema200_near`
−0.033 (both SHRINK the stake — the "risk less when stretched" direction), `f_rsi` +0.028,
`f_bb_pos` +0.030, `f_is_buy` +0.029, `f_close_pos_in_bar` +0.022, `f_ret_6_atr` +0.022;
λ = 1000 every quarter (heavy shrinkage — the score is weak and the map is essentially a rank
transform). Sized R beat flat in 6 of 8 quarters and in both halves, BUT the label-shuffle
null has p95 = +28.5R: a random re-weighting of these 12 pre-screened features produces the
same lift 12 % of the time, and the one-feature BUY/SELL baseline already delivers +23.5R of
the +26.1R. **The model's information beyond "buys are better than sells" (≈ +2.7R over 762
trades) is indistinguishable from noise.**
## Verdict (pre-registered rule applied, no rescue attempted)
**FAIL on both timeframes → no stake multiplier ships.** No artifact, no `sizing.enabled`, no
alert line, no `--sizing` backtester flag. What remains in the tree is behaviour-neutral
research plumbing: `position_sizing.build_features` (65 decision-time features; the multiplier
path returns 1.0 and no artifact exists), `SignalReport.penalty_tags/penalty_total/
momentum_fading` (record-keeping), `TradeSetup.stake_multiplier` (None; reserved), the
`--export-features` flag, `book_report.py`, `burst_map.py`, `sizing_model.py`, and Module 29
tests. `SizingConfig` was REMOVED again before merge (rule 8: nothing reads it).
**What the owner should take from this (plain language):**
1. On the 5-minute chart there is nothing to size on. Stretched trades are the good ones; the
system's exhaustion flags carry no usable information about the outcome; a flexible model
given all 65 facts does worse than flat sizing out of sample.
2. On the 15-minute chart the only robust sizing signal is BUY vs SELL, which the gates already
act on. The exhaustion flag does point the "risk less" way in every one of 8 unseen
quarters, but the money it would make (≈ +3R per year) is inside noise at 1,086 trades.
3. "Predict direction better" with a learned second opinion is closed at this sample size:
the strongest non-linear model (all features, gradient boosting) fails on both charts.
**Open avenues (not rescues — each needs a NEW pre-registration):**
- Re-run the M15 exhaustion-tag sizing when the book has ≈ 2× the trades (≈ 2028) or on a
sealed forward sample; the sign is stable, the magnitude is not proven.
- The shuffle null is wide because the size map rank-stretches a weak score to 0.5–1.5×; a
narrower map (e.g. 0.75–1.25×) would shrink both lift and null — pre-register before testing.
- Session and weekday structure (M5: NY best / overlap weakest; M15: Wednesday flat) is stable
across halves but was rejected as a gate repeatedly; as a *size* input it was inside the
12-feature model and did not survive the null.
## Addendum 2026-09-06 — violent bars: signs before them, and protecting OPEN trades
Owner follow-up: "we might have an order running for hours before a violent move; we want
protection." Tools: `backtest/research/burst_precursors.py` (signs before a burst) and
`backtest/research/open_trade_bursts.py` (exact replay of every book trade on raw bars with the
backtester's exit model — reproduces `r_multiple` on 100 % of trades — plus protection variants).
Outputs: `sizing_2026-09/burst_precursors_{m5,m15}.txt`, `open_trade_bursts_{m5,m15}.txt`.
**Signs before a burst (both TFs, both halves stable):** the clock (close-hours 01, 12, 13 UTC
hold ~40 % of bursts in 13 % of bars); tick activity in the last 3 bars vs 50-bar mean (top
quintile ≈ 2× rate); the last bars already wider than normal (top quintile ≈ 2.2×; the
tightest quintile ≈ 0.3–0.5× — NOT calm-before-storm at bar scale); a move already under way
over the past hour (≈ 1.6×); a burst minutes ago (≈ 1.4×). Out of sample (fit half 1, score
half 2) a logistic model reaches AUC 0.75–0.77 for the next bar; the top 5 % of moments carry a
3.4–5.5 % burst chance vs 0.8–1.1 % base, and 24 % vs 9 % (M5) for "within the next hour".
Burst direction continues the prior hour's drift 61 % of the time. Signs say "ripe", not "now".
**Damage to open trades (base exits):** M5 — 164 trades (19 %) meet an opposing burst while
open: mean open R +0.42 just before → −0.06 just after → final +0.01; 43 % are stopped on that
bar; 54 % end as losers. M15 — 246 trades (23 %): +0.27 → −0.20 → final −0.12; 39 % stopped on
the bar. Trades already in profit before the burst still finish positive on average (M5 +0.53,
M15 +0.27). Bursts WITH the trade (n 176 / 246) finish at +1.11 / +0.83R — they are where the
winners come from.
**Protection variants — ALL negative on both TFs, both halves:**
| variant | M5 ΔR (h1/h2) | M15 ΔR (h1/h2) |
|---|---|---|
| flatten before US-data hours (12–13 UTC) | −31.9 (−13.6/−18.3) | −103.0 (−67.4/−35.6) |
| flatten before Asian open (00–01 UTC) | −38.7 (−22.0/−16.7) | −73.0 (−42.0/−30.9) |
| protective 0.5R stop through the data hours | −29.6 (−12.3/−17.3) | −82.3 (−55.5/−26.8) |
| protective 0.5R stop through the Asian open | −19.2 (−9.5/−9.7) | −35.1 (−17.3/−17.8) |
| exit next open after an opposing burst | −19.1 (−6.5/−12.5) | −6.9 (−11.9/+5.0) |
| 0.5R protective stop after an opposing burst | −9.0 (−3.0/−6.0) | −8.3 (−15.5/+7.3) |
The existing stop/trail already contains the opposing-burst damage (≈ 0.4–0.5R per hit); every
pre-emptive rule removes far more from the with-trade bursts than it saves. **Verdict: no
open-trade protection rule ships; do not re-propose flatten/tighten-before-window or
react-to-burst exits without a new mechanism.** Display-only "violent-move conditions" note on
alerts is the only harmless use of the precursor signs.
## Addendum 2026-09-06 — calendar events, indicator states and time slots before violent bars
Tools: `backtest/research/calendar_impact.py` (needs `backtest/cache/calendar.parquet`, scraped
2023-09-01→2026-09-10 with `ff_calendar_scraper.py`: 15,124 events, 1,055 high-impact USD in
window) and `backtest/research/burst_indicator_map.py` (states from the backtester's own
`precompute_indicators`, taken at t−1). Outputs `sizing_2026-09/calendar_impact_{m5,m15}.txt`,
`burst_indicator_map_{m5,m15}.txt`. Burst = range ≥ 3× prior ATR14; lifts quoted are stable in
BOTH halves on both TFs unless noted.
**Calendar.** The M5 bar containing a high-impact USD release averages 4.4× normal range; 54 %
of such releases produce a burst within the hour (base 9 %). By event: Non-Farm Payrolls (with
Unemployment Rate / Avg Hourly Earnings) 97 %, 9.3× bar, $11.9 mean 1-h move; CPI 91 %, 8.3×,
$9.0; FOMC decision/statement 87.5 %, 7.1×, $18.2 (press conference a further $20 hour); FOMC
projections 92 %; Retail Sales 64 %; PPI 54 %; Core PCE 50 %; Unemployment Claims 44 %; GDP
45–58 %; ISM PMIs 26 %; Powell speaks 33 %; Trump speaks 21 %; UoM sentiment ≈ 5 %. 20 % (M5)
/ 31 % (M15) of ALL bursts fall within ±30 min of a high-impact USD release (3.7–4.2 % of time);
in the 30 min AFTER one, next-bar burst odds are 8–17 % (7–16× base); 15 min before, 2–3 %.
Open trades that held through a high-impact release did BETTER (M5 +0.41R vs +0.16R; M15 +0.27
vs +0.07), even when the first 15-min move went against them (M5 +0.09, M15 +0.22). Flattening
5 min before a release: −5.4R (M5) / −59.3R (M15); 0.5R protective stop through the hour:
−2.4R / −43.1R. **Events are the biggest single cause of violent bars and still not a reason
to touch an open trade.**
**Indicator states before a burst (next-bar lift, M5 / M15).** Price OUTSIDE the Bollinger
bands: below 2.9× / 1.8–2.9×, above 2.1× / 2.2×, middle 0.2–0.8 band 0.7×. RSI < 30: 2.6–2.9× /
1.8–2.2×; RSI ≥ 70: 1.9–2.1× / 1.6–1.9×; RSI 40–60: 0.7× (calmest). Close outside the Keltner
channel: 1.4–2.2×; inside 0.76×. ADX RISING > 3: 1.4–1.5× / 1.2×; ADX FALLING < −3: 0.5–0.6×
(safest state); the ADX level itself carries nothing. MACD histogram negative and still falling
1.3× / 1.2–1.4×; histogram fading (either sign) 0.8×. Fully bearish 4-EMA stack 1.3–1.4× /
1.3×; "mostly bull" 0.8× on M5. Regime: transitioning 1.4–1.6× / 1.5–1.6×, ranging 0.88×.
Volatility regime: extreme 1.5–1.6× / 1.4–1.9×, low 0.6–0.8×. ROC beyond ±0.3 %: 1.2–1.9×
(>1 %: 1.3–10×, n small). Bollinger-width squeeze: 0.59× on M5 (LESS likely next bar — narrow
bands are not a warning); TTM squeeze (bands inside Keltner) 1.6× on M15 only. EMA200 distance,
day of month, first-Friday-as-a-day: nothing stable (first Friday matters only through its
12:30 slot).
**Time slots (M5 next-bar burst %, base 0.86 %):** Fri 01:xx 3.4 %, Fri 12:xx 3.4 %, Tue/Wed
01:xx 3.2 %, Thu 12:xx 3.1 %, Fri 13:xx 3.1 %, Sun 22:xx reopen 3.0 %, Thu 01:xx 2.7 %, Wed
12:xx 2.6 %, Sun 23:xx 2.4 %, Tue 00:xx 2.3 % (all 2.5–3.9× base). Quietest: 03–04 and 16–17.
**Direction of the burst (share going UP):** fully-bull stack 62 % (M5) / 50 % (M15); fully-bear
stack 26 % / 29 %; RSI < 30 → 22 % / 30 % up (i.e. 70–78 % DOWN); RSI ≥ 70 → 64 % / 52 % up;
MACD neg & falling → 30 % / 34 % up; trending_down regime → 26 % / 26 % up; trending_up → 60 % /
46 % up. Bearish states foretell a DOWN burst reliably on both TFs; bullish states foretell an
UP burst on M5 only.
**Reading:** violent bars arrive when price is already at an extreme with momentum accelerating
(outside the bands, RSI extreme, ADX rising, MACD still falling, transitioning regime, extreme
volatility) and/or a release is minutes away — "the storm is already brewing", not "calm before
the storm". Calm mid-band bars with fading momentum and falling ADX are the safest. None of it
changes trade handling (protection tests above); it is display material for a warning line.
## Addendum 2026-09-06 — violent-move WATCH rule (display-only feature, `violent_watch.py`)
Owner asked for a warning. Delivery: a line on signal alerts + a standalone heads-up to the owner
(once per hour per trigger set, daily cap), dark by default (`violent_watch` config block).
The first draft rule ("hot" = calendar | ≥3 indicator readings | clock & ≥2 readings) was
evaluated bar-by-bar on 3 years (`backtest/research/watch_rule_eval.py`, scratch variant
sweep): it flagged 27 % of bars hot at only ≈2× odds and would have sent ≈13 heads-ups/day on
M5 — rejected. Sweep of 11 variants (M5 / M15, both halves):
| hot definition | bars hot | next-bar lift h1/h2 | next-hour rate | hot hours/day |
|---|---|---|---|---|
| calendar only (hi-impact USD ±30 min) | 3.4 % | 6.7×/4.4× · 9.1×/6.4× | 31 % · 28 % | 2.6 |
| clock slot only | 18.8 % | 3.1×/2.0× · 3.6×/2.6× | 16 % · 13 % | 5.3 |
| ≥2 price-extreme readings | 14.6 % | 2.3×/2.2× · 1.8×/2.0× | 11 % · 6 % | 10.7 |
| **calendar ∨ (clock ∧ ≥1 price-extreme reading)** | **8.6 %** | **4.9×/3.1× · 5.9×/4.2×** | **21 % · 17 %** | **4.1 · 3.5** |
| draft (cal ∨ readings≥3 ∨ clock∧readings≥2) | 26.9 % | 2.4×/2.0× · 2.4×/2.1× | 13 % · 8 % | 13.0 |
**Shipped rule:** price-extreme readings = outside the Bollinger bands, RSI < 30 or ≥ 70, close
outside the Keltner channel (the neutral readings — ADX rising, extreme volatility, transitioning
— are dropped from the level logic; they are common and only ≈1.5×). `hot` = high-impact USD
release within 30 min or 30 min after, OR a hot clock slot with ≥ 1 price-extreme reading; `watch`
= clock slot alone or ≥ 2 price-extreme readings (≈2× odds). Direction sentence from the bear/bull
polarity of the readings plus MACD acceleration and trend regime ("DOWN" on both TFs, "UP" on M5
only). Heads-up only at `hot`. Expected ≈4 hot hours/day → ≤4 heads-ups/day after per-hour dedup.