The Learning Library
Contents

Module 9 Companion — Causal Multi-Timeframe Context for Intraday Entries

Part III · Intraday Design, Alpha & Playbooks · Companion to Module 9 Prerequisites: M9, M8, M5, M22, M23


Overview

The core module established the causal multi-timeframe framework: higher-timeframe features release only after their source bars close, forward-filling never leaks forming data, all positions force flatten before session close, and every slower layer must beat an identical ungated baseline on purged out-of-sample net return.

This companion stocks the lab with three structured experiments designed to answer one central question: Does adding higher-timeframe context improve net session profitability after accounting for reduced trade frequency and transaction friction?

None of these experiments claims to prove alpha. Each is a preregistered ablation audit with explicit cost ceilings, metrics, and revise/reject criteria.

Experiment Core Question Target Lane & Clock Primary Slower Context
E1 — Completed H1 Trend Gate Does aligning M5 pullback entries with completed H1 20 EMA trend improve net session P&L? Commodity ETP (RTH) + Crypto perp (UTC synthetic) Completed H1 20 EMA slope + H1 ADX(14) ≥ 18
E2 — Prior-Session Level Map Does scoring breakouts against prior-day levels (PDH/PDL) outperform raw intraday channel breakouts? Same two lanes on M5 Prior Day High/Low (PDH/PDL) + Prior H4 levels with buffer
E3 — Volatility Gate & Cost Routing Does filtering trades by H1 ATR percentile (20%–85%) protect capital from quiet chop and explosive stops? Same two lanes on winning timeframe Completed H1 ATR percentile band (20%–85%) + Spread-to-ATR ceiling

💡 The Division of Labor Analogy: A gate is a bouncer, not an athlete. It cannot create winning moves; it can only refuse trades that enter unfavorable market regimes. The true test of a gate is not win rate—it is net return per eligible session after accounting for every missed opportunity.

📌 Data Contract: All experiments operate strictly on OHLCV + spread data. Signals evaluate on the close of bar t and fill at the open of bar t+1. Spreads are deducted on entry and exit. Ambiguous same-bar exits resolve stop-first. All positions force flatten before session close. Completed H1/H4/D1 bars serve as causal context only.


How It Works

The three experiments follow a coordinated evaluation pipeline.

E1 — The Trend Gate Ablation

We compute 20 EMA slope and ADX(14) on completed H1 bars. After releasing features strictly at the H1 bar close, we gate M5 pullback entries: longs are permitted only when H1 trend is bullish (EMA slope > 0) and trend strength is confirmed (ADX ≥ 18). We ablate the gated strategy directly against an ungated M5 baseline under identical execution rules.

E2 — The Level Map Ablation

We construct Prior Day High (PDH) and Low (PDL) from completed daily bars, and prior H4 extremes from completed H4 bars. M5 price action is scored as an objective structural event: a breakout occurs when price closes beyond PDH + (0.10 × ATR). We test whether conditioning on prior-session levels produces superior risk-adjusted returns compared to unanchored M5 channel breaks.

E3 — The Volatility Gate & Routing Audit

We rank completed H1 ATR against its trailing 100-bar history. Entries are permitted only when ATR percentile sits within the tradable 20% to 85% band. We evaluate whether this filter successfully eliminates low-wage quiet chop and high-risk volatility expansions.

Figure: The Module 9 experimental workflow. Slower context feeds into gated ablations to determine whether higher-timeframe features earn their complexity.

flowchart TD
    data["OHLCV + spread<br/>M5 + completed H1/H4/D1 releases"] --> e1["E1 Trend Gate<br/>H1 EMA/ADX/Donchian -> M5"]
    data --> e2["E2 Level Map<br/>PDH/PDL + prior H4 -> M5 events"]
    data --> e3["E3 Volatility & Cost Gate<br/>ATR percentile + spread/ATR"]
    e1 -->|"frozen M5 trigger + gates"| ablate["Gated vs ungated ablation<br/>same engine, same session"]
    e2 --> ablate
    e3 --> ablate
    ablate --> decision{"Net lift +<br/>opportunity floor?"}
    decision -->|"pass"| carry["Carry gates + clock<br/>to M10/M11 as baseline"]
    decision -->|"revise"| narrow["Revise: narrow clock<br/>swap H1 for H4 or buffer"]
    decision -->|"reject"| reject["Reject gate<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 data
    class e1,e2,e3,ablate,narrow process
    class decision decision
    class reject risk
    class carry ok

Strategy Rules

Each experiment follows our standardized nine-point protocol.

E1 — Completed H1 Trend Alignment Gate (M5 Pullback Control)

1. Market, Timeframe, and Data Assumptions

  • Lanes: One commodity ETP (GLD or PDBC) on RTH and one BTC/ETH linear perpetual on a UTC synthetic session (00:00–23:50 UTC).
  • Timeframes: M5 execution; H1 completed-bar trend context.
  • Data: Clean OHLCV + timestamped spread per bar.

2. Mechanics vs. Testable Hypothesis

  • Mechanics: Higher-timeframe flow represents institutional liquidity. Aligning intraday trades with slower trends filters out counter-trend distribution MQL5 on higher-timeframe alignment.
  • Hypothesis: Requiring M5 pullback longs to align with completed H1 20 EMA bullish slope and H1 ADX ≥ 18 yields higher net session return than an ungated baseline under purged walk-forward testing.

3. Required Indicators & Calculations

  • Completed H1 20 EMA slope and 14-period Wilder ADX.
  • M5 ATR(14) and feasibility ratios (Spread ÷ ATR ≤ 15%).
  • M5 pullback-resume trigger: close[t] > close[t-1] following an intraday dip below 5 EMA.

4. Causal HTF Context & Synchronization

  • An H1 bar closing at 10:00 is joined to M5 bars starting at the 10:05 open. The forming 10:00–11:00 bar is invisible.

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

  • Long Entry: bias_long[H1] and cost_pass and pullback_resume[M5] → fill at open[t+1] paying half-spread.
  • Stop Loss: entry_price - (1.2 × ATR_M5).
  • Profit Target: entry_price + (1.0 × ATR_M5).
  • Time Stop: 16 M5 bars (80 minutes).
  • Forced Flatten: Mandatory exit at session close.

6. Cost Filters & Execution

  • Spread-to-ATR ≤ 15%; Cost-to-Target ≤ 10%. Stop-first ambiguous resolution.

7. Sizing & Risk Limits

  • Standard ATR-based risk sizing (e.g., 0.5% equity risk per trade).

8. ML Extension (Walk-Forward Meta-Filter)

  • Train a Logistic Regression classifier on chronological folds to predict trade follow-through using H1 ADX, M5 pullback depth, and spread percentiles.

9. Failure Modes & Rejection Criteria

  • Opportunity Starvation: Gate raises win rate by 3% but cuts trades by 85%, lowering total P&L. Verdict: Reject gate.
  • Rejection Trigger: Reject if gated net session return is lower than ungated baseline on out-of-sample folds.

E2 — Prior-Session Level Map (PDH / PDL Breakout Control)

1. Market, Timeframe, and Data Assumptions

  • Same lanes and session clocks as E1. Primary execution on M5.
  • Daily PDH and PDL computed from completed daily bars.

2. Mechanics vs. Testable Hypothesis

  • Mechanics: Prior-day high and low are objective structural benchmarks tracked by institutional algorithms BuildTradingStrategies on previous-day levels.
  • Hypothesis: Trading M5 breakouts beyond PDH + (0.10 × ATR) with H1 trend alignment produces positive net expectancy after spreads and outperforms unanchored channel breakouts.

3. Required Indicators & Calculations

  • Completed Daily High (PDH), Low (PDL), and Close (PDC).
  • Completed H4 High/Low extremes.
  • M5 ATR(14) for dynamic buffer sizing (0.10 × ATR).

4. Causal Synchronization

  • Daily levels are fixed before session open. Prior H4 levels update strictly at H4 bar closes.

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

  • Breakout Long: close[t] > PDH + buffer and bias_long[H1] → fill at open[t+1].
  • Stop Loss: entry_price - (1.0 × ATR_M5) or below breakout bar low.
  • Profit Target: entry_price + (0.50 × Prior_Day_Range) or 1.2 × ATR_M5.
  • Forced Flatten: Mandatory session close.

6. Cost Filters & Execution

  • Standard double cost ceiling and stop-first ambiguous resolution.

7. Sizing & Risk

  • Volatility-scaled risk sizing per M23.

8. ML Extension (Breakout vs. Sweep Classifier)

  • Train a shallow XGBoost or Tree model to classify level tests as true breakouts vs. false sweeps using opening volume and prior-day range width.

9. Failure Modes & Rejection Criteria

  • Microstructure Churn: Tight buffers cause single-tick wicks to trigger false breakouts. Fix: Enforce 0.10 × ATR buffer.
  • Rejection Trigger: Reject if level breakouts fail to achieve positive net return after fees under walk-forward testing.

E3 — Volatility-Percentile Gate & Cost Routing

1. Market, Timeframe, and Data Assumptions

  • Same dual-lane scope on M5.
  • Completed H1 ATR(14) ranked against trailing 100 completed H1 bars.

2. Mechanics vs. Testable Hypothesis

  • Mechanics: ATR percentile normalizes volatility across assets DesireToTrade on ATR percentile.
  • Hypothesis: Restricting M5 trading to regimes where H1 ATR percentile is between 20% and 85% eliminates unprofitable chop and explosive drawdowns, raising net session Sharpe.

3. Required Indicators & Calculations

  • 14-period ATR on completed H1 bars.
  • Rolling 100-period percentile rank: (ATR < current_ATR).mean() * 100.

4. Causal Synchronization

  • H1 ATR percentile updates at each H1 bar close and applies to subsequent M5 bars.

5. Entry & Flatten Contract

  • Evaluates frozen M5 breakout or reversion baseline; armed only when 20% ≤ ATR_pct ≤ 85%. Mandatory session flatten.

6. Cost Filters & Execution

  • Strict enforcement of Spread-to-ATR ≤ 15%.

7. Sizing & Risk

  • Standard risk limits. Optionally scale down size by 50% when ATR percentile is in the 20%–35% band.

8. ML Extension (Regime Router)

  • Fit a 3-state Gaussian Mixture Model (GMM) on standardized ATR and spread to dynamically route between Breakout, Reversion, and Stand-Aside sleeves.

9. Failure Modes & Rejection Criteria

  • Band Over-Tightening: Restricting the band to 40%–60% starves trade frequency.
  • Rejection Trigger: Reject the volatility gate if it fails to improve net session return on out-of-sample test folds.

Building It In Python

Below is the complete, vectorized Polars and pandas implementation for E1, E2, and E3.

1. Intuition: E1 Causal H1 Trend Gate

We resample H1 bars causally, compute 20 EMA trend, and align features backward to M5.

# E1: Causal H1 trend gate aligned to M5 bars
# Q: How do we synchronize H1 trend features without lookahead?
import pandas as pd
import numpy as np

def build_e1_causal_trend_dataset(m5_df: pd.DataFrame) -> pd.DataFrame:
    # 1. Resample to completed H1 bars (Right-labeled)
    h1 = m5_df["close"].resample("1h", label="right", closed="right").last().to_frame()

    # 2. Compute H1 20 EMA and Trend Slope
    h1["ema20"] = h1["close"].ewm(span=20, min_periods=20).mean()
    h1["ema_slope"] = h1["ema20"].diff()
    h1["bias_long"] = h1["ema_slope"] > 0

    # 3. Backward As-Of Alignment to M5
    m5_df["h1_bias_long"] = h1["bias_long"].reindex(m5_df.index, method="ffill").fillna(False)

    # 4. M5 ATR and Feasibility Ratios
    prev_close = m5_df["close"].shift(1)
    tr = np.maximum(
        m5_df["high"] - m5_df["low"],
        np.maximum((m5_df["high"] - prev_close).abs(), (m5_df["low"] - prev_close).abs())
    )
    m5_df["atr14"] = tr.ewm(span=14, adjust=False).mean()
    m5_df["spread_to_atr"] = m5_df["spread"] / m5_df["atr14"]
    cost_pass = m5_df["spread_to_atr"] <= 0.15

    # 5. M5 Pullback-Resume Trigger
    pullback = m5_df["close"].shift(1) < m5_df["close"].shift(1).rolling(5).mean()
    resume = m5_df["close"] > m5_df["close"].shift(1)

    m5_df["sig_ungated"] = pullback & resume & cost_pass
    m5_df["sig_gated"] = m5_df["sig_ungated"] & m5_df["h1_bias_long"]

    return m5_df

2. Intuition: E2 Prior-Day Level Map

We construct PDH/PDL from completed daily bars and score M5 breakout events.

# E2: Prior-Day Level Map and M5 Breakout Trigger
def build_e2_level_map(m5_df: pd.DataFrame) -> pd.DataFrame:
    # Daily Levels from completed days (Right-labeled)
    d1_high = m5_df["high"].resample("1D", label="right", closed="right").max()
    d1_low = m5_df["low"].resample("1D", label="right", closed="right").min()

    m5_df["pdh"] = d1_high.reindex(m5_df.index, method="ffill")
    m5_df["pdl"] = d1_low.reindex(m5_df.index, method="ffill")

    # Breakout Trigger beyond PDH + 0.10 ATR buffer
    buffer = 0.10 * m5_df["atr14"]
    m5_df["sig_pdh_breakout"] = (
        (m5_df["close"] > m5_df["pdh"] + buffer) &
        m5_df["h1_bias_long"] &
        (m5_df["spread_to_atr"] <= 0.15)
    )
    return m5_df

3. Intuition: E3 Volatility Percentile Filter

We compute completed H1 ATR percentile and gate trades within the 20%–85% band.

# E3: H1 ATR Percentile Gate
def build_e3_volatility_gate(m5_df: pd.DataFrame) -> pd.DataFrame:
    # Compute H1 ATR on completed bars
    h1 = m5_df[["high", "low", "close"]].resample("1h", label="right", closed="right").agg({
        "high": "max", "low": "min", "close": "last"
    })
    prev_c = h1["close"].shift(1)
    h1_tr = np.maximum(h1["high"] - h1["low"], np.maximum((h1["high"] - prev_c).abs(), (h1["low"] - prev_c).abs()))
    h1["atr14"] = h1_tr.ewm(span=14, adjust=False).mean()

    # 100-period ATR Percentile
    h1["atr_pct"] = h1["atr14"].rolling(100, min_periods=50).apply(
        lambda w: (w < w.iloc[-1]).mean() * 100, raw=False
    )
    h1["vol_gate_pass"] = h1["atr_pct"].between(20, 85)

    # Align to M5
    m5_df["vol_gate_pass"] = h1["vol_gate_pass"].reindex(m5_df.index, method="ffill").fillna(False)
    m5_df["sig_vol_gated"] = m5_df["sig_gated"] & m5_df["vol_gate_pass"]

    return m5_df

4. Intuition: Conservative Bar-Replay Ablation Engine

We simulate both gated and ungated variants through our conservative execution engine.

# Conservative Replay Engine for Gated vs. Ungated Ablation
def run_ablation_replay(m5_df: pd.DataFrame, signal_col: str) -> dict:
    records = m5_df.to_dicts() if hasattr(m5_df, "to_dicts") else m5_df.to_dict("records")
    trades = []
    in_pos = False
    entry_p = 0.0
    stop_p = 0.0
    target_p = 0.0

    for i in range(len(records) - 1):
        curr = records[i]
        nxt = records[i+1]

        # Enter at next bar open
        if not in_pos and curr[signal_col] and not curr.get("is_final_bar", False):
            in_pos = True
            entry_p = nxt["open"] + 0.5 * nxt["spread"]
            atr = curr["atr14"]
            stop_p = entry_p - (1.2 * atr)
            target_p = entry_p + (1.0 * atr)
            continue

        if in_pos:
            hit_stop = curr["low"] <= stop_p
            hit_target = curr["high"] >= target_p

            # Stop-First Ambiguous Resolution
            if hit_stop and hit_target:
                trades.append({"pnl": stop_p - entry_p - curr["spread"], "type": "ambig_stop"})
                in_pos = False
            elif hit_stop:
                trades.append({"pnl": stop_p - entry_p - curr["spread"], "type": "stop"})
                in_pos = False
            elif hit_target:
                trades.append({"pnl": target_p - entry_p - curr["spread"], "type": "target"})
                in_pos = False
            elif curr.get("is_final_bar", False):
                trades.append({"pnl": curr["close"] - entry_p - curr["spread"], "type": "flatten"})
                in_pos = False

    pnl_series = [t["pnl"] for t in trades]
    return {
        "trade_count": len(trades),
        "total_pnl": sum(pnl_series),
        "win_rate": np.mean([p > 0 for p in pnl_series]) if trades else 0.0,
    }

Testing It Honestly

Honest testing requires comparing gated strategies against an identical ungated baseline:

  1. Gated vs. Ungated Ablation: Evaluate the strategy with and without the HTF gate. The gate is justified only if it improves net return per session after accounting for reduced trade count.
  2. Next-Bar-Open Execution: Every fill occurs at the open of bar t+1. Never fill at the signal bar’s close.
  3. Stop-First Ambiguous Resolution: When both stop and target are touched in a single bar, book the stop loss.
  4. Mandatory Session Flatten: Every position is closed at or before the session boundary.
  5. Purged Walk-Forward Splits: Test folds are session-aligned with embargo periods to prevent information leakage.

Variants & Extensions

Variant Modification Intended Benefit Key Metric to Report
H1 vs. H4 Trend Source Use H4 bars for trend permission Smoother, less noisy trend direction Trade count vs. net session P&L
Prior H4 Level Mapping Use H4 high/low instead of PDH/PDL Nearer intraday structural references Breakout frequency and buffer sensitivity
ATR Percentile Band Tuning Test bands: 20–85 vs. 25–75 vs. 15–90 Optimizes volatility filtering tightness Session P&L across volatility regimes
Directional Sizing Modifier Trade counter-trend setups at 50% size Retains trade opportunity with reduced risk Total P&L vs. stand-aside rule

Hands-On Project

Deliverable: docs/research/m09_causal_mtf_experiments.md and runnable notebook notebooks/m09_causal_mtf_experiments.ipynb.

Project Card — M9 Multi-Timeframe Experiments (v1.0)

Specification Area Project Requirement
Assets & Lanes One commodity ETP (GLD or PDBC) and one crypto perpetual (BTC or ETH) on an approved venue.
Experiments Evaluated E1 Trend Gate, E2 Level Map, and E3 Volatility Gate on M5.
Execution Rules Signal on close of bar t → fill at open of bar t+1; deduct timestamped spread; stop-first ambiguous resolution.
Session Calendars ETP: RTH 09:30–16:00 ET; Crypto: UTC 00:00–23:50 synthetic session (forced flatten at 23:50 UTC).
Validation Design 5-fold chronological purged walk-forward with embargo; separate untouched holdout period.
Acceptance Criteria Gated net expectancy exceeds ungated baseline on purged OOS; session trade floor met; ambiguous share < 5%.

Step-by-Step Instructions

  1. Prepare clean M5 data. Construct right-labeled H1 EMA trend, ATR percentile, and daily PDH/PDL levels.
  2. Verify causal alignment by printing boundary rows around H1 bar closes.
  3. Replay the ungated M5 baseline and record total session P&L.
  4. Replay E1 (Trend Gate), E2 (Level Map), and E3 (Volatility Gate) under identical execution rules.
  5. Run side-by-side walk-forward ablations comparing net return per session across all variants.
  6. Publish the final research memo documenting whether each gate earned its complexity.

Key Takeaways

  • HTF context is publish/subscribe: Slower bars publish only at their close. Lower timeframes subscribe causally.
  • Ablation is non-negotiable: An HTF gate must beat an identical ungated baseline on net session P&L, not just win rate.
  • Prior-day levels provide structural anchors: PDH and PDL turn arbitrary intraday wicks into objective breakout events.
  • Volatility gates filter out unviable tapes: ATR percentile ensures you only trade when market movement comfortably exceeds spread friction.
  • Enforce conservative execution: Fill at next-bar open, deduct full spreads, resolve ambiguous bars stop-first, and force flatten at session end.

References


Next: Module 10 — Intraday Trend-Following & Momentum Playbooks · Core: Module 9 — Causal Multi-Timeframe Context