Cross-day rolling Donchian channels for valid day-one signals

With a 30-bar 5-minute lookback computed within-session, no signal can exist for the first 2.5 hours of each day. Rolling the channel across session boundaries restores the open — at the cost of overnight-gap distortion that needs its own policy.

The dead zone

A 30-bar Donchian channel on 5-minute bars needs 30 completed bars before it is defined. Computed within-session, that means no valid signal for the first 2.5 hours of every trading day — entries cluster into late morning and midday by construction, and any edge specific to the open is structurally unobservable. This is a quiet form of time-of-day selection: the backtest never even sees the regime it excluded.

The fix: rolling_rth

The channel scope used across the Donchian projects is rolling_rth: for each signal bar, the lookback window takes prior completed RTH bars, and if the current session has fewer than N bars, it fills backward from the previous session (and earlier if needed). Implementation is a plain rolling window over a continuous multi-day RTH frame:

out["channel_upper"] = out["high"].rolling(lookback, min_periods=lookback).max().shift(1)
out["channel_lower"] = out["low"].rolling(lookback, min_periods=lookback).min().shift(1)

The .shift(1) keeps the current bar out of its own channel. Two deliberate boundaries remain:

The gap problem the fix creates

An overnight gap means the morning’s channel is built partly from yesterday’s range. A gap-up open “breaks out” of yesterday’s channel almost by definition, and gap-inflated ranges set ATR-based stops too wide. The projects’ review process flagged this explicitly, and the spec answer is a configurable gap policy — default intraday_true_range_only for ATR, with include_gap and cap_gap_at_p95 as sensitivity variants. SPX being a cash index helps: there is no overnight session to model, only the structural 17.5-hour close-to-open void.

Related plumbing that has to agree with the channel: half-days are auto-detected (last bar before 14:00 → 12:50 cutoff instead of 15:50), and a startup_no_trade_minutes grid (0–30) controls how soon after the open entries are allowed — relevant because 0DTE option spreads are widest in the first minutes.

The lesson

Warmup rules are strategy decisions, not implementation details. “When is the indicator defined?” determines which market regimes your backtest is even allowed to observe — make the choice explicit, instrument it, and stress the boundary it creates.