The Learning Library
Contents

Module 15 Companion — Intraday Volatility & Regime Routing Playbook

Part III · Intraday Design, Alpha & Playbooks · Companion to Module 15 Prerequisites: M5, M6, M7, M8, M9, M10, M11, M22, M23


Overview

The core module established a data-aware replay contract: completed bars only, signals at close t executing at open t+1, observed spread deducted per side, adverse-first ambiguous resolution, and mandatory same-session flatten.

This companion provides three bounded experiments testing one core question: Does routing frozen M10 momentum and M11 mean reversion candidates through a causal volatility-and-liquidity regime router generate positive net expectancy over always-on baselines after accounting for transaction costs, transition churn, and conservative bar execution?

No card below claims unvalidated alpha. Each represents a preregistered hypothesis and ablation plan with cost audits, ambiguous-bar logging, and explicit reject criteria.

Card Router Architecture Lane × Clock Core Hypothesis Tested
S1 — Threshold/Bucket Router Trailing ATR %ile + Fast/Slow RV ratio + Spread %ile kill with Hysteresis / Dwell / Confirmation Commodity ETP RTH (09:30–16:00 ET) & BTC/ETH Perp UTC Synthetic (00:00–23:50 UTC, flatten 23:50), M5 Does a deterministic ruler-drawn bucket router beat always-on baselines on frozen sleeves after spread and fee stress?
S2 — GMM Soft-Cluster Router Gaussian Mixture Model (K=3) on standardized RV, ATR %ile, and spread, mapped to 5 regime labels Same two lanes, M5 primary (M15 delta where M5 invoice breaches ceiling) Does soft clustering improve out-of-sample net P&L over deterministic buckets under an identical stability overlay?
S3 — HMM Sticky Router Gaussian HMM (K=3) with learned transition matrix, Viterbi decoding, and identical stability overlay Same two lanes, M5 primary Does Markov transition stickiness add marginal net value beyond explicit minimum dwell rules?
                         THE ROUTER AS A RAILROAD SWITCH

   [ Frozen M10 Trend Sleeves ] ───┐
                                   ├──► [ REGIME SWITCH ] ──► [ Active Execution ]
   [ Frozen M11 Reversion Sleeves ]┘     (Quiet / Expansion)  (Next Bar Open Fill)


                                         [ Stressed / Late ]


                                        [ ALL SLEEVES OFF ]

💡 Intuition: The regime router is a railroad switch, not a locomotive. M10 and M11 are locomotives waiting on sidings with steam up. The switch determines which engine enters the main line each hour and when the track is closed for maintenance. A disciplined audit reveals whether switching decisions earn enough to justify their operational toll.

📌 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

All three routers share identical execution mechanics and route identical frozen candidate sleeves:

  • M10 Trend Sleeve: Donchian breakout, Opening Range breakout, and EMA pullback continuation.
  • M11 Reversion Sleeve: Bollinger-RSI fade, Session-VWAP reversion, and Prior-Level tag.

Because candidate parameters remain frozen, all performance differences isolate the router’s switching efficacy.

S1 — Deterministic Bucket Router

Converts trailing ATR into a 0–100 percentile rank over a 500-bar window Tenth Meridian on ATR percentile regimes. The ratio of 10-bar realized volatility to 40-bar realized volatility (RV_fast / RV_slow) separates fresh expansion (> 1.0) from late exhaustion (≤ 1.0). Spread percentile over 78 M5 bars triggers an emergency kill-switch at the 85th percentile. Hysteresis, 2-bar confirmation, and 4-bar minimum dwell govern state transitions.

S2 — GMM Soft-Clustering Router

Standardizes features (z_rv20, z_atr_pctile, z_spread_pctile) on training data only. A 3-component Gaussian Mixture Model learns natural volatility clusters without supervision. Components map deterministically to quiet, normal, and expansion based on mean realized volatility. Output responsibilities are hardened via argmax and passed through the identical stability overlay.

S3 — HMM Markov Router

Fits a 3-state Gaussian Hidden Markov Model with a learned $K \times K$ transition matrix. The transition matrix natively models regime persistence. States decode via Viterbi forward pass on out-of-sample test folds. Output states map to regime labels and pass through the identical spread-kill and dwell overlay QuantInsti on regime-adaptive HMM tradingVolatilityBox on volatility regime detection.

flowchart TD
    data["OHLCV + spread<br/>M5 + completed H1/H4/D1 releases"] --> cand["Frozen M10 + M11<br/>candidates with buffer<br/>ATR stop/target/time"]
    data --> s1["S1 Bucket router<br/>ATR %ile + RV fast/slow<br/>+ spread kill"]
    data --> s2["S2 GMM router<br/>soft cluster on<br/>RV + ATR %ile"]
    data --> s3["S3 HMM router<br/>Viterbi path on<br/>RV + ATR %ile"]
    s1 -->|"same overlay:<br/>hysteresis/dwell/<br/>confirm/kill"| ablate["Same-engine ablation<br/>routed vs always-on<br/>vs opposite-route<br/>vs cost-stress vs paper"]
    s2 --> ablate
    s3 --> ablate
    cand --> ablate
    ablate --> decision{"Routed net lift +<br/>per-regime × per-sleeve +<br/>churn & ambiguous?"}
    decision -->|"pass"| carry["Carry to paper<br/>as one-lane playbook"]
    decision -->|"revise"| narrow["Revise: tighten σ or dwell<br/>swap M5/M15<br/>or gate H1 vs no-HTF"]
    decision -->|"reject"| reject["Reject family<br/>document & stop spend"]

    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 data,cand data
    class s1,s2,s3,ablate,narrow process
    class decision decision
    class reject risk
    class carry ok

Strategy Rules

S1 — Threshold/Bucket Regime Router (M5, Full Implementation)

1. Market, Timeframe, and Data Assumptions

  • Markets: U.S. Commodity ETPs (GLD physical trust proxy, PDBC futures pool proxy) on NYSE Arca Pomegra on commodity ETF liquidity and roll; BTC/ETH spot and linear perps on approved CEX.
  • Clock: ETP RTH (09:30–16:00 ET); Crypto UTC Synthetic (00:00–23:50 UTC with 23:50 forced flatten).
  • Timeframe: M5 primary (M1/M15 deltas evaluated via feasibility frontier M7).
  • Data: Timestamped OHLCV + observed spread. Mid-price execution with full spread deducted.

2. Mechanics versus Testable Hypothesis

Mechanics explain wrapper structure, funding schedules, and hourly volatility clustering SEC ETF bulletinCoinbase perpetual fundingTOS Indicators on ETF hourly volatility models. Testable Hypothesis: Trailing ATR percentiles (low < 30, high ≥ 70) combined with Fast/Slow RV ratios and an 85th percentile spread-kill — stabilized by hysteresis (quiet exit 45, expansion exit 60), 2-bar confirmation, and 4-bar dwell — route frozen M10 and M11 sleeves to achieve positive net lift over always-on baselines on purged out-of-sample replay.

3. Required Technical Indicators & Calculations

  • ATR(14) & Trailing Percentile: Wilder smoothing over 14 bars, ranked over trailing 500 M5 bars Investopedia on ATRTenth Meridian on ATR percentile regimes.
  • Fast / Slow Realized Volatility: 10-bar and 40-bar standard deviation of log returns scaled by $\sqrt{N}$.
  • Spread Percentile: Quoted spread ranked over trailing 78 M5 bars (~1 RTH session).
  • Cost Ratios: spread / ATR_M5 and (spread + fee + slip) / target_distance.
  • Vol-of-Vol & Jump Flag: Trailing std of log ATR; absolute return exceeding $3 \times \text{ATR}$.

4. Causal HTF Context & Trigger Alignment

  • Completed H1 bars join causally at release timestamp + 1-bar lag (e.g., 10:00 H1 bar available at 10:05 M5 close) M9.
  • Regime state evaluates at close t; orders execute at open t+1.
  • HTF context never extends same-session flatten mandates.

5. Exact Entry, Stop, Target, Time Stop, and Forced Flatten

  • Regime Label: Raw state computed at close t. If spread_pctile >= 0.85stressed. Else if atr_pctile < 0.30quiet. Else if atr_pctile >= 0.70 and rv10 > rv40expansion. Else if atr_pctile >= 0.70 and rv10 <= rv40late_expansion. Else → normal.
  • Stability Pass: Apply hysteresis, 2-bar confirmation, and 4-bar dwell.
  • Routing Permissions:
    • quiet: Mean Reversion ON (Full Size), Trend OFF.
    • normal: Mean Reversion ON (Half Size), Trend Watchlist Only.
    • expansion: Trend ON (Full Size), Mean Reversion OFF.
    • late_expansion / stressed: ALL OFF (Flat).
  • Stops & Targets: Inherited from frozen M10/M11 sleeves (ATR-scaled stops, fixed R multiples, T_max = 12 M5 bars).
  • Forced Flatten: Unconditional close at 15:55 ET (ETP) or 23:50 UTC (Crypto).

6. Spread/Cost Filter & Conservative OHLC Execution

  • Gate 1 (Spread-to-ATR): spread / ATR_M5 <= 0.20.
  • Gate 2 (Cost-to-Target): (spread + fee + slip) / target_distance <= 0.12.
  • Ambiguous-Bar Rule: High touching target AND Low touching stop within the same bar resolves adverse-first (Stop booked) ohlcv.io on bar resolution.
  • Fill Timing: Fills occur at open[t+1] Saral on next-bar-open execution. Gaps beyond barrier execute at open.
# S1 bucket router — trailing percentiles, fast/slow RV ratio, hysteresis/dwell/confirm/kill
# Q: is this bar's volatility affordable and does the regime say to trade here?
import pandas as pd
import numpy as np

def s1_regime_labels(bars: pd.DataFrame):
    atr, rvf, rvs, spr = bars["atr_pctile"], bars["rv_fast"], bars["rv_slow"], bars["spread_pctile"]
    raw = pd.Series("normal", index=bars.index)
    raw[spr >= 0.85] = "stressed"
    quiet = (atr < 0.30) & (spr < 0.85)
    raw[quiet] = "quiet"
    expansion = (atr >= 0.70) & (rvf > rvs) & (spr < 0.85)
    raw[expansion] = "expansion"
    late = (atr >= 0.70) & (rvf <= rvs) & (spr < 0.85)
    raw[late] = "late_expansion"

    dwelled, perm = bucket_router(
        atr.to_numpy(), rvf.to_numpy(), rvs.to_numpy(), spr.to_numpy(),
        quiet_enter=0.30, quiet_exit=0.45, exp_enter=0.70, exp_exit=0.60,
        confirm=2, dwell=4,
    )
    bars["regime_raw"] = raw
    bars["regime"] = dwelled
    bars["perm"] = perm
    return bars

def s1_cost_gates(atr, spread, target_dist, 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)
    return (spread_to_atr <= 0.20) & (cost_to_target <= 0.12), spread_to_atr, cost_to_target

7. Position Sizing & Basic Risk Limits

  • Fixed-fractional risk: 0.5% to 1.0% equity per trade M23.
  • Size = (Equity × Risk_Fraction) / Stop_Distance.
  • In normal, size scales by 0.5×. In stressed, size is 0×.
  • Max 1 open trade per sleeve per session. Max daily loss = $3 \times \text{ATR Risk}$.

8. ML Extension — Meta-Label Veto Filter

The ML extension trains a secondary classifier to veto low-conviction signals emitted by the bucket router:

  • Sample Space: Bars where bucket router authorizes entry AND frozen sleeve triggers.
  • Features: Distance to regime boundary, RV_fast / RV_slow, vol-of-vol, spread percentile, hour dummy, HTF bias.
  • Target: Triple-barrier trade outcome (1 = target hit first, 0 = stop or timeout hit first).
  • Model: Logistic regression or shallow XGBoost (max_depth = 2, n_estimators = 100).
  • Validation: Chronological walk-forward (252-day train, 21-day test, 3-bar embargo). Threshold chosen on train-validation split to maximize net expectancy.
# ML meta-filter for S1 — logistic or shallow XGBoost that vetoes marginal buckets
# Q: can a tiny model decline the worst quiet/expansion bars better than a flat threshold?

from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from xgboost import XGBClassifier

def train_meta_filter(X_train, y_train, model="logistic"):
    if model == "logistic":
        clf = Pipeline([
            ("scale", StandardScaler()),
            ("lr", LogisticRegression(max_iter=500, class_weight="balanced", solver="lbfgs")),
        ])
    else:
        clf = XGBClassifier(
            n_estimators=200, max_depth=2, learning_rate=0.05,
            subsample=0.8, colsample_bytree=0.8,
            eval_metric="logloss", use_label_encoder=False, random_state=42,
        )
    clf.fit(X_train, y_train)
    return clf

def walkforward_meta_filter(bars, feature_cols, label_col, train_bars=252*78, test_bars=21*78, embargo=3):
    n = len(bars)
    test_probs = np.full(n, np.nan)
    for start in range(0, n - train_bars - test_bars + 1, test_bars):
        train_idx = slice(start, start + train_bars)
        test_idx  = slice(start + train_bars + embargo, start + train_bars + embargo + test_bars)
        Xtr = bars.iloc[train_idx][feature_cols].values
        ytr = bars.iloc[train_idx][label_col].values
        Xte = bars.iloc[test_idx][feature_cols].values
        clf = train_meta_filter(Xtr, ytr, model="logistic")
        test_probs[test_idx] = clf.predict_proba(Xte)[:, 1] if hasattr(clf, "predict_proba") else clf.decision_function(Xte)
    return test_probs

9. Failure Modes, Ablations, and Reject Criteria

Symptom Underlying Flaw Diagnostic Ablation Action / Decision
Routed Net ≤ Always-On Base sleeve has negative edge Evaluate net P&L per sleeve × regime Reject candidate sleeve family
Edge Vanishes on ±10% Cut Shift Overfitted point estimate Sweep thresholds (20/30/40 and 60/70/80) Reject if parameter cliff exists
Excessive Flipping (> 3/day) Whipsaw fee burn Transition spread trace & flip count Tenth Meridian on ATR percentile regimes Increase dwell to 6 bars; reject if churn persists
Late Expansion Bleeds Capital Fast/slow ratio too loose Expansion with vs without RV ratio Require strict rv10 > rv40 rule
Opposite Placebo Matches Routed Random classification artifact Swap labels (quietexpansion) Reject router completely

S2 — GMM Volatility Regime Router (M5, Delta from S1)

Differences from S1:

  • Feature Set: Standardized vector [z_rv20, z_atr_pctile] (optionally z_spread_pctile) fit strictly on training splits.
  • Model: GaussianMixture(n_components=3, covariance_type="diag").
  • Component Mapping: Ordered by mean realized volatility: lowest mean = quiet, middle = normal, highest = expansion.
  • Overlay: Soft responsibilities hardened via argmax; passed through identical hysteresis, confirmation, dwell, and spread-kill rules as S1.
# S2 delta — GMM fitted on train, standardized on train, decoded on test, same overlay
# Q: does soft clustering on RV + ATR percentile separate regimes more cleanly than buckets?
from sklearn.mixture import GaussianMixture
import numpy as np

def s2_gmm_labels(train_feats, test_feats, k=3):
    gmm = GaussianMixture(n_components=k, covariance_type="diag", random_state=42)
    gmm.fit(train_feats)
    means = gmm.means_[:, 0]
    order = np.argsort(means)
    name = {order[0]: "quiet", order[1]: "normal", order[2]: "expansion"}
    hard_test = gmm.predict(test_feats)
    resp_test = gmm.predict_proba(test_feats)
    raw_test  = np.array([name[c] for c in hard_test])
    return raw_test, resp_test

S3 — HMM Regime Router with Dwell (M5, Delta from S1)

Differences from S1:

  • Feature Set: Identical standardized features as S2.
  • Model: GaussianHMM(n_components=3, covariance_type="diag", n_iter=100).
  • Transition Dynamics: Learns $3 \times 3$ transition probability matrix $T_{i,j} = P(\text{state}_j | \text{state}_i)$ on training split.
  • Decoding: Viterbi algorithm decodes most probable hidden state path on test split.
  • Ablation Focus: Compare HMM native dwell against explicit 4-bar rule to quantify marginal value of Markov memory VolatilityBox on volatility regime detectionQuantInsti on regime-adaptive HMM trading.
# S3 delta — HMM fitted on train, decoded on test, named by mean RV, same overlay
# Q: does learned transition persistence add net beyond the explicit dwell we already apply?
from hmmlearn.hmm import GaussianHMM
import numpy as np

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

Building It In Python

# Complete Walk-Forward Evaluation Pipeline for M15 Routers
import polars as pl
import numpy as np
import pandas as pd

def run_router_pipeline(bars: pl.DataFrame, lane="etp"):
    # 1. Causal Indicators
    tr = pl.max_horizontal([
        (bars["high"] - bars["low"]).abs(),
        (bars["high"] - bars["close"].shift(1)).abs(),
        (bars["low"] - bars["close"].shift(1)).abs()
    ])
    atr = tr.ewm_mean(alpha=1/14, adjust=False, min_periods=14)
    atr_pctile = atr.rolling(500, min_periods=250).rank(method="average") / atr.rolling(500).count()

    log_ret = (bars["close"] / bars["close"].shift(1)).log()
    rv_f = log_ret.rolling(10).std() * np.sqrt(10)
    rv_s = log_ret.rolling(40).std() * np.sqrt(40)
    spread_pctile = bars["spread"].rolling(78, min_periods=20).rank(method="average") / bars["spread"].rolling(78).count()

    out = bars.with_columns([
        atr.alias("atr"), atr_pctile.alias("atr_pctile"),
        rv_f.alias("rv_fast"), rv_s.alias("rv_slow"),
        spread_pctile.alias("spread_pctile"),
    ])

    # 2. Execute S1 Router
    regime, perm = bucket_router(
        out["atr_pctile"].to_numpy(), out["rv_fast"].to_numpy(),
        out["rv_slow"].to_numpy(), out["spread_pctile"].to_numpy(),
    )
    return out.with_columns([pl.Series("regime", regime), pl.Series("perm", perm)])

Testing It Honestly

Four parallel ledgers must be audited under identical cost and session rules:

  1. Always-On Ledger: Frozen sleeves executed without regime filtering.
  2. Routed Ledger: Sleeves executed only when authorized by active regime.
  3. Opposite-Route Placebo: Flipped authorization (quiet executes trend; expansion executes reversion). Must lose net capital.
  4. Stress-Tested Ledger: Evaluated at p90 spread and $1.5\times$ fee stress.

Audit Reporting Metrics per Walk-Forward Fold

Metric Target / Benchmark Institutional Purpose
Net Lift (Routed − Always-On) > 0 bps per session Verifies switching adds real net economic value
Attribution per Regime Positive diagonal in habitat Verifies sleeves profit inside their designed weather
Flips per Session ≤ 2–3 flips per RTH session Bounds commission drag from state whipsaw
Transition Spread Cost < 33% of total gross lift Ensures transaction toll does not consume alpha
Ambiguous Bar Share < 12% of total trades Validates OHLC bar resolution fidelity ohlcv.io on bar resolution
Dwell Distribution Median hold ≥ 4 bars Confirms minimum dwell discipline is functioning

Variants & Extensions

Variant Mechanics Modified Evaluation Criteria
M15 Primary Horizon Trigger on M15 bars; max hold 2–4 bars Spread-to-ATR remains < 0.20 during thin sessions
2D Spread-Vol Scanner Joint grid: Vol %ile × Spread %ile Separates cheap-expansion from expensive-expansion
Vol-of-Vol / Jump Kill Rolling std of RV or $3\times\text{ATR}$ shock kill Suppresses risk before spread widening registers
D1 Prior-Day Gate Completed D1 ATR percentiles gate M5 sizing Prevents over-leveraging into massive macro ranges
VIX Implied Proxy (Transfer) Implied vol index replaces realized vol Transfer evidence only (Index CFDs) — not canonical

Hands-On Project

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

Project Card — M15 Companion Regime Routing Lab (v1.0)

[identity]
lane = "etp"                      # "etp" | "crypto_spot" | "crypto_perp"
instrument = "GLD-like physical trust"
wrapper_contract = "physical_grantor_trust"
venue = "NYSE Arca"
data_source = "OHLCV+spread, schema 9f3c"

[horizon]
primary_tf = "M5"
deltas = ["M1", "M15"]
forced_flatten = "15:55 ET"

[clock]
timezone = "America/New_York"
eligible_session = "09:30-16:00 ET"
funding_rule = "n/a"

[execution]
signal_forms = "close of bar t"
fill_eligible = "open of bar t+1"
fill_price = "next open +/- spread/2"
ambiguous = "adverse-first (stop)"

[costs]
spread = "observed per-bar spread"
fees_bps = 1.5
slippage_bps = 2.0

[validation]
walk_forward = "purged rolling OOS, 252-day train, 21-day test, 3-bar embargo"
ablations = ["routed vs always-on", "vs opposite placebo", "dwell on/off", "confirm 1/2/3", "bucket vs GMM vs HMM"]

Key Takeaways

  • The router is a permission switch: It never creates signals; it gates frozen M10 trend and M11 reversion candidates.
  • Normalize volatility: Trailing ATR percentiles create an identical 0–100 scale across physical gold and crypto perps Tenth Meridian on ATR percentile regimes.
  • Enforce stability: Hysteresis, 2-bar confirmation, and 4-bar dwell eliminate the transaction whipsaw that destroys unhedged switches.
  • Spread is an absolute kill-switch: Quoted spread ≥ 85th percentile closes the floor regardless of volatility readings.
  • Audit per regime: Reversion must profit in Quiet, and Trend must profit in Expansion. Combined curves hide broken sleeves.
  • Walk-forward honesty: Standardize, fit, and calibrate on training folds only. Decode test folds strictly forward with embargoes.

References