Module 8 — Low-Timeframe Trading Lab (Tick to M15)
Part III · Intraday Design, Alpha & Playbooks · Priority 🎯 Core 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, M22, M23, M7
Overview
Start this laboratory with the price you can actually execute in the market. Never start with an indicator’s theoretical signal.
A commodity ETP trades during U.S. regular hours. Its market price can deviate from NAV, and you pay a bid-ask spread on every transaction SEC ETF bulletin. A futures-based commodity pool rolls contracts continuously. When the futures curve is in contango, rolling contracts drags performance relative to spot Fidelity on contango/backwardation. A physical trust like GLD holds bullion and discloses daily holdings SPDR Gold Shares (GLD). Meanwhile, a BTC or ETH linear perpetual exchanges funding payments between longs and shorts on an exchange-specific schedule that must be accounted for if held through funding timestamps Coinbase perpetual funding.
Our core hypothesis is narrow: at M1, M5, and M15, simple technical baselines can be tested rigorously enough that a negative feasibility verdict is just as valuable as a positive one.
Intraday edge is not about complex price prediction. It is about whether a simple trade setup can cover transaction costs and exit flat before the session close. Who is on the other side of your trade? The market maker quoting the spread and, in perpetuals, the funding counterparty. Your strategy must generate an average gross gain that comfortably exceeds that transaction toll.
Evidence status: hypothesis, mechanics-supported. Product wrappers and exchange mechanics explain why trading frictions behave as they do. They do not prove that an intraday edge exists. You must establish net profitability through rigorous replay.
The primary pitfall at low timeframes is the ambiguous-bar mirage. An OHLC bar records only that a high and a low were touched. It does not record the intrabar price path. A backtester that assumes a profit target was hit before a stop loss when both levels sit inside the same bar will report fictional profits ohlcv.io on bar resolution. Combined with filling at the signal bar’s close instead of the next bar’s open Saral on next-bar-open execution, this error creates an illusion of profitability that vanishes in live trading.
💡 The Audit Ledger Analogy: Treat the OHLC bar as an audit ledger. The bar’s ATR is the gross wage the market offers; the spread is the broker’s mandatory toll. When both stop and target are touched inside the same bar, that entry is an ambiguous journal entry. To remain honest, you must book the worst-case loss.
📌 Data Contract: This module operates strictly on OHLCV + spread data. We assume no order book, tick data, or partial fills. Signals evaluate at the close of bar
tand fill at the open of bart+1. Spreads are deducted on both entry and exit. Same-bar barrier conflicts resolve stop-first. All positions are forced flat before the session close. Completed H1/H4/D1 bars serve as causal context only.
How It Works
The laboratory operates on three coordinated components.
1. The Conservative Replay Engine
The engine replays M1, M5, and M15 bars with strict causal discipline:
- Execution Timing: Signals calculated on the close of bar
texecute at the open of bart+1. - Cost Deduction: Timestamped spread plus fee and slippage stress are deducted on every trade.
- Ambiguous-Bar Resolution: If a bar touches both the stop loss and profit target, the engine books the stop loss first (worst case) or excludes the trade and logs the ambiguous share.
- Forced Flatten: Every position is closed unconditionally at or before the final eligible bar of the session (ETP RTH close or crypto synthetic boundary). Gaps fill at the next open.
2. Two Deterministic Control Baselines
The lab evaluates two standard technical controls:
- Breakout Control: Donchian channel breakouts with ATR range-expansion and ADX trend filters.
- Reversion Control: Bollinger Band and Keltner Channel mean reversion with RSI and session VWAP stretch filters.
3. Pre-Trade Feasibility Gates
Before any trade is executed, two cost gates must pass:
- Spread-to-ATR:
Spread ÷ ATR ≤ 15%. - Cost-to-Target:
(Spread + Fees + Slippage) ÷ Target ≤ 10%.
Figure: The Module 8 laboratory pipeline. Data flows from left to right through cost filters, signal generation, conservative execution, and session-end flattening.
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 BB/Keltner<br/>RSI ADX VWAP slopes"]
atr --> signal["Two controls<br/>breakout vs reversion"]
clock --> gate1{"Spread-to-ATR<br/>below ceiling?"}
atr --> gate1
clock --> gate2{"Cost-to-target<br/>below ceiling?"}
atr --> gate2
clock --> gate3{"Bar eligible<br/>in declared session?"}
gate1 -->|"no"| reject["Block bar<br/>no new entry"]
gate2 -->|"no"| reject
gate3 -->|"no"| reject
gate1 -->|"yes"| ready["Eligible for<br/>next-bar-open eval"]
gate2 -->|"yes"| ready
gate3 -->|"yes"| ready
signal --> ready
ready --> exec["Bar-based execution<br/>close t -> open t+1<br/>spread deducted"]
exec --> ambig{"Both barriers<br/>touched?"}
ambig -->|"no"| book["Book realized<br/>stop or target"]
ambig -->|"yes"| adverse["Conservative: stop/adverse<br/>first or exclude & count"]
book --> flatten{"Flatten at<br/>session boundary?"}
adverse --> flatten
flatten -->|"yes"| flat["Forced flatten<br/>same session"]
flatten -->|"no"| hold["Hold to next bar<br/>time stop still 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,signal,exec,book,adverse,flat,hold process
class gate1,gate2,gate3,ambig,flatten decision
class reject risk
class ready okThe Step-by-Step Replay Workflow
- Load clean OHLCV + spread data for your commodity ETP or crypto perpetual.
- Compute indicators on completed bars (ATR, Donchian, Bollinger, VWAP).
- Evaluate cost gates at bar
tclose: Verify Spread-to-ATR and Cost-to-Target are below ceilings. - Arm trade for bar
t+1open: Pay the ask price (open + half-spread) for longs. - Simulate bar-by-bar progression: Check stops, targets, and time limits against subsequent OHLC bars.
- Apply conservative exit rules: Book stop loss if both barriers are touched in a single bar.
- Force flatten at session end: Close any open position at the final session bar.
Strategy Rules
These rules establish the laboratory baseline contract inherited by all subsequent strategy modules (M10–M12).
The Invariant Lab Contract
| Contract Area | Laboratory Rule | Practical Purpose |
|---|---|---|
| Price Basis & Spread | Declare mid vs. last price; document spread units (full vs. half, price vs. bps). | Prevents undercounting execution costs on M1/M5 bars. |
| Session & Flattening | ETP: RTH (09:30–16:00 ET); Crypto: UTC synthetic (00:00–23:50 UTC). Force flatten at session end. | Eliminates overnight gap risk and unmodeled funding costs. |
| Execution Timing | Signal on close of bar t → fill at open of bar t+1. |
Eliminates look-ahead bias from same-close execution. |
| Path Ambiguity | Both stop and target touched in one bar → resolve stop-first. | Accounts for intrabar path uncertainty conservatively. |
| Cost Ceilings | Spread ÷ ATR ≤ 15% and All-in Cost ÷ Target ≤ 10%. |
Discards expensive bars before spending compute on signals. |
| HTF Context | Completed H1/H4/D1 features joined strictly at bar close timestamp. | Prevents leakage of forming higher-timeframe bars. |
| Risk Limits | Fixed ATR stop loss, session trade caps, and daily loss limits from M23. | Prevents catastrophic drawdown during adverse regimes. |
The Two Control Baselines
1. Breakout Control (Donchian / ATR / ADX)
- Long Entry:
close[t] > donchian_high[t]by buffer, withADX(14) > 18and cost gates passing. - Stop Loss:
entry_price - (1.2 × ATR). - Profit Target:
entry_price + (1.0 × ATR). - Time Stop: 16 bars (80 minutes on M5) or session end.
2. Reversion Control (Bollinger / Keltner / RSI / VWAP)
- Long Entry:
close[t] < bb_lower[t]andclose[t] < session_vwap × 0.998withRSI(14) < 30andADX(14) < 20(non-trending tape). - Stop Loss:
entry_price - (1.2 × ATR). - Profit Target: Reversion to session VWAP or Bollinger midline.
- Time Stop: 12 bars (60 minutes on M5) or session end.
📌 Transfer Evidence Note: Documented breakout patterns Volatility Box on opening range breakout and mean-reversion studies Volatility Box on Keltner vs Bollinger in equity index futures represent transfer evidence. Use them to establish prior ranges for lookbacks and multipliers, never as proof of net edge in your target lane.
Building It In Python
Below is a complete, vectorized Polars implementation of the laboratory indicators and replay engine.
1. Intuition: Calculate Lab Indicators
We compute ATR, Donchian channels, Bollinger Bands, Keltner Channels, RSI, and session VWAP from completed bars.
# Laboratory technical indicators in Polars
# Q: How do we construct clean, causal features?
import polars as pl
def compute_lab_indicators(frame: pl.DataFrame) -> pl.DataFrame:
# 1. Average True Range (Wilder EMA)
prev_close = frame["close"].shift(1)
tr = pl.max_horizontal([
frame["high"] - frame["low"],
(frame["high"] - prev_close).abs(),
(frame["low"] - prev_close).abs(),
])
atr14 = tr.ewm_mean(span=14, adjust=False).alias("atr_14")
# 2. Donchian Channels (shifted by 1 bar to avoid lookahead)
don_high = frame["high"].shift(1).rolling(20).max().alias("don_high")
don_low = frame["low"].shift(1).rolling(20).min().alias("don_low")
# 3. Bollinger Bands (20-period SMA +/- 2 SD)
sma20 = frame["close"].rolling(20).mean()
sd20 = frame["close"].rolling(20).std()
bb_upper = (sma20 + 2.0 * sd20).alias("bb_upper")
bb_lower = (sma20 - 2.0 * sd20).alias("bb_lower")
# 4. RSI (14-period Wilder)
delta = frame["close"].diff()
gain = delta.clip(0, None).ewm_mean(alpha=1/14, adjust=False)
loss = (-delta.clip(None, 0)).ewm_mean(alpha=1/14, adjust=False)
rs = gain / (loss + 1e-10)
rsi14 = (100 - (100 / (1 + rs))).alias("rsi_14")
# 5. Cumulative Session VWAP
tp = (frame["high"] + frame["low"] + frame["close"]) / 3
cum_pv = (tp * frame["volume"]).cum_sum().over("session_id")
cum_vol = frame["volume"].cum_sum().over("session_id")
vwap = (cum_pv / (cum_vol + 1e-10)).alias("session_vwap")
return frame.with_columns([atr14, don_high, don_low, bb_upper, bb_lower, rsi14, vwap])
2. Intuition: Generate Signals & Apply Feasibility Gates
We generate signals at bar t close and enforce Spread-to-ATR and Cost-to-Target ceilings.
# Signal generation and cost gating
# Q: Which bars qualify for execution?
def generate_lab_signals(frame: pl.DataFrame) -> pl.DataFrame:
# Feasibility Cost Gates
s2atr = frame["spread"] / frame["atr_14"]
c2t = frame["spread"] / (0.9 * frame["atr_14"])
cost_pass = (s2atr <= 0.15) & (c2t <= 0.10)
# Breakout Control Signal
breakout_long = (frame["close"] > frame["don_high"] * 1.0005) & cost_pass
# Reversion Control Signal
reversion_long = (
(frame["close"] < frame["bb_lower"]) &
(frame["rsi_14"] < 30) &
(frame["close"] < frame["session_vwap"] * 0.998) &
cost_pass
)
return frame.with_columns([
s2atr.alias("spread_to_atr"),
c2t.alias("cost_to_target"),
breakout_long.alias("sig_breakout"),
reversion_long.alias("sig_reversion"),
])
3. Intuition: Conservative Bar-Replay Engine
We simulate trade progression bar by bar. Entry occurs at next-bar open; ambiguous exits resolve stop-first; open positions flatten at session end.
# Conservative bar-based replay engine
# Q: Does our edge survive honest execution and worst-case path resolution?
def run_conservative_replay(
frame: pl.DataFrame,
signal_col: str,
k_stop: float = 1.2,
k_target: float = 1.0,
max_bars: int = 16
) -> pl.DataFrame:
records = frame.to_dicts()
trades = []
in_pos = False
entry_price = 0.0
stop_price = 0.0
target_price = 0.0
entry_idx = 0
for i in range(len(records) - 1):
curr = records[i]
nxt = records[i+1]
# Check Entry Signal (Signal on t -> Fill on t+1 open)
if not in_pos and curr[signal_col] and not curr["is_final_bar"]:
in_pos = True
entry_price = nxt["open"] + 0.5 * nxt["spread"] # Pay half-spread on entry
atr = curr["atr_14"]
stop_price = entry_price - (k_stop * atr)
target_price = entry_price + (k_target * atr)
entry_idx = i + 1
continue
# Manage Open Position
if in_pos:
bars_held = i - entry_idx + 1
hit_stop = curr["low"] <= stop_price
hit_target = curr["high"] >= target_price
# 1. Ambiguous Bar Resolution (Stop-First)
if hit_stop and hit_target:
exit_price = stop_price - 0.5 * curr["spread"]
trades.append({"entry": entry_price, "exit": exit_price, "net": exit_price - entry_price, "type": "ambig_stop"})
in_pos = False
# 2. Clean Stop
elif hit_stop:
exit_price = stop_price - 0.5 * curr["spread"]
trades.append({"entry": entry_price, "exit": exit_price, "net": exit_price - entry_price, "type": "stop"})
in_pos = False
# 3. Clean Target
elif hit_target:
exit_price = target_price - 0.5 * curr["spread"]
trades.append({"entry": entry_price, "exit": exit_price, "net": exit_price - entry_price, "type": "target"})
in_pos = False
# 4. Time Stop or Forced Session Flatten
elif bars_held >= max_bars or curr["is_final_bar"]:
exit_price = curr["close"] - 0.5 * curr["spread"]
trades.append({"entry": entry_price, "exit": exit_price, "net": exit_price - entry_price, "type": "flatten"})
in_pos = False
return pl.DataFrame(trades)
Hand-Checkable Numeric Example
Suppose an M5 gold ETP setup triggers a reversion signal with the following parameters:
| Parameter | Value | Calculation / Meaning |
|---|---|---|
| Current M5 Close | $42.10 | Below lower Bollinger Band ($42.12) |
| Session VWAP | $42.35 | Stretched tape: $42.10 < 0.998 \times 42.35$ ($42.26) |
| RSI(14) | 27.0 | Oversold tail confirmed (< 30) |
| M5 ATR | $0.18 | Gross bar wage |
| M5 Spread | $0.030 | Round-trip toll |
| Spread-to-ATR | 16.7% | $0.030 ÷ 0.18$ (Exceeds 15% ceiling) |
| Verdict | ❌ BLOCKED | Cost gate refuses to arm the trade |
What this means for your P&L: Even though price is deeply stretched and RSI is oversold, the lab refuses to trade. The $0.03 spread consumes 16.7% of the bar’s expected move, meaning transaction costs would erode your expected edge.
Testing It Honestly
Honest backtesting enforces the data-aware contract without compromise:
- Next-Bar-Open Execution: Every fill occurs at the open of bar
t+1. Testing with same-bar close execution artificially borrows overnight gaps Saral on next-bar-open execution. - Stop-First Ambiguous Resolution: When a single bar touches both stop and target, resolve adversely. An edge that exists only when winning ambiguous bars is fictitious ohlcv.io on bar resolution.
- Mandatory Session Flatten: Every trade closes at or before the session boundary. No positions carry overnight.
- Purged Walk-Forward Splits: Test folds are session-aligned with embargo periods to prevent label leakage.
- Cost Stress Scans: Every backtest is evaluated under median spreads, p90 spread spikes, and added slippage stress.
# Purged walk-forward cross-validation skeleton
def generate_purged_splits(session_ids: list, n_splits: int = 5, embargo_sessions: int = 1):
unique_sessions = sorted(list(set(session_ids)))
split_size = len(unique_sessions) // (n_splits + 1)
for i in range(1, n_splits + 1):
train_sessions = unique_sessions[:i * split_size]
test_sessions = unique_sessions[i * split_size + embargo_sessions:(i + 1) * split_size]
yield train_sessions, test_sessions
⚠️ Pitfall Diagnostic: Run your strategy under two quick stress tests:
- Same-Close vs. Next-Open: If profits vanish when moving from same-close to next-open fills, your backtest was peeking at the gap.
- Median vs. p90 Spread: If net return collapses under 90th percentile spreads, your strategy is overconcentrated in illiquid hours.
Variants & Extensions
| Variant | Modification | Practical Purpose | Trade-off |
|---|---|---|---|
| M15 Step-Out | Replay on M15 bars | Rescues setups where M5 Spread-to-ATR is prohibitive | Lower trade frequency; wider stops |
| Session Window Slicing | Restrict trading to peak liquidity hours | Avoids wide spreads and low ATR periods | Reduces total session sample size |
| Causal HTF Gating | Require completed H1 EMA trend alignment | Filters out low-expectancy counter-trend signals | Fewer trades per session |
| Perpetual Funding Handling | Close before funding timestamp or book actual rate | Accounts for funding cash flows in crypto perps | Requires tracking exchange funding schedules |
Hands-On Project
Deliverable: docs/research/m08_feasibility_lab.md and reproducible notebook notebooks/m08_feasibility_lab.ipynb.
Project Card — M8 Baseline Feasibility (v1.0)
| Area | Pre-Registration Requirement |
|---|---|
| Identity | One commodity ETP (GLD or PDBC) and one crypto perpetual (BTC or ETH) on an approved venue; declare price basis and spread units. |
| Horizon | Replay M1, M5, and M15; maximum hold duration in bars; mandatory same-session forced flatten. |
| Clock | ETP: RTH 09:30–16:00 ET; Crypto: UTC 00:00–23:50 synthetic session (flatten at 23:50 UTC). |
| Execution | Signal on close of bar t → fill at open of bar t+1; deduct timestamped spread; stop-first ambiguous resolution. |
| Costs | Full spread deducted per round trip plus fee/slippage stress; evaluate under median and p90 spread surfaces. |
| Validation | Purged rolling walk-forward with embargo; untouched final holdout period. |
| Acceptance Criteria | Positive net expectancy on purged OOS; survival under p90 spread stress; parameter plateau (no single-cell spikes); ambiguous trade share < 5%. |
Step-by-Step Instructions
- Load clean M1/M5/M15 data. Compute ATR, Donchian, Bollinger, RSI, and session VWAP.
- Replay both the Breakout and Reversion baselines across all three timeframes.
- Apply next-bar-open execution and stop-first ambiguous-bar resolution.
- Run purged walk-forward cross-validation and report net expectancy per fold.
- Perform a parameter sensitivity sweep around lookbacks and ATR multipliers to verify parameter plateaus.
- Publish the final research memo with explicit Pass, Revise, or Reject verdicts for each lane.
Key Takeaways
- Execution timing determines validity: Backtests that fill at the signal bar’s close borrow unearned gaps. Honest testing fills at the next bar’s open.
- Ambiguous bars must resolve adversely: An OHLC bar cannot prove intrabar path order. Always book the stop loss when both barriers are touched.
- Cost gates prevent expensive mistakes: Spread-to-ATR and Cost-to-Target filters discard unviable bars before signals are evaluated.
- Baselines provide standard controls: Donchian breakouts and Bollinger/VWAP reversions establish honest benchmarks for subsequent modules.
- No position survives the session: Forced flattening eliminates overnight gap risk and unmodeled funding costs.
- Look for parameter plateaus: A strategy that works at only one parameter setting is a statistical fluke. True edges show stability across neighbor values.
References
- U.S. Securities and Exchange Commission — Exchange-Traded Funds Investor Bulletin — https://www.sec.gov/investor/alerts/etfs.pdf — Mechanics. ETF share trading and NAV mechanics.
- Fidelity — Commodity ETFs: Contango and Backwardation — https://www.fidelity.com/learning-center/investment-products/etf/commodity-etfs-contango-backwardation — Mechanics. Roll yield mechanics in futures-based funds.
- SPDR Gold Shares (GLD) — Prospectus and Holdings — https://www.ssga.com/us/en/intermediary/etfs/spdr-gold-shares-gld — Mechanics. Physical trust wrapper disclosures.
- Invesco — Optimum Yield Diversified Commodity Strategy No K-1 ETF (PDBC) — https://www.invesco.com/us/financial-products/etfs/invesco-optimum-yield-diversified-commodity-strategy-no-k-1-etf.html — Mechanics. Dynamic rolling pool methodology.
- Coinbase — Funding Rates on Perpetual Futures — https://help.coinbase.com/en/derivatives/perpetual-style-futures/funding-rate — Mechanics. Perpetual funding cash-flow mechanism.
- Saral Money — Backtest Execution Timing: Fill at Next Bar’s Open — https://saral.money/blog/next-bar-open-execution-timing/ — Mechanics. Same-close vs. next-open execution bias.
- ohlcv.io — The Bar Resolution Problem (Backtesting Pitfalls 04) — https://ohlcv.io/posts/backtesting-pitfalls/04-bar-resolution/ — Mechanics. Intrabar path ambiguity and conservative resolution.
- NautilusTrader — Bar-Based Execution Semantics — https://nautilustrader.io/docs/latest/concepts/backtesting/bar-execution/ — Mechanics. Synthetic intrabar path modeling.
- QuantJourney — Execution Assumptions in Backtesting — https://backtester.quantjourney.cloud/engine/execution-assumptions — Mechanics. Stop-first priority and fill assumptions.
- Volatility Box — Opening Range Volatility Breakout — https://volatilitybox.com/research/opening-range-volatility-breakout/ — Transfer Evidence. Range-expansion heuristics.
- Volatility Box — Keltner Channels vs. Bollinger Bands — https://volatilitybox.com/research/keltner-channels-vs-bollinger-bands/ — Transfer Evidence. Volatility envelope dynamics.
Next: Module 9 — Causal Multi-Timeframe Context for Intraday Entries · Companion: Module 8 Strategies — Low-Timeframe Lab Experiments