Nautilus Clocks and Timers
Nautilus exposes one
Clocktrait with two implementations:TestClock(backtest, manually advanced, no syscalls) andLiveClock(live, Tokio-async, wall-clock). Strategy code uses the same interface; the engine substitutes the implementation at the boundary. Strict monotonic nanosecond time via lock-freeAtomicTimeunderpins determinism. Every engine component receives the clock by dependency injection - there are no globals.
Core claim
self.clock is not etiquette, it is the determinism contract. Reading
wall-clock time outside self.clock (via datetime.now(), time.time(),
etc.) bypasses the abstraction that makes backtest output reproducible
and identical to live. The AtomicTime implementation uses lock-free
CAS so every read is strictly monotonic by ≥1ns, even under
multi-thread contention.
Timer API
Two shapes, both routing to on_time_event:
- Time alert -
set_time_alert_ns(name, alert_time_ns, callback?): fires once at a specific time. - Repeating timer -
set_timer_ns(name, interval_ns, start_time_ns?, stop_time_ns?, callback?): fires at fixed intervals, optional bounds.
Both accept optional per-timer callbacks; otherwise the strategy’s
registered on_time_event is the default sink. Timer names are
interned strings (Ustr) for O(1) equality and cancel.
TimeEvent shape
Fields: name, event_id (UUID4), ts_event (scheduled fire time),
ts_init (when the event object was constructed). In backtest both are
equal; in live ts_init − ts_event measures handler latency - useful
for the same kind of latency analysis the rest of the bus events get.
Backtest determinism details
- Engine sets all clocks to backtest start time.
- For each data point, advance time to its timestamp.
- Collect pending timer events in a priority queue.
- Pop events in
ts_eventorder, set clock to event’s exactts_event, run handler. - Re-advance after every handler to capture timers the handler itself just set (recursive timer support).
BTreeMap(notHashMap) for timer storage to guarantee stable iteration order.
When it applies
Every MK3 component, every strategy method, every adapter. Specifically:
- Daily-cap reset on session boundary →
set_time_alert_ns. - Hard-close 14:15 CT →
set_time_alert_ns. - Post-loss cooldown timers →
set_time_alert_ns. - Stagnant-stop / hold-time checks →
set_timer_nsrepeating. - The A2 ts_init replay-determinism gate (2026-05-15-mk3-data-foundation-constraint) rests on this same monotonic-AtomicTime guarantee.
When it breaks
- Direct
datetime.now()/time.time()calls - instant kill of backtest reproducibility. Grep-clean for these on every Codex handoff. __init__reaching forself.clock- clock isn’t wired yet at construction; wire fromon_start(per[[nautilus-actors]]).- Cross-component clock skew - by design impossible; every component is injected with the same clock instance. If you ever see drift, suspect a bug in your own state.
Evidence
- derived - author blog 2026-05-21, Clock and Timer Architecture in NautilusTrader, https://nautilustrader.io/blog/clocks-and-timers/
- derived -
nautilus_trader/common/clock.pyxandcrates/common/src/clock.rsin the installed venv
See Also
- Nautilus Actors - Strategy lifecycle, no-clock-in-
__init__ - Nautilus Architecture - single-threaded determinism premise
- Nautilus Custom Data -
ts_event/ts_initon data; same semantics as on TimeEvents - 2026-05-15-mk3-data-foundation-constraint - A2 determinism gate
Timeline
- 2026-05-21 | author blog - Filed from https://nautilustrader.io/blog/clocks-and-timers/ for MK3 canon.