Nautilus Clocks and Timers

Nautilus exposes one Clock trait with two implementations: TestClock (backtest, manually advanced, no syscalls) and LiveClock (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-free AtomicTime underpins 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_event order, set clock to event’s exact ts_event, run handler.
  • Re-advance after every handler to capture timers the handler itself just set (recursive timer support).
  • BTreeMap (not HashMap) 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_ns repeating.
  • 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 for self.clock - clock isn’t wired yet at construction; wire from on_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

See Also


Timeline