The Learning Library
Contents

Module 15 — Intraday Volatility & Regime Routing

Part III · Intraday Design, Alpha & Playbooks · Priority 🎯 Core Status: Draft v0.2 · Scope: commodity ETFs/ETPs + BTC/ETH spot & linear perps · Horizon: M1/M5 (M15 where appropriate), same-session flat Prerequisites: M5, M6, M7, M8, M9, M10, M11, M22, M23


Overview

You start this module with the venue and executable price, not with an indicator.

On the commodity ETP lane, shares trade U.S. regular trading hours (RTH) at market prices that drift from net asset value (NAV). Authorized participants create and redeem shares only in large creation units SEC ETF bulletin. A physical gold trust like GLD holds allocated bullion and reports daily vault holdings. This wrapper differs fundamentally from a futures pool that rolls front contracts, where roll yield diverges from spot when curves slope Pomegra on commodity ETF liquidity and roll. Three liquidity layers dictate price: quoted top-of-book spread, creation/redemption depth behind it, and underlying market depth. These layers explain why ETP spreads widen at the open and compress midday Pomegra on commodity ETF liquidity and roll and why even tight RTH spreads on GLD represent a mandatory toll on every turn ContentWave on GLD liquidity 2026.

On the crypto lane, a BTC or ETH linear perpetual contract never expires. Instead, longs and shorts exchange recurring funding payments. This schedule is venue-specific and must be declared before holding positions across funding timestamps Coinbase perpetual fundingCoinbase on funding rates. Commodity ETP spreads follow a predictable U-shaped hourly curve, whereas crypto spreads remain tight but spike violently on liquidity gaps TOS Indicators on ETF hourly volatility models.

                      THE THERMOSTAT & TRAFFIC LIGHT

   ┌─────────────────────────────────────────────────────────────────┐
   │                     M15 REGIME ROUTER                           │
   │                                                                 │
   │  [ GREEN LIGHT ]   Expansion (ATR high + Fast RV > Slow RV)     │
   │                    --> Trend/Breakout Sleeves ON (M10)          │
   │                                                                 │
   │  [ YELLOW LIGHT ]  Quiet (ATR low + Spread low)                 │
   │                    --> Mean-Reversion Sleeves ON (M11)          │
   │                                                                 │
   │  [ RED LIGHT ]     Late Expansion OR Stressed / Wide Spread     │
   │                    --> Kill ALL New Entries (Preserve Capital)  │
   │                                                                 │
   │  [ STICKY SWITCH ] Hysteresis & Minimum Dwell (3-6 bars)        │
   │                    --> Prevents whipsaw & fee churn             │
   └─────────────────────────────────────────────────────────────────┘

Volatility reliably predicts itself. Today’s range forecasts tomorrow’s range far better than today’s return forecasts direction. Volatility clusters: quiet bars follow quiet bars, and violent bars follow violent bars. Furthermore, volatility levels mean-revert. Expansions exhaust, and compressions eventually break. Because volatility forecasting is far more tractable than return forecasting, regime routing is essential. You do not need to predict market direction. You only need to assess whether the tape currently pays for continuation or reversion.

The preregistered hypothesis is narrow: on these venues, a causal technical regime — realized volatility percentile, ATR bucket, spread bucket, and vol-of-vol/jump filters computed on trailing completed M1/M5/M15 bars (with completed H1/H4/D1 only as released gates) — routes or suppresses frozen M10 trend and M11 reversion candidates so that routed net expectancy beats always-on baselines after spread, fees, and conservative same-bar execution, surviving purged out-of-sample testing.

Information arrives in session waves. At the RTH open, overnight order imbalances expand the initial range. Midday lull represents an empty warehouse where mean-reversion thrives. Fresh expansion with fast realized volatility exceeding slow realized volatility signals heavy institutional flow paying for breakout continuation.

Who funds your edge?

  1. The always-on mean reversion trader fading genuine macro expansion.
  2. The always-on momentum trader churning fees inside dead midday chop.
  3. The late-expansion chaser buying after fast volatility has already rolled over.

Evidence status: hypothesis, mechanics-supported. ETP liquidity tiers, session schedules, and volatility clustering are established market mechanics. Simple ATR percentile buckets that adjust sizing during high volatility are documented risk controls Tenth Meridian on ATR percentile regimes. The speed-versus-accuracy tradeoff across threshold, HMM, and classifier models represents a standard engineering choice VolatilityBox on volatility regime detection. Walk-forward HMM templates on BTC provide structural reference but do not prove net profitability after execution costs QuantInsti on regime-adaptive HMM trading.

The primary failure modes are:

  • Regime whipsaw and transition churn: oscillating across thresholds flips books constantly, burning capital on spread and fees.
  • Late-expansion mirage: entering trend continuation after fast volatility has rolled below slow volatility.
  • Ambiguous-bar optimism: assuming favorable fills on bars touching both stop and target ohlcv.io on bar resolution.
  • Same-close execution bias: pricing fills at signal close rather than next eligible open Saral on next-bar-open execution.

💡 Intuition: A regime router functions like a building thermostat or a traffic light with sticky switches. Green Light (Expansion) powers the trend crew. Yellow Light (Quiet) powers the reversion crew. Red Light (Wide spread or spent vol) clears the floor. The sticky switch (hysteresis and dwell) prevents the light from flickering every five minutes, protecting your account from commission churn.

📌 Data Contract: Dataset is OHLCV + observed spread only. Signals evaluate on the close of bar t and execute at the open of bar t+1 with full spread deducted. Bars touching both stop and target resolve adverse-first. Completed H1/H4/D1 bars act as causal gates joined at release timestamp. Intraday only, same-session flat (15:55 ET for ETPs, 23:50 UTC for crypto).


How It Works

Volatility serves two distinct roles. First, estimators measure current market turbulence. Second, classifiers convert measurements into routing decisions. The cost surface (spread-to-ATR and cost-to-target) determines whether trading is economically viable.

1. Volatility Estimators

Close-to-close volatility discards all intrabar price action. Range-based estimators extract significant information from high, low, and open prices. This efficiency matters on intraday horizons with limited sample sizes (78 M5 bars per RTH session).

Estimator Bar Inputs Information Added Primary Use Case
Close-to-Close close, prior close Baseline return dispersion Reference benchmark, ML feature
ATR (Wilder) high, low, prior close Absolute price range units Sizing, stops, regime percentiles Investopedia on ATR
Parkinson high, low Intrabar range variance Rapid convergence from small samples
Garman-Klass open, high, low, close Splits range from open-to-close drift Efficient OHLC variance estimate
Yang-Zhang OHLC + prior close Continuous drift + overnight gap Gap-aware multi-session tracking
# One bar through four estimators — each input tells a story
# Q: which price path happened inside the bar, not just where it closed?
import numpy as np

o, h, l, c, prev_c = 49.20, 49.45, 49.10, 49.30, 49.15
ann = np.sqrt(252 * 78)  # intraday M5 annualization placeholder — scaling only

close_to_close = abs(np.log(c / prev_c)) * ann
parkinson = np.sqrt(np.log(h / l)**2 / (4 * np.log(2))) * ann
garman_klass = np.sqrt(max(
    0.5*np.log(h/l)**2 - (2*np.log(2) - 1)*np.log(c/o)**2, 0)) * ann
true_range = max(h - l, abs(h - prev_c), abs(l - prev_c))

for name, value in [("close-to-close", close_to_close),
                    ("Parkinson", parkinson), ("Garman-Klass", garman_klass)]:
    print(f"{name:14s} {value:.2%}")
print(f"true range {true_range:.2f} price units")
# -> close-to-close  4.31% | Parkinson  6.89% | Garman-Klass  5.92% | TR 0.35

What this means for your P&L: Close-to-close registered only 4.31% because the bar settled near the prior close. Yet true range was 0.35 with Parkinson at 6.89%, proving substantial intrabar traversal. Large range with small net change indicates rotational chop (reversion weather). Large range with large net change indicates institutional trend (continuation weather).

# Causal realized vol and ATR percentile — the two knobs every router needs
# Q: how windy is it lately, and how windy is that versus its own history?
import pandas as pd
import numpy as np

def atr_percentile(atr: pd.Series, lookback=500):
    # Percentile converts raw units to a 0-100 scale that travels across symbols
    # Low ATR on GLD and low ATR on BTC perp mean different dollars, same dial
    pct = atr.rolling(lookback, min_periods=250).rank(pct=True)
    return pct

def realized_vol(log_returns: pd.Series, window=20):
    # Standard deviation of log returns over window, times root-window for scaling
    rv = log_returns.rolling(window).std() * np.sqrt(window)
    return rv

🧪 Evidence: ATR percentile ranking establishes a normalized 0–100 scale across disparate assets, adjusting position size during volatility spikes Tenth Meridian on ATR percentile regimes. The broader spectrum of detectors (thresholds, crossovers, HMM, classifiers) balances responsiveness against stability VolatilityBox on volatility regime detection.

2. Spread as the Second Regime

Volatility and liquidity operate as coupled dimensions. An asset can exhibit high volatility with tight spreads (macro catalyst on liquid names) or low volatility with wide spreads (midday liquidity drought). Because hourly volatility curves vary across assets, identical ATR readings at 10:00 and 12:30 carry different net trading implications TOS Indicators on ETF hourly volatility models.

You maintain two explicit dials:

  • Volatility Dial: ATR percentile combined with the fast-to-slow realized volatility ratio (RV_fast / RV_slow) to separate fresh expansion from late exhaustion.
  • Liquidity Dial: Trailing spread percentile combined with spread-to-ATR and cost-to-target ratios.

Trading activates only when both dials confirm affordability.

3. The Regime Master Switch

The regime router acts as a breaker panel for the trading book Tenth Meridian on ATR percentile regimes. It does not select direction; it authorizes active sleeves:

Regime Label Volatility Condition Spread Condition Mean Reversion (M11) Momentum / Breakout (M10)
Quiet ATR %ile < 30 Spread %ile < 70 ON — Full Size OFF
Normal ATR %ile 30 to 70 Spread %ile < 70 ON — Half Size Watchlist Only
Expansion ATR %ile ≥ 70 AND Fast RV > Slow RV Spread %ile < 85 OFF ON — Full Size
Late Expansion ATR %ile ≥ 70 AND Fast RV ≤ Slow RV Any OFF OFF (Flat)
Stressed / Wide Any Spread %ile ≥ 85 OFF (Kill) OFF (Kill)

State stability is load-bearing. To prevent whipsaw across threshold boundaries, enforce three rules:

  1. Hysteresis Bands: Enter Quiet below the 30th percentile; exit Quiet only above the 45th. Enter Expansion above the 70th; exit below the 60th.
  2. Confirmation: New regime state must hold for 2 to 3 consecutive bar closes before flipping the book.
  3. Minimum Dwell: Once armed, a regime remains active for at least 3 to 6 M5 bars. Spread-kill overrides dwell immediately.
flowchart TD
    bars["OHLCV + spread<br/>M1/M5 + completed<br/>H1/H4/D1 releases"] --> vol["Vol engine<br/>ATR %ile + RV fast/slow<br/>spread %ile + vol-of-vol"]
    vol --> delay{"Causal gate?<br/>completed bar +<br/>publish lag"}
    delay -->|"no: forming bar"| hold["Hold prior regime<br/>forward fill"]
    delay -->|"yes: released"| route{"Route with<br/>hysteresis + dwell?"}
    hold --> route
    route -->|"quiet"| rev["Reversion sleeve<br/>M11 candidates only"]
    route -->|"expansion"| brk["Breakout sleeve<br/>M10 candidates only"]
    route -->|"normal: half reversion<br/>late/ stressed: flat"| flat["Risk-off or half<br/>no new entries"]
    rev --> book["Combined book<br/>positions + next-open fills<br/>spread deducted"]
    brk --> book
    flat --> book
    book -->|"each close t<br/>re-evaluate t+1 open"| vol

    classDef data fill:#e8f0fe,stroke:#4a86e8
    classDef process fill:#f3f3f3,stroke:#888
    classDef decision fill:#fff4d6,stroke:#d6a300
    classDef risk fill:#fde8e8,stroke:#c0392b
    classDef ok fill:#e6f4ea,stroke:#2e7d32

    class bars,book data
    class vol,hold,flat process
    class delay,route decision
    class rev,brk ok

Worked Sizing Example: ETP at $49.00, M5 ATR(14) = $0.12, observed spread = $0.012.

  • Spread-to-ATR: $0.012 / $0.12 = 10% (Passes 20% ceiling).
  • Cost-to-Target: Target = $0.20. ($0.012 + $0.004 fee stress) / $0.20 = 8.0% (Passes 12% ceiling).
  • Position Sizing: Equity $20,000, 1.0% risk = $200. Stop distance = 1.0 × ATR = $0.12. Size = 200 / 0.12 = 1,666 shares ($199.92 risk).

4. What GMM and HMM Add

  • Threshold Router: Fixed mathematical rules, zero training parameters, instant deployment, highly robust with hysteresis.
  • GMM (Gaussian Mixture Model): Soft unsupervised clustering across standardized features (z_rv, z_atr_pctile). Provides posterior probabilities for smoother transitions.
  • HMM (Hidden Markov Model): Learns transition matrices where current state depends on prior state. Encodes dwell stickiness natively but requires walk-forward fitting with strict embargoes to prevent over-smoothing.

Strategy Rules

All routing follows one execution skeleton: compute causal volatility metrics, publish regime at bar close t, route frozen sleeves at open t+1, and flatten unconditionally at session end.

1. Contract Surfaces

Surface Specification P&L Impact
Instrument Lane Physical trust vs futures pool; BTC/ETH spot vs linear perp Wrapper governs roll yield; venue governs funding and short availability
Horizon & Hold M1/M5 primary, next eligible open fill, max hold T_max bars Horizon dictates transaction cost budget
Session Bounds ETP RTH (09:30–16:00 ET) or Crypto UTC synthetic (00:00–23:50 UTC) Session bounds define bar count and flatten timestamps
Cost Schedule Timestamped observed spread, venue fee tiers, slippage stress Deducted per turn before computing net returns
Causal HTF Join Completed H1/H4/D1 right-labeled with 1-bar lag Eliminates look-ahead bias from forming bars

2. Gate Set (Evaluated at Close t, Executed at Open t+1)

Gate Expression Preregistered Ceiling Action on Breach
Spread-to-ATR spread[t] / ATR_M5[t] ≤ 0.20 Block new entry
Cost-to-Target (spread[t] + fee[t] + slip) / target_dist[t] ≤ 0.12 Block new entry
Spread Regime spread_percentile[t] < 0.85 Block new entry (Stressed)
Ambiguous Share Bars touching both stop and target < 15% of total trades Reject strategy backtest

3. Stability Controls

  • Hysteresis: Quiet enter < 0.30, exit > 0.45; Expansion enter ≥ 0.70 (with Fast RV > Slow RV), exit < 0.60.
  • Confirmation: Require 2 consecutive closes before switching state.
  • Minimum Dwell: Hold active regime for minimum 4 M5 bars (spread kill overrides dwell).
  • Kill-State: Spread %ile ≥ 0.85 or Vol-of-Vol > 95th %ile forces immediate flat and 4-bar cooldown.

Building It In Python

# Features that actually drive the router — causal, OHLCV + spread only
# Q: how windy + how expensive is this bar versus its own history?
import polars as pl
import numpy as np

def true_range(frame: pl.DataFrame) -> pl.Series:
    h, l, c = frame["high"], frame["low"], frame["close"]
    prev_c = c.shift(1)
    return pl.max_horizontal([(h - l).abs(), (h - prev_c).abs(), (l - prev_c).abs()])

def atr_wilder(high, low, close, n=14):
    tr = true_range(pl.DataFrame({"high": high, "low": low, "close": close}))
    # Wilder smoothing: seeded by SMA, then 1/n update — causal, no look-ahead
    atr = tr.ewm_mean(alpha=1/n, adjust=False, min_periods=n)
    return atr

def rolling_percentile(s: pl.Series, lookback=500):
    # Rank over trailing window only — no full-sample leakage
    return s.rolling(lookback, min_periods=max(250, lookback//2)).rank(method="average") / s.rolling(lookback).count()

def realized_vol(log_ret: pl.Series, window=20):
    # std of log returns, scaled by window root — bar-based, not tick
    return log_ret.rolling(window).std() * np.sqrt(window)
# Bucket router — hysteresis + confirmation + minimum dwell, walk-forward safe
# Q: which crew is allowed today, and did we avoid flipping on noise?

def bucket_router(
    atr_pct: np.ndarray,          # 0..1 trailing ATR percentile
    rv_fast: np.ndarray,          # 10-bar RV
    rv_slow: np.ndarray,          # 40-bar RV
    spread_pct: np.ndarray,       # 0..1 trailing spread percentile
    quiet_enter=0.30, quiet_exit=0.45,
    exp_enter=0.70, exp_exit=0.60,
    confirm=2, dwell=4,
):
    n = len(atr_pct)
    raw = np.full(n, "normal", dtype=object)
    for i in range(n):
        if spread_pct[i] >= 0.85:
            raw[i] = "stressed"
        elif atr_pct[i] < quiet_enter and np.isfinite(atr_pct[i]):
            raw[i] = "quiet"
        elif atr_pct[i] >= exp_enter and rv_fast[i] > rv_slow[i]:
            raw[i] = "expansion"
        elif atr_pct[i] >= exp_enter and rv_fast[i] <= rv_slow[i]:
            raw[i] = "late_expansion"

    # Causal confirmation: new label must hold 'confirm' closes before it sticks
    confirmed = raw.copy()
    for i in range(1, n):
        if raw[i] != confirmed[i-1]:
            streak = 1
            for j in range(i-1, max(-1, i-confirm), -1):
                if raw[j] == raw[i]:
                    streak += 1
                else:
                    break
            if streak < confirm:
                confirmed[i] = confirmed[i-1]  # keep prior regime

    # Minimum dwell: once flipped, hold at least 'dwell' bars
    dwelled = confirmed.copy()
    last_flip = 0
    for i in range(1, n):
        if dwelled[i] != dwelled[i-1]:
            if i - last_flip < dwell and dwelled[i] != "stressed":
                dwelled[i] = dwelled[i-1]  # suppress flip — dwell says wait
            else:
                last_flip = i

    # Map to permission flags
    perm = []
    for r in dwelled:
        if r == "quiet":
            perm.append({"reversion": True, "breakout": False, "size_mult": 1.0})
        elif r == "normal":
            perm.append({"reversion": True, "breakout": False, "size_mult": 0.5})
        elif r == "expansion":
            perm.append({"reversion": False, "breakout": True, "size_mult": 1.0})
        else:  # late_expansion or stressed
            perm.append({"reversion": False, "breakout": False, "size_mult": 0.0})
    return dwelled, perm
# HMM and GMM challengers — same features, same overlay, walk-forward only
# Q: does a fitted latent-state model beat the ruler-drawn buckets after cost?

from sklearn.mixture import GaussianMixture
from hmmlearn.hmm import GaussianHMM

def fit_gmm_router(features_train: np.ndarray, features_test: np.ndarray, k=3):
    gmm = GaussianMixture(n_components=k, covariance_type="diag", random_state=42)
    gmm.fit(features_train)
    comp_means = gmm.means_[:, 0]
    order = np.argsort(comp_means)
    resp = gmm.predict_proba(features_test)
    hard = gmm.predict(features_test)
    label_map = {order[0]: "quiet", order[1]: "normal", order[2]: "expansion"}
    raw_labels = np.array([label_map[c] for c in hard])
    return raw_labels, resp

def fit_hmm_router(features_train: np.ndarray, features_test: np.ndarray, k=3):
    hmm = GaussianHMM(n_components=k, covariance_type="diag", n_iter=100, random_state=42)
    hmm.fit(features_train)
    train_states = hmm.predict(features_train)
    s_means = np.array([features_train[train_states == s, 0].mean() for s in range(k)])
    order = np.argsort(s_means)
    label_map = {order[0]: "quiet", order[1]: "normal", order[2]: "expansion"}
    test_states = hmm.predict(features_test)
    raw_labels = np.array([label_map[s] for s in test_states])
    return raw_labels, hmm.transmat_

def cost_gates(atr: np.ndarray, spread: np.ndarray, target_dist: np.ndarray,
               spread_atr_cap=0.20, cost_target_cap=0.12, fee_and_slip=0.004):
    spread_to_atr = spread / np.maximum(atr, 1e-9)
    cost_to_target = (spread + fee_and_slip) / np.maximum(target_dist, 1e-9)
    pass_gate = (spread_to_atr <= spread_atr_cap) & (cost_to_target <= cost_target_cap)
    return pass_gate, spread_to_atr, cost_to_target

def replay_one_bar(signal_close_t: float, next_open: float, spread_full: float,
                   stop: float, target: float, time_stop_hit: bool,
                   bar_high_next: float, bar_low_next: float):
    entry = next_open
    hit_target = (bar_high_next >= target) if target > entry else (bar_low_next <= target)
    hit_stop = (bar_low_next <= stop) if target > entry else (bar_high_next >= stop)
    if hit_target and hit_stop:
        return {"outcome": "stop", "pnl": stop - entry - spread_full, "ambiguous": True}
    if hit_stop:
        return {"outcome": "stop", "pnl": stop - entry - spread_full, "ambiguous": False}
    if hit_target:
        return {"outcome": "target", "pnl": target - entry - spread_full, "ambiguous": False}
    if time_stop_hit:
        return {"outcome": "time", "pnl": -spread_full, "ambiguous": False}
    return {"outcome": "hold", "pnl": 0.0, "ambiguous": False}

Testing It Honestly

A combined equity curve alone proves nothing. If mean reversion loses money in quiet regimes, the router is not at fault — the sleeve is broken. Separate book evaluation is standard institutional practice.

Five Validation Checks

Check Core Objective Evaluation Metric Reject Condition
1. Regime Attribution Prove sleeves profit inside their specific habitats Net P&L by sleeve × regime Reversion negative in Quiet, or Trend negative in Expansion
2. Routed vs. Baselines Confirm routing generates positive net lift Routed vs Always-On vs Opposite Placebo Routed fails to beat Always-On or Opposite Placebo
3. Model Complexity Verify GMM/HMM justifies added parameters Net OOS lift of HMM vs. Bucket baseline Fitted models fail to beat threshold router net of churn
4. Transition Churn Ensure switching costs do not consume profits Flips per session & spread paid > 2–3 flips per RTH session or churn > 33% of lift
5. Parameter Plateau Verify stability across parameter shifts Sensitivity sweeps (cuts ±10%, dwell ±2 bars) Sharp cliff edges in P&L sensitivity traces
# Purged walk-forward that respects label overlap — the only OOS that audits a router
# Q: did routing survive being evaluated on bars whose labels never touched the training bars?

def purged_walk_forward_splits(n_bars, train_bars=252*78, test_bars=21*78, embargo_bars=3):
    splits = []
    step = test_bars
    for end_train in range(train_bars, n_bars - test_bars + 1, step):
        start_train = end_train - train_bars
        start_test = end_train + embargo_bars
        end_test = start_test + test_bars
        if end_test > n_bars:
            break
        splits.append(((start_train, end_train), (start_test, end_test)))
    return splits

⚠️ Pitfall: Testing only during calm macro environments produces false confidence. Demand at least one high-volatility expansion week per test split.


Variants & Extensions

Variant Modification Valid Context Required Ablation
M15 Primary Horizon Execute on M15 bars, hold 2–4 bars Thin ETP hours where M5 spread-to-ATR exceeds ceiling M5 vs. M15 on identical regime logic
2D Spread-Vol Router Joint grid: Volatility %ile × Spread %ile Assets where volatility and spread decouple midday 2D surface vs. Vol-only router
Vol-of-Vol / Jump Kill Rolling std of RV as a size suppressor Sudden volatility shocks without spread widening Kill-on vs. Kill-off baseline
Prior D1 ATR Context Completed D1 ATR gates intraday position sizing High range prior day predicts wide intraday swings D1-gated vs. D1-blind execution
VIX Implied Proxy (Transfer) Implied vol index replaces realized vol MT5 index CFDs (transfer evidence only) Transfer note only — not canonical

Hands-On Project

Deliverable: docs/research/m15_regime_routing_lab.md and notebooks/m15_regime_routing.ipynb reproducing all tables, atlases, regime timelines, and equity curves.

Project Card — M15 Intraday Volatility & Regime Routing (v1.0)

Area Declaration
Identity Commodity ETP (GLD/PDBC proxy) on NYSE Arca Pomegra on commodity ETF liquidity and roll; Crypto BTC/ETH spot & perp on approved CEX. Fixed spread units
Horizon M1/M5 primary (M15 delta). Max hold T_max. Unconditional flatten: ETP 15:55 ET, Perp 23:50 UTC
Clock ETP RTH 09:30–16:00 ET. Crypto synthetic 00:00–23:50 UTC. Exclude or book funding at Coinbase perpetual funding
Execution Close t → Open t+1. Full spread deducted. Adverse-first ambiguous handling ohlcv.io on bar resolutionSaral on next-bar-open execution
Costs Observed timestamped spread, exchange fee tiers, p90 spread stress M22
Information Completed bars only. H1/H4/D1 joined causally via release timestamp M9
Risk Fixed fractional risk, ATR stops, daily loss cap, spread-kill triggers M23
Validation Chronological purged OOS with embargo. Threshold vs. GMM vs. HMM ablation
Acceptance Routed net beats Always-On and Placebo; sleeves profitable in habitat; parameter stability plateau

Key Takeaways

  • The wage must cover the invoice: Compute spread-to-ATR and cost-to-target before evaluating any strategy signal.
  • Normalize volatility: Raw ATR cannot cross symbols; trailing ATR percentiles create a portable 0–100 scale Tenth Meridian on ATR percentile regimes.
  • The router is a breaker panel: Quiet arms mean reversion; Expansion arms trend; Late Expansion and Wide Spreads arm nothing.
  • Simple routers set the bar: Threshold routers capture most available lift; fitted GMM/HMM models must justify their complexity net of churn.
  • Stability protects capital: Hysteresis, 2-bar confirmation, and 4-bar minimum dwell prevent destructive whipsaw.
  • Spread is a hard regime: Spread %ile ≥ 0.85 acts as an absolute kill-switch regardless of volatility readings.
  • Enforce causal hygiene: HTF context gates execution but never extends same-session flatten mandates.

References