Dual-clock backtesting: minute-level signals with a 1-second risk clock
Signals fire on closed minute bars; stops, targets, and trailing logic are evaluated on 1-second closes with a strict per-second event order. Validation showed minute-only risk evaluation distorts tight-stop strategies by −55% while leaving wide-stop profiles almost untouched.
The architecture
The high-fidelity backtest engine runs two clocks against one canonical data source:
- Signal clock. Donchian signals are computed on closed 1m/5m/15m/30m bars, all aggregated from the same SPX 1-second series. The forming bar never participates in its own channel.
- Risk clock. Stop-loss, take-profit, trailing stops, time stops, and EOD exits are evaluated on 1-second closes only — never on intrabar highs/lows, which would require inventing the path inside a bar.
A four-state machine (FLAT → PENDING_ENTRY → OPEN → PENDING_EXIT → FLAT) ties the clocks together with a fixed per-second event order: fill pending orders at the second’s open, then process signal-bar closes, then check risk triggers at the second’s close. One unified rule prevents lookahead at the boundary: in any single second, at most one of {exit fill, new entry acceptance} may happen.
What minute-only risk evaluation gets wrong
Validation re-ran the same strategies with risk evaluated on 1-minute bars instead, across 778 trading days. For a tight-stop scalper profile (1m signals, 5-point stop, 3/5-point trailing):
- Total PnL error −55%: 1-second truth +5,732 points across 17,262 trades; the best minute-mode produced +2,584 points across 14,643 trades.
- Missed trailing exits −44%: 7,263 trailing-stop exits in truth vs 4,071 detected close-only on minute bars — arm-and-fire sequences completing inside a single bar are invisible.
- Phantom stop-outs +3%: intrabar high/low detection fires stops the 1-second close path never touched, then “fills” them at a reverted bar close.
The same comparison on a wide-stop rider profile (30m signals, 10-point trailing) deviated by only +0.6% — minute bars are fine when triggers are much larger than typical intrabar ranges.
Rules that keep the engine honest
- Signal-bar timestamps must be an exact subset of the 1-second timeline (no drift, no rounding).
- Filters (VIX, daily-range) evaluate at signal-bar close, never at the execution second.
- EOD exit is configured (15:49:59 trigger → 15:50:00 fill), not derived.
- Eleven mandatory integration tests pin these semantics, including byte-for-byte Python↔Rust parity of the replay core.
The lesson
Clock fidelity must match stop tightness. A minute-bar backtest is not “approximately right” for a 5-point intraday stop — it is a different strategy. Decide the risk clock first, then ask whether your data can support it.