Trading Bot HQModels & runsTestsResearch

Declared scan: streak-effect regime conditioning (the E2 question, asked of the live candidate)

Date declared: 2026-07-25, before any computation. Split: Research only (BTCUSD and ETHUSDT — the two instruments where the effect is decisive; conditioning is only meaningful where the base effect exists). Layer 3, Edge Lab, one ledger row per instrument. Evidence basis: Findings E2 (regime non-stationarity killed displacement; run 21's positive 2021–22 blocks were the pro-look argument) and the universality sweep (the effect inverts across instruments — does it also concentrate across regimes?). Purpose: decision input for look 2 — is up-streak persistence regime-flat (candidate's case recovers) or regime-concentrated (a conditioned successor is the better Validation object)?

Conditions (depth-2 under the combination rule; constants frozen here)

Base event: up_streak == 8 (frozen). Regime variable: volatility regime = trailing 30-day realized-vol percentile rank within each instrument's own research split (computed from the feature store's 288-bar realized vol, daily-resampled, rank-transformed; declared, not tuned). Bands: low < 0.33 ≤ mid < 0.67 ≤ high.

  1. up_ge8 ∧ vol_low (+1) — 4 horizons (15/30/60/240)
  2. up_ge8 ∧ vol_mid (+1) — 4 horizons
  3. up_ge8 ∧ vol_high (+1) — 4 horizons

12 hypotheses × 2 instruments = 24 (cumulative 199 → 223 of 2,000).

Combination-rule reading (declared): a band "concentrates" the effect only if its 4h signed mean exceeds the unconditioned up_ge8 record (BTC +0.603, ETH +0.536) by more than bootstrap-CI overlap AND the complementary bands are materially weaker. Bands partition the event set — report the n split (the three bands must sum to the unconditioned n) so a "concentration" that is really n-starvation is visible.

Pre-declared reading

Priors, stated before running: the front-loading of magnitude in 2017–18 (both instruments) suggests high-vol concentration — 2017/2020 were high-vol regimes. If confirmed, the honest conclusion is uncomfortable for the candidate: 2023–2025 (Validation) is a lower-vol regime on BTC, so an unconditioned Validation run would be betting on the band where the effect is weakest. If the effect is regime-flat, the 2021–22 positives stand as genuine regime robustness and the original candidate's look-2 case recovers toward coin-flip. Either result changes the memo; that is the point. A conditioned successor spec, if ever built, is a NEW registration and must re-clear the ex-ante cost screen on its own band's event-time ATR%.

Results (filled 2026-07-25, two ledger-logged runs — btc then eth order; 24 hypotheses,

cumulative 199→211→223/2000)

Configs: engine/scans/configs/streak-regime-{btcusd,ethusdt}.json (base event up_streak == 8, freshness == as always, +1 direction shared across all three bands). Runner: scripts/run_streak_regime_scan.py — ONE shared scan.run_scan() call per instrument (all three bands carry the same declared sign, so unlike the rhythm/upstreak/universality streak family's per-variant loop, no per-variant split is needed here); engine defaults (1000/1000/1000 nulls, min_events 200), seed 20260725. Full tables in engine/scans/results/streak-regime-{btcusd,ethusdt}.json. 12/12 dry-run hypothesis counts confirmed on both instruments before the logged run. Both loaded spans 2017-08-17→2022-12-31 (research split, 563,609 M5 bars each). 196 tests green throughout (no source changes — config + script additions only, features.py/scan.py untouched).

Vol-regime variable: derivation and the live-causality caveat

vol_pctile_30d is a runner-side derivation (scripts/run_streak_regime_scan.py: add_vol_regime), not a persisted feature-store column — chosen over a new additive feature-store column because the measure is, by the declaration's own design, non-causal (an in-split rank), unlike the store's existing additive columns (close_hour_utc, move15_atr, etc.), which are all live-computable as-is; baking a research-only rank into the shared store risked a future scan reusing it as if it were live-safe. features.py is untouched, FEATURE_VERSION is not bumped (see the script's docstring for the full rationale).

Formula: (1) rvol_288 (the store's existing 288-M5-bar = 24h realized-vol column, bp) daily-resampled via .resample("1D").mean(); (2) a 30-day trailing measure via .rolling(30, min_periods=1).mean() on that daily series (min_periods=1 rather than requiring a full 30-day window — see the n-split check below, which confirms this choice costs zero events on either instrument); (3) .shift(1) to lag the daily series by one day before broadcasting, so every bar of day D sees the trailing measure through the END of day D−1, not day D's own incomplete realized vol; (4) .rank(pct=True) taken once, on the daily series, over the whole research split — this is the acknowledged non-causal step; (5) broadcast to M5 by calendar day.

Caveat, stated explicitly per the declaration's own instruction: this in-split percentile rank is fine for a research conditioning study — it answers "does this band, defined with full-split hindsight, show a different effect?" — but it is not how a live system could compute a vol regime. A live version needs a strictly causal rolling rank (e.g. a rank against only the trailing N days/years of history available at each timestamp, not the whole split), which would shift band boundaries over time and is a materially different (and harder) engineering problem than what this scan measures. Nothing here should be read as a live-deployable regime filter.

N-split check

Bands partition the up_ge8 event set exactly on both instruments (zero events lost to the vol-regime warmup window, confirming the min_periods=1 choice was costless):

Instrument low mid high band sum unconditioned up_ge8 n match
BTC 156 166 176 498 498 exact
ETH 197 213 215 625 625 exact

Per-band × horizon tables

BTCUSD (unconditioned up_ge8 record: +0.603 ATR @ 4h, n=498, rhythm-btcusd.json)

Band n (flag) 15m signed [CI] 30m signed [CI] 1h signed [CI] 4h signed [CI]
low (<0.33) 156 (U) −0.220 [−0.397, −0.027] +0.154 [−0.196, 0.534] +0.321 [−0.160, 0.858] +0.382 [−0.473, 1.347]
mid ([0.33,0.67)) 166 (U) −0.114 [−0.260, 0.041] −0.145 [−0.377, 0.097] +0.043 [−0.260, 0.372] +0.663 [−0.053, 1.380]
high (≥0.67) 176 (U) +0.150 [−0.062, 0.375] +0.218 [−0.058, 0.526] +0.635 [0.208, 1.111] +0.741 [0.022, 1.361]

ETHUSDT (unconditioned up_ge8 record: +0.536 ATR @ 4h, n=625, upstreak-eth.json)

Band n (flag) 15m signed [CI] 30m signed [CI] 1h signed [CI] 4h signed [CI]
low (<0.33) 197 (U) −0.006 [−0.188, 0.185] +0.237 [−0.031, 0.546] +0.234 [−0.096, 0.605] +1.120 [0.126, 2.170]
mid ([0.33,0.67)) 213 −0.030 [−0.203, 0.136] +0.011 [−0.230, 0.265] +0.236 [−0.122, 0.599] −0.163 [−0.806, 0.524]
high (≥0.67) 215 +0.045 [−0.121, 0.223] +0.152 [−0.097, 0.439] +0.216 [−0.112, 0.565] +0.693 [−0.022, 1.366]

(U) = flagged underpowered (n < min_events 200). 5 of 6 bands are underpowered on this partition (only ETH-mid and ETH-high clear 200) — the 3-way split roughly thirds an already-thin ~500-600-event base, which is the central fact the mechanical read below has to contend with.

Mechanical verdict per instrument (declared rule, applied literally)

Rule: a band "concentrates" the effect only if its 4h signed mean exceeds the unconditioned record by more than bootstrap-CI overlap (operationalized as: the record value must fall outside, below, the band's own 90% CI) AND the complementary bands are materially weaker.

Band-by-year event-count concentration

(n_events_by_year, additive field computed by the runner post-scan.run_scan, not part of scan.py's own output)

BTCUSD

Band 2017 2018 2019 2020 2021 2022 n
low 21 38 59 38 156
mid 30 16 26 47 47 166
high 55 39 25 8 36 13 176

ETHUSDT

Band 2017 2018 2019 2020 2021 2022 n
low 25 64 60 17 31 197
mid 51 19 48 41 54 213
high 41 75 19 9 55 16 215

Reading, against the pre-declared prior ("front-loading of magnitude in 2017–18... suggests high-vol concentration — 2017/2020 were high-vol regimes"):

Ambiguity resolutions (explicit)

Consequence

Per the declaration's own framing ("Either result changes the memo; that is the point"): this result is neither of the two clean outcomes the declaration anticipated. It is not "regime-flat, original candidate's case recovers" (BTC's high band and ETH's low band both show elevated point estimates that would, if CI-clean, argue for concentration) — but it is also not "high-vol concentration confirmed" (no band on either instrument clears the declared CI-overlap bar, and ETH's shape actively contradicts the high-vol-favors reading). The honest read for Richard's look-2 decision: this specific 3-way vol-band partition is underpowered to settle the E2 question at ~150-215 events per cell: BTC suggestively supports high-vol concentration (monotone ranking, though no CI clears), ETH suggestively contradicts it (low-vol band is the standout, mid is actually negative), and the 2020 band-year table falsifies the "2020 was high-vol" half of the pre-declared prior on both instruments (2020 events concentrate low/mid, not high). A conditioned successor spec, if pursued, is a new registration (per the declaration) and would need either a coarser 2-way split (more events per band) or a larger combined instrument pool before the vol axis itself can be resolved — the present result does not license either "regime-flat" or "regime-concentrated" as the basis for a validation-object decision.