The Learning Library
Contents

Module 10 — Intraday Trend & Momentum

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 in U.S. regular hours at a market price that drifts from its published NAV. Creation and redemption happen exclusively in large blocks through authorized participants SEC ETF bulletin. A physical gold trust like GLD holds allocated bullion and reports daily holdings as part of its wrapper contract SPDR Gold Shares (GLD). Conversely, a futures-based pool rolls contracts. Its return diverges from spot whenever the curve slopes: contango imposes persistent drag, while backwardation adds yield Pomegra on commodity ETF liquidity and roll.

On the crypto lane, a BTC or ETH linear perpetual never expires. Instead, it exchanges periodic funding payments between longs and shorts on a strict venue schedule Coinbase perpetual funding and Coinbase on funding rates. Spreads on commodity ETPs 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-horizon directional persistence—opening-range continuation, Donchian breakouts, or EMA pullbacks—can overcome execution spread, exchange fees, and mandatory session flattens when strictly filtered by spread-to-ATR gates and causal completed-bar context.

The economic intuition is clear:

  • Information arrives in sequential waves. Overnight inventory imbalances and initial price discovery create an opening range.
  • Persistent order flow forces expansion. Institutional rebalancing and large algorithmic parents push price beyond early boundaries.
  • Pullbacks represent temporary liquidity absorption. Inside an established trend, shallow retracements reflect profit-taking that dominant flow quickly absorbs.

Who sits on the other side of your fill?

  1. The mean-reversion trader blindly fading every band extension.
  2. The discretionary trader taking premature profits at the range border.
  3. The institutional hedger mechanically unloading risk due to fixed mandates.

Evidence status: hypothesis, mechanics-supported. Wrapper structures, trading sessions, spreads, and intraday volatility shapes are documented facts. However, whether any trend rule extracts net alpha after fees remains an empirical question answered only by disciplined replay.

The primary failure mode is intraday whipsaw compounded by the spread toll. A breakout triggers on a volatile bar, reverses instantly, and extracts two round-trip spreads against a tiny ATR move. Secondary pitfalls include opening-range overfitting (optimizing the window to past noise) and ambiguous-bar mirages (assuming intrabar targets hit before stops) ohlcv.io on bar resolution, worsened by filling at the signal close rather than the next open Saral on next-bar-open execution.

💡 The Institutional Locomotive: Think of intraday trend following as catching a heavy freight train. Opening ranges and breakouts are institutional buying waves clearing out resting liquidity. You board the train after it clears the station switch, not by guessing its direction before the whistle blows. Pullbacks are brief rest stops where the engine absorbs minor profit-taking before resuming speed.

📌 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 bars (H1/H4/D1) serve only as causal context gates joined at their formal release timestamps. All positions flatten before the session close.


How It Works

Three classic trend models share a unified execution engine and session clock. They differ solely in their definition of trend initiation:

  1. Opening-Range (OR) Continuation: The market establishes a high and low during the initial 15 to 60 minutes (e.g., 09:30–10:00 ET for ETPs, 00:00–01:00 UTC for crypto). A bar close breaking this range plus a buffer signals continuation Volatility Box on opening range breakout and MQL5 on ORB strategies.
  2. Donchian/ATR Channel Breakout: Price breaks the highest high or lowest low of the past $N$ completed M5 bars (typically 20 to 55) quantstrategy.io on Donchian channels and LuxAlgo on Donchian channels. Volatility scaling via ATR standardizes risk across gold and crypto Investopedia on ATR and Volatility Box on volatility-adjusted stops.
  3. EMA Pullback Continuation: A slow baseline (EMA 50) defines trend direction. Price dips below a fast baseline (EMA 20) during a retracement, then closes back above it with momentum to signal resumption.

Before evaluating any signal, the engine enforces strict pre-trade filters: spread-to-ATR ratios, cost-to-target limits, and higher-timeframe alignment.

Figure: The M10 trend engine architecture. Every trade must satisfy cost hurdles and completed-bar causality before execution.

flowchart TD
    bars["OHLCV + spread<br/>M1/M5/M15 completed bars"] --> clock["Session clock<br/>ETF RTH vs UTC synthetic"]
    bars --> atr["Technical calculations<br/>ATR Donchian EMA ADX<br/>OR range Keltner/VWAP"]
    clock --> htf{"Completed H1/H4/D1<br/>bias & release check"}
    atr --> htf
    htf -->|"not yet released<br/>or gate 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["Signal families<br/>OR break / Donchian<br/>or EMA reclaim"]
    families --> signal{"Close of bar t<br/>beyond level + buffer?"}
    signal -->|"no"| block
    signal -->|"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 stop<br/>target 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,clock data
    class atr,families,exec,manage,adverse,flat,hold process
    class htf,gates,signal,ambig,flatten decision
    class block risk

Step-by-Step Engine Walkthrough

  1. Session Initialization: Define the trading window (e.g., 09:30–16:00 ET for ETPs; 00:00–23:50 UTC for crypto).
  2. Feature Computation: Calculate ATR(14), Donchian channels, EMAs, and opening-range extremes on completed bars only.
  3. Causal HTF Gating: Check higher-timeframe alignment using only released H1 bars (e.g., H1 close above EMA 20).
  4. Cost Triage: Verify that spread / ATR and (spread + fee) / target sit below preregistered ceilings.
  5. Signal Generation: Detect if bar t closes beyond the target threshold plus buffer.
  6. Next-Open Execution: Enter at the open of bar t+1, immediately deducting the observed spread.
  7. Conservative Trade Management: If a single bar touches both stop and profit target, enforce the adverse stop outcome.
  8. Mandatory Flatten: Liquidate all active positions at the session cutoff bar.

⚠️ Pitfall: Never compare 24/7 crypto bars directly to regular-hours ETF bars without defining a synthetic session. Unfiltered 24-hour crypto data hides illiquid overnight hours where spreads make trend following unviable.


Strategy Rules

All strategies in this module inherit a strict, non-negotiable 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
Bar Ambiguity Stop-first resolution if high and low hit both exit thresholds Guarantees conservative, honest backtest accounting
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 Fixed-fractional volatility scaling based on ATR stop distance Equalizes dollar risk across disparate asset prices

Pre-Trade Cost & Volatility 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
Spread Percentile $\text{Spread}_t > \text{Rolling } p75(\text{Spread})$ $\le p75$ Stand aside: local liquidity is abnormally thin
Trend Strength $\text{ADX}_{14, t}$ on M5 or completed H1 $\ge 14 - 18$ Stand aside: market is range-bound and prone to whipsaws
Auction / Funding Timestamp inside auction or funding window Outside only Stand aside: unmodeled microstructure slippage

Core Strategy Families

Family A: Opening-Range (OR) Continuation

  • OR Formation Window: 09:30–10:00 ET (ETP) or 00:00–00:30 UTC (Crypto). High and low freeze immediately at window close.
  • Long Trigger: close[t] > OR_high + buffer, where buffer = 0.10 * ATR_M5[t].
  • Short Trigger: close[t] < OR_low - buffer (crypto/short-enabled lanes only).
  • Eligibility Window: Only the first qualifying breakout between 10:00 and 11:30 ET is traded.
  • Stop Loss: entry_price - 1.2 * ATR_M5[t] (longs).
  • Profit Target: entry_price + 1.0 * ATR_M5[t] or 0.75 * OR_width.
  • Time Stop & Flatten: Maximum hold of 16 bars (80 minutes); mandatory flatten at session end.

Family B: Donchian/ATR Channel Breakout

  • Channel Definition: Highest high and lowest low of the last $N=20$ completed bars, lagged by 1 bar (.shift(1)).
  • Long Trigger: close[t] > Donchian_High[t] + buffer with $\text{ADX} \ge 16$.
  • Short Trigger: close[t] < Donchian_Low[t] - buffer.
  • Channel Width Gate: Block trades if channel width $< 0.6 \times \text{ATR}$ (low-volatility dead tape).
  • Risk Controls: Inherits standard ATR stop, target, time stop, and session flatten rules.

Family C: EMA Pullback Continuation

  • Trend Bias: close[t] > EMA(50) on completed M5 bars.
  • Pullback Condition: close[t-1] < EMA(20) while maintaining close[t-1] > EMA(50).
  • Reclaim Trigger: close[t] > EMA(20) + buffer with close[t] > close[t-1].
  • Risk Controls: Identical ATR-scaled exit and same-session flatten rules.

📌 Transfer Evidence Note: Heuristics derived from index futures or equity breakouts provide structural inspiration, not alpha proof. Spreads, borrow availability, and session clocks differ substantially for commodity ETPs and crypto perps.


Building It In Python

Below is the complete, modular Polars implementation of the trend engine.

import polars as pl

def calculate_atr(frame: pl.DataFrame, period: int = 14) -> pl.DataFrame:
    """Calculate Welles Wilder True Range and ATR from completed bars."""
    prev_close = pl.col("close").shift(1)
    tr = pl.max_horizontal([
        pl.col("high") - pl.col("low"),
        (pl.col("high") - prev_close).abs(),
        (pl.col("low") - prev_close).abs(),
    ])
    return frame.with_columns(
        tr.ewm_mean(span=period, adjust=False).alias(f"atr_{period}")
    )

def calculate_donchian(frame: pl.DataFrame, lookback: int = 20) -> pl.DataFrame:
    """Calculate causal Donchian channels using strictly completed past bars."""
    return frame.with_columns([
        pl.col("high").shift(1).rolling_max(lookback).alias("don_high"),
        pl.col("low").shift(1).rolling_min(lookback).alias("don_low"),
    ]).with_columns(
        ((pl.col("don_high") + pl.col("don_low")) / 2.0).alias("don_mid")
    )

def extract_opening_range(
    frame: pl.DataFrame, session_col: str = "session_id", or_end_time: str = "10:00:00"
) -> pl.DataFrame:
    """Extract and forward-fill fixed opening-range extremes per session."""
    or_levels = (
        frame.filter(pl.col("time_str") <= or_end_time)
        .group_by(session_col)
        .agg([
            pl.col("high").max().alias("or_high"),
            pl.col("low").min().alias("or_low"),
        ])
        .with_columns((pl.col("or_high") - pl.col("or_low")).alias("or_width"))
    )
    return frame.join(or_levels, on=session_col, how="left")

def apply_cost_gates(
    frame: pl.DataFrame,
    atr_col: str = "atr_14",
    target_mult: float = 1.0,
    fee_per_unit: float = 0.0,
    max_spread_to_atr: float = 0.20,
    max_cost_to_target: float = 0.12,
) -> pl.DataFrame:
    """Filter bars where trading friction exceeds statistical edge."""
    target_dist = pl.col(atr_col) * target_mult
    spread_to_atr = pl.col("spread") / pl.col(atr_col)
    cost_to_target = (pl.col("spread") + fee_per_unit) / target_dist
    spread_p75 = pl.col("spread").rolling_quantile(0.75, window_size=252)

    pass_gate = (
        (spread_to_atr <= max_spread_to_atr)
        & (cost_to_target <= max_cost_to_target)
        & (pl.col("spread") <= spread_p75)
    )

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

def simulate_bar_execution(
    entry_side: str, bar_high: float, bar_low: float, stop_loss: float, take_profit: float
) -> str:
    """Evaluate intrabar barrier touches under conservative adverse rules."""
    hit_stop = (bar_low <= stop_loss) if entry_side == "long" else (bar_high >= stop_loss)
    hit_target = (bar_high >= take_profit) if entry_side == "long" else (bar_low <= take_profit)

    if hit_stop and hit_target:
        return "ambiguous_stop_first"  # Conservative: assume loss occurred first
    if hit_stop:
        return "stop"
    if hit_target:
        return "target"
    return "hold"

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

Let us evaluate the exact same breakout setup across two distinct trading lanes:

  • Setup: Asset Price = $50.00, M5 ATR = $0.12, Target = $1.0 \times \text{ATR}$ ($0.12), Stop = $1.2 \times \text{ATR}$ ($0.144).
  • Gate Ceilings: Max Spread / ATR = 0.20, Max Cost / Target = 0.12.
Market Lane Observed Spread Spread / ATR Cost / Target Cost Gate Verdict Realized P&L Impact
GLD (Midday Lull) $0.020 $16.7%$ $16.7%$ REJECTED Saves account from slow churn where fees eat $>16%$ of profits
GLD (Morning Flow) $0.010 $8.3%$ $8.3%$ ACCEPTED Low friction allows directional momentum to capture net profit
BTC Perp (Liquid Hour) $2.00 (on $40 ATR) $5.0%$ $5.0%$ ACCEPTED Tight relative spread provides optimal edge realization

Testing It Honestly

Honest backtesting requires adhering to strict simulation standards:

  1. Causal Data Pipeline: Compute all technical indicators strictly on closed bars. Join H1/D1 data using point-in-time timestamps (release_time = bar_close + lag).
  2. Next-Open Execution: Fill signals at the opening tick of bar t+1. Never assume fills at the signal bar’s closing price 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.
def generate_purged_folds(n_bars: int, n_splits: int = 5, embargo: int = 20):
    """Generate causal train/test splits with embargo buffers."""
    fold_size = n_bars // (n_splits + 1)
    for i in range(1, n_splits + 1):
        train_end = i * fold_size
        test_start = train_end + embargo
        test_end = min(test_start + fold_size, n_bars)
        yield list(range(0, train_end)), list(range(test_start, test_end))

⚠️ The Two Golden Diagnostics:

  1. Same-Close vs. Next-Open: If your strategy is only profitable when executing at the signal close, your edge is an artifact of look-ahead bias.
  2. Median vs. P90 Spread Stress: If profits disappear under 90th-percentile spread stress, the system relies on unrealistic liquidity.

Variants & Extensions

Variant Configuration Implementation Adjustment Primary Benefit Operational Trade-off
OR Window (15m vs. 30m vs. 60m) Adjust formation duration 15m captures early momentum; 60m reduces false breakouts 15m suffers higher whipsaw; 60m reduces trade count
Donchian Lookback (20 to 55) Extend channel lookback Filters noise on volatile intraday tapes Slower entries reduce total captured move
ADX Trend Filter ($\ge 18$) Require minimum ADX Eliminates chop during consolidating regimes Discards early breakout opportunities
Time-of-Day Windowing Restrict entries to 10:00–11:30 ET Avoids midday spread widening and dead volume Limits total daily opportunity set
Spread Percentile Cap Stand down if spread $> p75$ Skips high-friction periods dynamically Slightly lowers session participation rate

Hands-On Project

Deliverable: Build docs/research/m10_trend_momentum_lab.md and notebooks/m10_trend_momentum.ipynb implementing the complete research pipeline.

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.

Step-by-Step Implementation Tasks

  1. Build Liquidity Atlas: Profile hourly spread and ATR distributions for both instruments.
  2. Implement Baseline Model: Code Family A (Opening-Range Breakout) using Polars.
  3. Execute Conservative Replay: Backtest across 12 months with next-open fills and adverse ambiguity handling.
  4. Stress Test: Apply 90th-percentile spread stress and 1-bar execution latency.
  5. Ablation Study: Compare ungated baseline performance against H1-filtered and cost-gated runs.

Preregistered Rejection Triggers

  • Strategy fails to produce positive net expectancy after deducting observed spreads.
  • Ambiguous bars account for $>15%$ of total trades and flip net P&L from positive to negative.
  • Performance collapses under 90th-percentile spread stress.
  • Parameter sensitivity analysis reveals an isolated spike rather than a stable performance plateau.

Key Takeaways

  • Friction Decides Viability: Intraday trend trading is a battle between market wage (ATR) and execution toll (spread). Pre-trade cost gates are mandatory.
  • Execution Must Be Causal: Fill orders at the open of bar t+1. Same-bar close execution introduces fatal look-ahead bias.
  • Enforce Conservative Ambiguity: Always assume adverse stop execution when a single bar spans both stop and target levels.
  • Hard Session Cutoffs: Intraday momentum models must flatten before session close to eliminate unmodeled overnight gap risk.
  • Audit Stability Over Optimization: Require wide parameter plateaus in walk-forward testing rather than overfitted point estimates.

References