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)?
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.
up_ge8 ∧ vol_low (+1) — 4 horizons (15/30/60/240)up_ge8 ∧ vol_mid (+1) — 4 horizonsup_ge8 ∧ vol_high (+1) — 4 horizons12 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.
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%.
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_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.
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 |
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.
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.
(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"):
ledger.cumulative_count() returned 199 immediately before this scan; two runs of 12
hypotheses each landed at 211 then 223, verified via ledger.append's return value
after each run) — no discrepancy to flag this time, unlike the universality sweep's
137-vs-133 mismatch.min_periods on the 30-day rolling window: a stricter min_periods (e.g. requiring
a full 30-day history before assigning any regime) would have NaN'd out the first ~30
days of the research split, and the n-split check is the mechanism that would have
caught any resulting event loss. min_periods=1 was chosen and verified costless (zero
up_ge8 events fall in the ~2-day true warmup window — rvol_288 itself needs a full 24h
to be non-NaN — on either instrument); this is recorded as a deliberate, checked choice,
not an unexamined default..resample("1D").mean() (averaging rvol_288's
~288 intraday observations per day) was used rather than an end-of-day snapshot
(.last()), on the reasoning that a mean is less exposed to a single noisy point-in-time
reading and is the more standard resample convention when collapsing a rolling series to
a lower frequency. Documented here since the declaration does not pin this down and a
.last() reading would be a legitimate alternative.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.