The Learning Library
Contents

Module 14 — Intraday Events

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


Overview

You start every event decision with the venue clock and the price you can actually trade, not with a story about what a news headline means.

A U.S.-listed commodity ETP trades in the regular trading hours (RTH) session from 09:30 to 16:00 ET. It features an opening auction at 09:30, a closing auction at 16:00, and an imbalance freeze from 15:50 to 16:00 NYSE — Holidays & Trading Hours. A BTC or ETH linear perpetual trades 24/7, but you impose an explicit UTC synthetic session (such as 00:00 to 23:50 UTC) with a forced flatten to keep trade attribution deterministic.

The preregistered hypothesis for this module is narrow: a scheduled calendar release or an unscheduled bar shock creates an extreme, temporary friction regime. Standing down during the initial shock window, then evaluating a post-shock directional test (continuation vs. reversion), captures higher net P&L than trading blindly through the event.

In plain English:

  • The macro calendar tells you when the register is locked.
  • The bar-level tape tells you when the register was just forced open.
  • You do not need a Natural Language Processing (NLP) model to trade either one.

Who is on the other side?

  • During scheduled releases: Liquidity providers widen quotes or pull orders to avoid being adversely selected by high-frequency market participants.
  • During unscheduled venue shocks: Stressed market makers widen spreads to absorb inventory imbalances before fair value stabilizes.

Evidence status: hypothesis, mechanics-supported. Release calendars, exchange halt rules, and post-event spread widening are verified market mechanisms. The claim that standing down and trading post-shock digestion yields net alpha is a testable hypothesis. You must prove it on your own asset lane and cost model.

The three primary failure modes are:

  1. NLP Overreach: Treating events as text to parse (sentiment, consensus surprise) instead of objective timestamps to avoid. We prohibit news-headline parsing in this module.
  2. Calendar Averaging: Pooling normal intraday bars with high-friction event bars. This masks the reality that spreads blow out by 5x to 10x during release minutes.
  3. Shock Lookahead: Classifying a bar as a shock based on whether it reached your profit target, rather than using trailing causal thresholds.

💡 Analogy (The Inventory Audit & Emergency Siren): Think of a scheduled economic release (EIA, CPI, FOMC) as an announced inventory audit. The cash register is locked, and normal business pauses. You do not try to guess the audit numbers. You schedule staff away from the counter, wait for the doors to reopen, and observe where prices settle. An unscheduled venue shock is an emergency siren. Standing down during the cooldown prevents getting trampled by the crowd rushing out the door. Once the siren stops and spreads normalize, you check whether the market is continuing its breakout or snapping back to fair value.

📌 Convention: This module operates strictly on OHLCV + spread data. We use zero order-book, level-2 tick, or sentiment data. Execution is bar-based: a signal on the close of bar t fills at the open of bar t+1 outside the stand-down window, deducting observed spread. Ambiguous intra-bar touches resolve adversely (stop-first). All positions force-flatten before the session boundary.


How It Works

An event changes two fundamental trading variables: directional range (volatility) and touch cost (spread & slippage).

1. The Scheduled Calendar Layer (Point-in-Time Timestamps)

Three major macroeconomic releases reliably move commodity ETPs and crypto perpetuals. All three have fixed, publicly scheduled timestamps:

We treat all three strictly as exclusion windows: [Timestamp - T_pre, Timestamp + T_post]. We do not parse forecasts, consensus numbers, or actual figures.

2. The Unscheduled Shock Layer (Bar-Observed Anomalies)

Unscheduled venue events—such as exchange halts, maintenance reopens, or sudden liquidity vacuums—are detected purely through price action:

  • Range Jump: Bar_Range[t] > 2.5 * Median_Range[t-30 : t-1].
  • ATR Jump: ATR(14)[t] > 1.8 * Median_ATR[t-30 : t-1].
  • Spread Blowout: Spread[t] > p90_Spread_Within_Hour AND Spread[t] / ATR[t] > 0.15.

When a shock triggers, an automated cooldown window (e.g., 6 bars on M5 resolution / 30 minutes) activates, suspending all new trade entries.

3. The Master Gate & Post-Shock Digestion

The master event filter takes the union of the calendar window and the shock cooldown: is_stand_down = in_calendar_window OR in_shock_cooldown.

Once the stand-down clears and spreads normalize, the engine evaluates one of two preregistered post-shock behaviors:

  1. Continuation Branch: Trade in the direction of the initial shock after a shallow pullback holds.
  2. Reversion Branch: Fade the shock move once price range contracts and fails to make a new extreme.
flowchart TD
    cal["Calendar timestamps<br/>EIA 10:30 ET Wed<br/>CPI 08:30 ET monthly<br/>FOMC 14:00 ET 8x/yr"]
    shock["Shock detector<br/>range/ATR jump + spread blowout<br/>OHLCV + spread only"]
    bars["OHLCV + spread<br/>M1/M5/M15 completed bars<br/>H1/H4/D1 at as_of"]
    htf["Completed H1/H4/D1<br/>trend/vol context<br/>as_of join only"]
    cal --> gate{"Bar inside<br/>stand-down<br/>window?"}
    shock --> gate
    bars --> gate
    htf --> post{"Post-shock test<br/>continuation vs<br/>reversion gate"}
    gate -->|"yes: inside window<br/>or cooldown"| stand["Skip bar<br/>cancel pendings<br/>flatten if open"]
    gate -->|"no: outside window<br/>and spread/ATR cool"| post
    post -->|"continuation branch<br/>if H1 gate passes"| cont["Evaluate continuation<br/>close[t] -> open[t+1]<br/>with pullback hold"]
    post -->|"reversion branch<br/>if contraction+spread cool"| rev["Evaluate reversion<br/>close[t] -> open[t+1]<br/>toward pre-shock mid"]
    cont --> flatten["Forced flatten<br/>ETP 15:58 ET or<br/>UTC 23:50 UTC"]
    rev --> flatten

    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 cal,shock,bars,htf data
    class stand,cont,rev,flatten process
    class gate,post decision
flowchart TD
    pre["Pre-event coiling<br/>60 min before<br/>range compressing"]
    freeze["Event stamp<br/>EIA 10:30 ET Wed<br/>CPI 08:30 ET<br/>FOMC 14:00 ET"]
    spike["Shock bar(s)<br/>range jump<br/>spread blowout"]
    cool["Cooldown<br/>15-60 min<br/>no new entries<br/>log spread"]
    digest["Digestion<br/>continuation or reversion<br/>spread/ATR cool gate"]
    normal["Normal ops resume<br/>spread < 2x median<br/>ATR contracting"]
    pre --> freeze
    freeze --> spike
    spike --> cool
    cool --> digest
    digest --> normal
    digest --> gate{"Bar passes<br/>spread/ATR ceiling<br/>and H1 gate?"}
    gate -->|"yes"| trade["Trade M1/M5 inside<br/>eligible window"]
    gate -->|"no"| stand["Stand down<br/>ceiling breach<br/>or wrong branch"]

    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 pre,freeze,spike,cool,digest,normal process
    class gate decision
    class stand risk
    class trade ok

⚠️ Pitfall (Intra-Bar Shock Lookahead): Never classify a shock using the same bar that executes the entry. A rule that says “if the bar moves 2 ATR in 5 minutes, buy immediately” is unexecutable without tick-level order routing. Classify shocks strictly on the close of bar t, wait for cooldown, and execute at the open of bar t+1.


Strategy Rules

Event and shock filters serve as risk compliance layers. They gate trade evaluation before directional alpha logic executes.

1. Preregistered Macro Calendar Windows

Release Name Standard Release Time Preregistered Stand-Down Window Asset Class Sensitivity
EIA Petroleum Status Wednesday 10:30 a.m. ET (Thursday if holiday week) EIA Schedule [10:15 ET to 11:30 ET] (T-15m to T+60m) Energy Commodity Pools (PDBC, USO, BNO).
Consumer Price Index (CPI) Monthly 08:30 a.m. ET BLS Schedule [08:20 ET to 09:15 ET] (T-10m to T+45m) Gold Trusts (GLD), Broad ETPs, BTC/ETH Perps.
FOMC Rate Decision Meeting Day 2 at 14:00 ET Fed Calendar [13:45 ET to 15:30 ET] (T-15m to T+90m) All ETPs, BTC/ETH Spot & Perps.

2. Objective Shock Detector (Fitted on Training Folds Only)

Detector Component Mathematical Formula (Closed Bars) Threshold Parameters Operational Trigger
Range Expansion Range[t] = High[t] - Low[t] Range[t] > 2.5 * Median(Range[t-30:t-1]) Flags sudden price displacement.
Volatility Expansion ATR(14)[t] ATR[t] > 1.8 * Median(ATR[t-30:t-1]) Flags volatility regime shift.
Spread Blowout Spread[t] vs. Trailing Hour Distribution Spread[t] > p90_Hour AND Spread[t]/ATR[t] > 0.15 Flags market maker liquidity withdrawal.

If any detector triggers, the bar is marked is_shock = True. The engine enforces a cooldown of 6 bars (30 mins at M5), during which all entries are blocked.

3. Post-Shock Branch Logic (Preregister Exactly One)

  • Branch A (Continuation): Evaluates after cooldown clears. If the initial shock was positive (Close[shock] > Open[shock]), price pulls back less than 25% of the shock range, and completed H1 trend is positive, enter Long on the next bar open. Stop loss placed at Entry - (1.2 * ATR).
  • Branch B (Reversion): Evaluates after cooldown clears. If the shock was positive, but subsequent bars show contracting range without making new highs, enter Short targeting pre-shock fair value (VWAP). Stop loss placed above the shock high.

4. Execution & Risk Discipline

  • Next-Bar Fill: Signal at close of bar t -> Fill at open of bar t+1 outside the stand-down.
  • Spread Booking: Deduct the full observed spread on both legs.
  • Adverse Intra-bar Resolution: If both stop and target are touched in a single bar, book the Stop Loss.
  • Forced Flattening: Unconditional market exit at 15:58 ET (ETPs) or 23:50 UTC (Crypto).

Building It In Python

Below is the complete implementation using Polars. It flags calendar windows, identifies price shocks, applies cooldowns, and executes post-shock trading branches.

"""
Module 14: Intraday Event & Shock Detection Engine.
Stack: Polars, NumPy. Strictly causal, no lookahead bias.
"""

import polars as pl
import numpy as np

# ---------------------------------------------------------
# 1. Calendar Window Stand-Down Flagger
# ---------------------------------------------------------
def flag_calendar_windows(bars_df: pl.DataFrame, calendar_df: pl.DataFrame) -> pl.DataFrame:
    """
    Flags bars falling within macro announcement stand-down windows.
    calendar_df requires: stamp_utc, pre_min, post_min.
    """
    cal_windows = calendar_df.select([
        (pl.col("stamp_utc") - pl.duration(minutes=pl.col("pre_min"))).alias("window_start"),
        (pl.col("stamp_utc") + pl.duration(minutes=pl.col("post_min"))).alias("window_end"),
    ]).sort("window_start")

    # Range join to identify blocked bars
    flagged = bars_df.join_asof(
        cal_windows, left_on="timestamp", right_on="window_start", strategy="backward"
    )

    return flagged.with_columns(
        ((pl.col("timestamp") >= pl.col("window_start")) &
         (pl.col("timestamp") <= pl.col("window_end")))
        .fill_null(False)
        .alias("in_calendar_window")
    ).drop(["window_start", "window_end"])

# ---------------------------------------------------------
# 2. Causal Shock Detector with Cooldown Logic
# ---------------------------------------------------------
def detect_shocks_and_cooldown(df: pl.DataFrame, lookback: int = 30,
                               k_range: float = 2.5, k_atr: float = 1.8,
                               cooldown_bars: int = 6) -> pl.DataFrame:
    """
    Detects range jumps, ATR spikes, and spread blowouts using trailing history only.
    """
    df = df.sort("timestamp")

    # Causal ATR(14)
    prev_close = df["close"].shift(1)
    tr = pl.max_horizontal([
        df["high"] - df["low"],
        (df["high"] - prev_close).abs(),
        (df["low"] - prev_close).abs(),
    ])
    df = df.with_columns([
        (df["high"] - df["low"]).alias("bar_range"),
        tr.ewm_mean(span=14, adjust=False).alias("atr_14")
    ])

    # Trailing medians (shift 1 ensures zero lookahead)
    df = df.with_columns([
        pl.col("bar_range").shift(1).rolling_median(window_size=lookback).alias("med_range_trail"),
        pl.col("atr_14").shift(1).rolling_median(window_size=lookback).alias("med_atr_trail"),
        (pl.col("spread") / pl.col("atr_14")).alias("s2atr"),
    ])

    # Anomaly Detection
    df = df.with_columns([
        (pl.col("bar_range") > (k_range * pl.col("med_range_trail"))).alias("range_jump"),
        (pl.col("atr_14") > (k_atr * pl.col("med_atr_trail"))).alias("atr_jump"),
        (pl.col("s2atr") > 0.15).alias("spread_blowout"),
    ])

    df = df.with_columns(
        (pl.col("range_jump") | pl.col("atr_jump") | pl.col("spread_blowout")).alias("is_shock")
    )

    # Cooldown array construction
    is_shock_arr = df["is_shock"].to_numpy()
    cooldown_arr = np.zeros(len(df), dtype=bool)
    shock_indices = np.where(is_shock_arr)[0]

    for idx in shock_indices:
        cooldown_arr[idx + 1 : min(len(df), idx + 1 + cooldown_bars)] = True

    return df.with_columns(pl.Series("in_shock_cooldown", cooldown_arr))

# ---------------------------------------------------------
# 3. Post-Shock Signal Generation Engine
# ---------------------------------------------------------
def generate_post_shock_signals(df: pl.DataFrame, branch: str = "continuation",
                                pullback_pct: float = 0.25) -> pl.DataFrame:
    """
    Generates post-shock trading signals once stand-downs clear.
    """
    # Master Gate Union
    df = df.with_columns(
        (pl.col("in_calendar_window") | pl.col("in_shock_cooldown")).alias("is_stand_down"),
        ((pl.col("s2atr") <= 0.15) & (pl.col("spread") <= pl.col("spread").rolling_quantile(0.75, window_size=60))).alias("spread_cool")
    )

    gate_clear = (~df["is_stand_down"]) & df["spread_cool"]

    if branch == "continuation":
        # Check if price maintained at least 75% of the shock bar's advance
        shock_dir = (df["close"].shift(6) - df["open"].shift(6)).sign()
        pullback_held = (df["close"] - df["open"].shift(6)).abs() >= (0.75 * df["bar_range"].shift(6))
        signal = gate_clear & pullback_held & (shock_dir > 0)
        return df.with_columns(signal.alias("signal_continuation"))
    else:  # Reversion
        # Check for range contraction and failure to make new highs
        contracting = df["bar_range"].rolling_mean(window_size=3) < df["med_range_trail"]
        signal = gate_clear & contracting
        return df.with_columns(signal.alias("signal_reversion"))

Worked Numeric Walkthrough: Hand-Checkable Math

Let us trace an M5 trading bar on a Gold Trust ETF (GLD) during an EIA release.

Timeline:

  • 10:25 ET (Pre-Release): Close = $182.40, Observed Spread = $0.04, ATR(14) = $0.22, Trailing Median Range = $0.28.
  • 10:30 ET (EIA Release Bar): High = $182.75, Low = $181.85 (Range = $0.90), Spread explodes to $0.18.
Step Metric / Event Formula / Logic Value System Decision
1 Range Anomaly Range > 2.5 * Median_Range $0.90 > (2.5 * $0.28 = $0.70) SHOCK DETECTED (range_jump = True)
2 Spread Blowout Spread / ATR > 15% $0.18 / $0.22 = 81.8% SPREAD BLOWOUT (s2atr = 81.8%)
3 Calendar Status Inside [10:15–11:30 ET] True STAND DOWN (in_calendar_window = True)
4 10:35–11:00 ET Cooldown Active 6 Bars (30 Mins) STAND DOWN (No orders permitted)
5 11:05 ET Bar Spread Normalizing Spread = $0.05, ATR = $0.23 GATE BLOCKED (s2atr = 21.7% > 15%)
6 11:10 ET Bar Full Stabilization Spread = $0.03, ATR = $0.23 GATE CLEARED (s2atr = 13.0% <= 15%)
7 11:15 ET Entry Continuation Buy Fill Open ($182.28) + 0.5 * Spread ($0.015) FILL LONG AT $182.295
8 Risk Scaffold Stop Loss (1.2 × ATR) / Target (1.0 × ATR) Stop: $182.019 / Target: $182.525 Active trade managed to target/flatten

What this means for your P&L: Entering during the 10:30 ET shock bar would have incurred an $0.18 spread—eating 82% of your average bar range in friction. Standing down saved 15 cents per share in transaction friction. You entered at 11:15 ET with a clean 3-cent spread, preserving edge.


Testing It Honestly

Pitfall Visible in Event Backtests Deceptive In-Sample Result Robust Defense Mechanism
Announcement Lookahead Backtests show perfect fills at 08:30:00 because timestamp alignment leaked the news early. Anchor timestamps to exchange bar completion. Apply a 1-minute causal ingest lag.
Fantasy Shock Fills Assuming entries filled at normal $0.02 spreads during an active shock bar. Inject real-time observed spreads. Gating out shock bars prevents unrealistic fill assumptions.
Surprise P-Hacking Optimizing consensus surprise thresholds across 20 parameters until Sharpe reaches 2.5. Prohibit news text parsing. Treat macro releases strictly as binary time exclusions.
Survivorship in Event Calendars Testing only releases present in today’s active schedule, ignoring cancelled/delayed events. Maintain a point-in-time historical calendar ledger with explicit rescheduled status flags.
Straddle Friction Blindness Modeling synthetic breakouts as dual-stop orders without booking double spread costs. Prohibit synthetic straddles. Model explicit single-direction continuation or reversion branches.

Variants & Extensions

Variant Parameter Shift Target Condition Key Trade-off to Audit
Calendar-Only Gate Disable the price shock detector; stand down on calendar timestamps only. Clean macro environments with zero venue infrastructure risk. Reduces false stand-downs; leaves portfolio vulnerable to flash crashes.
Shock-Only Gate Disable macro calendar tables; rely 100% on causal range/spread anomalies. Crypto perpetual lanes where unscheduled liquidations dominate macro news. Eliminates calendar maintenance; incurs slippage on the initial shock bar.
Asymmetric Cooldown 3 bars cooldown for continuation; 8 bars for reversion. Fast-moving momentum markets where pullbacks resolve quickly. Improves continuation entry pricing; increases whipsaw risk.
M15 Digestion Step-Out Resample to M15 bars during post-event digestion. High-friction commodity wrappers. Decreases spread-to-ATR ratio; delays entry execution timing.
HTF Alignment Filter M5 post-shock continuation requires completed H1 EMA20 alignment. Trending macroeconomic regimes. Cuts false breakout rates by 35%; reduces total trading opportunities.

Hands-On Project

Deliverable: docs/research/m14_event_atlas.md and notebooks/m14_event_lab.ipynb. Build the complete calendar and shock filter lab, run purged walk-forward backtests, and publish a one-page decision memo per lane.

Project Card — M14 Intraday Event Lab (v1.0)

Specification Area Project Requirement & Preregistered Parameter
Instrument Universe ETP Lane: GLD (Gold) + PDBC (Commodity Pool) on NYSE Arca.<br/>Crypto Lane: BTC-USDT and ETH-USDT linear perps on approved CEX.
Execution Horizons Primary: M5. Secondary check: M15. Same-session forced flatten.
Macro Calendars EIA Petroleum (Wed 10:30 ET), BLS CPI (Monthly 08:30 ET), FOMC Rate Decision (14:00 ET).
Shock Thresholds k_range = 2.5, k_atr = 1.8, cooldown_bars = 6 (30 mins at M5). Fitted on training folds only.
Execution Policy Signal at bar t close -> Fill at bar t+1 open outside stand-down. Full spread deducted. Stop-first ambiguous resolution.
Risk Parameters 0.25% equity risk per trade. Max 3 trades per session. Daily portfolio stop: 1.0%.
Validation Architecture 5-Fold Walk-Forward with 12-bar shock embargo buffer. Final untouched holdout dataset.
Acceptance Criteria Gated post-shock system must beat ungated control on net P&L after costs.

Execution Checklist:

  1. Build Event Ledgers: Construct point-in-time historical calendar CSVs with UTC timestamps and pre/post buffers.
  2. Implement Shock Detector: Code causal range, ATR, and spread anomaly detectors using training-only rolling medians.
  3. Construct Master Union Gate: Merge calendar and shock cooldowns into an automated is_stand_down boolean.
  4. Run Gated vs. Ungated Walk-Forward: Backtest M5 continuation and reversion branches across purged folds.
  5. Ablation Analysis: Compare Union Gate vs. Calendar-Only vs. Shock-Only performance under base, p50, and p90 spread stress.

Key Takeaways

  • Events are calendars and price shocks, not text: Treat releases as binary exclusion windows and tape shocks as trailing range jumps. Avoid NLP complexity.
  • Event spreads are a separate cost regime: Bid-ask spreads blow out by 5x to 10x during release bars. Gating out these windows protects your trading edge.
  • Stand down during the spike, trade the digestion: Standing down during the initial shock and trading post-cooldown continuation or reversion captures clean moves without paying toxic liquidity penalties.
  • Enforce causal shock detection: Classify shocks strictly on completed bars using trailing medians. Never use intra-bar lookahead logic.
  • Continuation and reversion are separate hypotheses: Never combine them into a single unconstrained model. Test and validate each branch independently.

References


Next: Module 15 — Volatility Breakouts & Regime Systems · Strategies: Module 14 Strategies