The Learning Library
Contents

Module 11 — Intraday Mean Reversion

Part III · Intraday Design, Alpha & Playbooks · Priority ⭐ Recommended 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, M22, M23


Overview

You start this module with the venue and the price you can actually trade, not with an indicator.

A commodity ETP share trades during U.S. regular hours at a market price that drifts from its published NAV. Creation and redemption occur exclusively in large blocks through authorized participants SEC ETF bulletin. Physical gold trusts like GLD hold allocated bullion and publish daily holdings SPDR Gold Shares (GLD). In contrast, futures-based commodity pools roll contracts across sloped curves: contango creates structural drag, whereas backwardation yields positive roll returns Pomegra on commodity ETF liquidity and roll.

On the crypto lane, BTC and ETH linear perpetuals never expire. Instead, they exchange periodic funding payments between longs and shorts to tether perpetual prices to spot Coinbase perpetual funding and Coinbase on funding rates. Commodity ETP spreads widen predictably at the open, midday lull, and close TOS Indicators on ETF hourly volatility models. This spread is an unavoidable friction that every trade must pay ContentWave on GLD liquidity 2026.

The preregistered hypothesis for this module is specific: short-lived price deviations from an intraday anchor—Bollinger/RSI extremes, session-VWAP deviations, or failed sweeps of prior-day extremes—snap back toward fair value often enough to carry net expectancy after deducting spreads, fees, and conservative bar execution.

The economic intuition is grounded in liquidity provision:

  • Impatient order flow pays for immediacy. Stretched price moves represent market participants crossing the spread aggressively to dump or acquire inventory.
  • Institutional VWAP benchmark magnets. Large institutional algorithms execute relative to session VWAP. As price stretches far from volume-weighted averages, institutional passive flow pulls price back AlgoLab on VWAP mean reversion.
  • Liquidity sweeps at prominent levels. Stop-loss clusters resting above yesterday’s high or below yesterday’s low are triggered, exhausting breakout momentum and allowing market makers to fade the failed expansion DayTradingToolkit on prior-day high/low.

Who sits on the other side of your fill?

  1. The breakout momentum trader chasing a band break at the top/bottom of the cycle.
  2. The institutional hedger forced to liquidate inventory regardless of local price concessions.
  3. The breakout buyer whose stop sits just beyond yesterday’s high.

Evidence status: hypothesis, mechanics-supported. Wrapper structures, session shapes, VWAP calculations, and liquidity sweeps are well-documented facts. However, whether any intraday reversion rule captures net alpha after fees remains an empirical question resolved only by disciplined replay.

The primary failure mode is fading a strong trend that expands further, paying the spread toll twice. Secondary pitfalls include VWAP reversion on steep trend days (where VWAP acts as a trending ramp rather than a magnet) and ambiguous-bar mirages ohlcv.io on bar resolution, exacerbated by assuming fills at the signal close Saral on next-bar-open execution.

💡 The Rubber Band & Clearance Rack: Think of intraday mean reversion as an extended rubber band or a retail clearance rack. Extreme price extensions (Bollinger/VWAP stretches) represent heavily stretched elastic. Your edge is buying the clearance item only after the markdown stops and the first buyer steps in (close-back-inside), riding the snapback to wholesale fair value (VWAP/SMA).

📌 Data Contract Convention: This module operates strictly on OHLCV + observed spread. We assume no order-book depth, tick feeds, or partial fills. Signals form at the close of bar t and execute at the open of bar t+1. Observed spreads are deducted on both entry and exit. Higher-timeframe context (H1/D1) serves purely as causal gates joined at formal release timestamps. All trades flatten before session close.


How It Works

Three reversion families share a unified execution engine and session clock. They differ only in how they identify extreme price dislocations:

  1. Bollinger/RSI Fade: Combines a 2-standard-deviation price extreme with RSI(2) momentum exhaustion ($< 10$ or $> 90$) TradingCompendium on RSI + Bollinger 2026. The entry triggers only when price closes back inside the band, confirming exhaustion TradingPlan on Bollinger band mean reversion.
  2. Session-VWAP Reversion: Anchors to the day’s volume-weighted average price AlgoLab on VWAP mean reversion. Trades trigger on a close back inside the $2\sigma$ deviation envelope, provided VWAP slope remains flat to moderate MomentumIQ on VWAP mean reversion.
  3. Prior-Day High/Low Sweep: Identifies liquidity pierces beyond yesterday’s high or low (PDH/PDL) DayTradingToolkit on prior-day high/low. When price pierces the level but fails to hold, closing back inside the prior session’s range, the engine fades the false breakout NetPicks on previous day high/low.

Figure: The M11 mean-reversion engine. Every setup requires a confirmed 2-step trigger (alert then close-back-inside) plus pre-trade cost and regime clearance.

flowchart TD
    bars["OHLCV + spread<br/>M1/M5/M15 completed bars"] --> clock["Session clock<br/>ETF RTH vs UTC synthetic"]
    bars --> calc["Technical calculations<br/>Bollinger RSI z ATR<br/>VWAP + bands prior levels"]
    clock --> htf{"Completed H1/H4/D1<br/>bias & release check"}
    calc --> htf
    htf -->|"not yet released<br/>or regime fails"| block["Stand aside<br/>log & count"]
    htf -->|"released & bias ok"| gates{"Spread-to-ATR<br/>and cost-to-target<br/>below ceiling?"}
    gates -->|"no"| block
    gates -->|"yes"| families["Reversion anchors<br/>Bollinger/RSI<br/>VWAP dev prior level"]
    families --> extreme{"Close beyond<br/>anchor + buffer?"}
    extreme -->|"no"| block
    extreme -->|"yes"| trigger{"Close BACK INSIDE<br/>or sweep close-back?"}
    trigger -->|"no: still extended"| block
    trigger -->|"yes"| exec["Arm for next bar<br/>signal on t -> open t+1<br/>spread deducted"]
    exec --> ambig{"Bar touches both<br/>stop and target?"}
    ambig -->|"yes"| adverse["Conservative: stop<br/>or exclude & count"]
    ambig -->|"no"| manage["Manage to target<br/>stop time-stop"]
    adverse --> flatten{"Forced flatten<br/>at session boundary?"}
    manage --> flatten
    flatten -->|"now"| flat["Flatten same session<br/>gap at next open"]
    flatten -->|"not yet"| hold["Hold to next bar<br/>time stop ticking"]

    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 data
    class calc,exec,hold,flat process
    class htf,gates,extreme,trigger,ambig,flatten decision
    class block,adverse risk
    class families ok

Step-by-Step Engine Walkthrough

  1. Session Initialization: Define the active trading window (09:30–16:00 ET for ETPs, 00:00–23:50 UTC for crypto).
  2. Anchor Computation: Calculate Bollinger Bands, session VWAP, and prior-day extremes from completed bars.
  3. Regime & Cost Triage: Verify ranging conditions ($\text{ADX} < 20 - 25$, moderate VWAP slope) and pass spread / ATR cost gates.
  4. Dislocation Alert: Detect a bar t_alert closing beyond the outer band or level.
  5. Close-Back Trigger: Confirm bar t_trigger closes back inside the anchor threshold.
  6. Next-Open Execution: Enter at open[t_trigger+1], immediately deducting observed half-spreads.
  7. Conservative Trade Management: Manage to target (mean/VWAP) or volatility stop, enforcing adverse stop-first resolution on ambiguous bars.
  8. Hard Session Flatten: Liquidate all open positions at the designated session closing bar.

Strategy Rules

All strategies inherit our strict intraday execution contract:

Replay Execution Contract

Area Contract Specification Rationale for Intraday Trading
Price Basis & Spread Mid vs. last defined in project card; spread column units explicitly verified Prevents flattering backtest returns with fractional basis errors
Session Clock Fixed RTH window for ETPs; synthetic UTC window for crypto with hard flatten Eliminates overnight carry risk and uncontrolled funding decay
Execution Timing Signal at close of bar t $\rightarrow$ fill at open of bar t+1 Completely eliminates same-bar look-ahead bias Saral on next-bar-open execution
Bar Ambiguity Stop-first resolution if high and low hit both exit thresholds Guarantees conservative, honest backtest accounting ohlcv.io on bar resolution
Cost Hurdles Mandatory spread / ATR and cost / target pre-trade filters Rejects trades where spread friction exceeds statistical edge
HTF Context As-of point-in-time joins using released bars only Prevents look-ahead leakage from developing macro bars
Position Sizing Volatility scaling based on ATR stop distance Equalizes dollar risk across disparate asset prices M23

Pre-Trade Cost & Regime Gates

Gate Metric Mathematical Definition Default Ceiling Action on Breach
Spread-to-ATR $\text{Spread}t / \text{ATR}{14, t}$ $\le 0.15 - 0.20$ Stand aside: bar volatility is too small to pay the spread
Cost-to-Target $(\text{Spread}_t + \text{Fees}) / \text{Target Distance}$ $\le 0.10 - 0.12$ Stand aside: spread consumes too much expected profit
Trend / Regime $\text{ADX}_{14, t}$ on M5 or completed H1 $\le 20 - 25$ Stand aside: market is trending strongly; reversion fails
VWAP Slope $ \Delta \text{VWAP}_{20} / \text{ATR}_{\text{hour}}$
Half-Life Alignment Estimated Ornstein-Uhlenbeck half-life $\tau$ $T_{\text{max}} \approx 2\tau$ Stand aside if half-life is infinite or unstable

Core Reversion Families

Family A: Bollinger/RSI Confluence Fade

  • Band Parameters: Bollinger(20, 2) on completed M5 closes; Wilder RSI(2).
  • Long Alert: close[t_alert] < BB_Lower[t_alert] AND RSI2[t_alert] <= 10.
  • Long Trigger: close[t_trigger] > BB_Lower[t_trigger] AND RSI2[t_trigger] > 5.
  • Short Trigger: Mirror logic at upper band (RSI2 >= 90).
  • Stop Loss: Low(t_alert) - 1.0 * ATR_M5.
  • Take Profit: Middle band (SMA 20) or fixed 1.5:1 RR.
  • Time Stop & Flatten: Maximum 12 bars (60 minutes); hard session flatten.

Family B: Session-VWAP Deviation Reversion

  • VWAP Anchor: Cumulative intraday volume-weighted average price reset at session open AlgoLab on VWAP mean reversion.
  • Deviation Bands: $\text{VWAP} \pm 2\sigma$ calculated from rolling 20-bar residuals.
  • Long Trigger: close[t_trigger] > VWAP_Lower2 following a pierce below $2\sigma$, subject to flat VWAP slope.
  • Take Profit: Session VWAP baseline or $1\sigma$ band.
  • Risk Controls: Stop placed at $3\sigma$ extreme or 1.0x ATR beyond breach low.

Family C: Prior-Day High/Low Sweep Rejection

  • Reference Levels: Prior Regular Trading Hours High (PDH) and Low (PDL) DayTradingToolkit on prior-day high/low.
  • Sweep Fade (Short): M5 bar pierces above $\text{PDH} + (0.10 \times \text{ATR})$ but closes back below PDH on bar t.
  • Sweep Fade (Long): M5 bar pierces below $\text{PDL} - (0.10 \times \text{ATR})$ but closes back above PDL on bar t.
  • Take Profit: Session VWAP or prior-day midpoint $(\text{PDH} + \text{PDL}) / 2$.
  • Stop Loss: $0.50 \times \text{ATR}$ beyond the sweep extreme wick.

Building It In Python

Below is the complete, modular Polars and NumPy implementation of the mean-reversion engine.

import polars as pl
import numpy as np

def calculate_bollinger_rsi(
    frame: pl.DataFrame, bb_window: int = 20, bb_dev: float = 2.0
) -> pl.DataFrame:
    """Calculate causal Bollinger Bands and Wilder RSI(2) on completed closes."""
    close = pl.col("close")
    sma = close.rolling_mean(bb_window)
    std = close.rolling_std(bb_window)

    # Wilder RSI(2) calculation
    delta = close.diff()
    gain = delta.clip(lower_bound=0).ewm_mean(alpha=0.5, adjust=False)
    loss = (-delta.clip(upper_bound=0)).ewm_mean(alpha=0.5, adjust=False)
    rsi2 = 100.0 - (100.0 / (1.0 + (gain / (loss + 1e-9))))

    return frame.with_columns([
        sma.alias("bb_mid"),
        (sma + bb_dev * std).alias("bb_upper"),
        (sma - bb_dev * std).alias("bb_lower"),
        rsi2.alias("rsi_2"),
    ])

def calculate_session_vwap(frame: pl.DataFrame, session_col: str = "session_id") -> pl.DataFrame:
    """Calculate cumulative session VWAP and rolling deviation bands."""
    pv = pl.col("close") * pl.col("volume")
    cum_pv = pv.cum_sum().over(session_col)
    cum_v = pl.col("volume").cum_sum().over(session_col)
    vwap = cum_pv / cum_v

    resid = pl.col("close") - vwap
    sigma = resid.rolling_std(20)

    return frame.with_columns([
        vwap.alias("vwap"),
        (vwap + 2.0 * sigma).alias("vwap_upper2"),
        (vwap - 2.0 * sigma).alias("vwap_lower2"),
    ])

def estimate_half_life(series: np.ndarray) -> float:
    """Estimate mean-reversion half-life via Ornstein-Uhlenbeck OLS regression."""
    lagged = series[:-1]
    delta = np.diff(series)
    phi = np.linalg.lstsq(lagged[:, None], delta[:, None], rcond=None)[0].item()
    if phi >= 0:
        return float("inf")  # Non-reverting series
    return float(-np.log(2) / np.log(1 + phi))

def apply_reversion_cost_gates(
    frame: pl.DataFrame,
    atr_col: str = "atr_14",
    target_mult: float = 1.0,
    max_spread_to_atr: float = 0.20,
    max_cost_to_target: float = 0.12,
) -> pl.DataFrame:
    """Filter bars where spread friction destroys mean-reversion expectancy."""
    target_dist = pl.col(atr_col) * target_mult
    spread_to_atr = pl.col("spread") / pl.col(atr_col)
    cost_to_target = pl.col("spread") / target_dist

    pass_gate = (
        (spread_to_atr <= max_spread_to_atr)
        & (cost_to_target <= max_cost_to_target)
    )

    return frame.with_columns([
        spread_to_atr.alias("spread_to_atr"),
        cost_to_target.alias("cost_to_target"),
        pass_gate.alias("cost_gate_pass"),
    ])

What This Means For Your P&L: Worked Numeric Example

Let us evaluate a Bollinger/RSI fade setup across liquid and illiquid trading hours:

  • Setup: Asset Price = $100.00, M5 ATR = $0.25, Target (to Mid-Band) = $0.30, Stop (beyond low) = $0.30.
  • Gate Ceilings: Max Spread / ATR = 0.20, Max Cost / Target = 0.12.
Market Condition Observed Spread Spread / ATR Cost / Target Gate Verdict Realized P&L Impact
RTH Mid-Morning (GLD) $0.020 $8.0%$ $6.7%$ ACCEPTED Tight spread allows clean 1.5:1 reward-to-risk capture
RTH Opening Whip (GLD) $0.060 $24.0%$ $20.0%$ REJECTED Avoids wide spreads that consume $>20%$ of mean-reversion target
BTC Perp (Consolidation) $2.50 (on $50 ATR) $5.0%$ $4.2%$ ACCEPTED Optimal low-friction snapback execution

Testing It Honestly

Honest testing requires adhering to strict simulation standards:

  1. Causal Data Pipeline: Compute all indicators strictly on closed bars. Join H1/D1 data using point-in-time release timestamps (release_time = bar_close + lag).
  2. Next-Open Execution: Fill signals at the open of bar t+1. Never execute at the signal bar’s close Saral on next-bar-open execution.
  3. Explicit Spread Deduction: Deduct the full observed bid-ask spread across the round trip.
  4. Conservative Ambiguity Resolution: If a bar touches both stop and target, record an adverse stop hit or exclude the trade and document the occurrence rate ohlcv.io on bar resolution.
  5. Purged Walk-Forward Splits: Validate strategies across chronologically rolling folds with embargo buffers to eliminate train-test leakage.

Variants & Extensions

Variant Configuration Implementation Adjustment Primary Benefit Operational Trade-off
Keltner vs. Bollinger ATR envelope vs. Standard Deviation Better accommodates fat-tailed gap moves Slower to respond to sudden intraday volatility
Rolling Z-Score Anchor Standardized $(P - \mu) / \sigma$ Clean dimensionless metric across asset classes Requires stationary rolling window estimation
Session-Mid Anchor $(H_{\text{session}} + L_{\text{session}}) / 2$ Computational simplicity; no volume data needed Ignores volume-weighted institutional fair value
Half-Life Scaled Exits Set time stop at $2 \times \text{Half-Life}$ Adapts holding period to asset’s empirical speed Unstable if underlying regime shifts to trending

Hands-On Project

Deliverable: Build docs/research/m11_mean_reversion_lab.md and implement the complete reversion lab in notebooks/m11_mean_reversion.ipynb.

Project Card Specification

  • Identity: GLD (ETP lane) and BTC-USDT Linear Perp (Crypto lane). Explicitly define price basis (mid/last) and spread column units.
  • Horizon & Clock: M5 decision bars. ETP: 09:30–16:00 ET (flatten at 15:58 ET). Crypto: 00:00–23:50 UTC (flatten at 23:50 UTC).
  • Execution Engine: Signal on close[t], enter on open[t+1]. Full observed spread deducted. Conservative stop-first ambiguous resolution.
  • Cost Ceilings: Spread-to-ATR $\le 0.20$; Cost-to-Target $\le 0.12$.
  • Validation: 5-fold purged rolling walk-forward with 20-bar embargo.

Preregistered Rejection Triggers

  • Strategy fails to produce positive net expectancy after deducting observed spreads.
  • Performance collapses under 90th-percentile spread stress.
  • Ambiguous bars account for $>10%$ of total trades and flip net P&L.
  • Half-life analysis indicates series is non-stationary ($\phi \ge 0$).

Key Takeaways

  • Reversion is Liquidity Provision: You are paid for absorbing impatient order flow, but you must pay the spread toll on both legs.
  • Confluence and Triggers are Mandatory: Never fade an extreme directly. Wait for price to close back inside the anchor threshold.
  • Slope Filters Save Accounts: Fading VWAP or bands during steep trend regimes results in catastrophic losses.
  • Enforce Conservative Ambiguity: Always assume adverse stop execution when a single bar spans both stop and target levels.
  • Flatten Hard Daily: Eliminate overnight carry risk by flattening all positions before the session close.

References