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

26 KiB
Raw Blame History

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.