BlogEngineeringPost

Clocks and Timers in NautilusTrader

April 2, 2026 · Chris Sellers

The clock has evolved over a decade across four languages: C#, Python, Cython, and now Rust. Each migration carried forward what worked, stripped away what didn't, and absorbed edge cases surfaced by traders running the engine across both research and live trading. Open-source contributors have helped refine the implementation through years of bug reports, fixes, and real-world use. The current Rust implementation is the most refined iteration to date.

The clock does more than return timestamps. It defines how time enters the engine: as monotonic nanosecond timestamps, as scheduled events, and as a dependency shared across backtesting and live trading. That design is what allows timer-driven logic to run against historical data and real markets through the same interface and execution model.

Time as events

NautilusTrader is an event-driven engine, and the clock follows the same pattern. When a timer fires, it produces a TimeEvent carrying four fields:

  • name: identifies the timer that produced the event, interned for O(1) equality
  • event_id: a unique identifier (UUID4)
  • ts_event: the scheduled fire time
  • ts_init: when the time event was created

The distinction between ts_event and ts_init matters for latency analysis in live trading: the gap between the two reveals how far the actual fire time drifted from the scheduled one.

TimeEvents flow through the same handler pipeline as market data events, order events, and position events. A strategy's on_time_event callback sits alongside other handlers such as on_bar, on_quote, and on_order_filled as part of a unified event model. The clock doesn't exist outside the event-driven architecture; it participates in it, producing typed events that the engine dispatches with the same ordering guarantees it applies to everything else.

Why nanosecond precision?

Major venues publish market data at nanosecond resolution natively. Nasdaq ITCH, CME MDP 3.0, and NYSE XDP all carry nanosecond timestamps. Institutional tick data from providers like Databento and LSEG Tick History preserves that precision across asset classes. NautilusTrader matches this resolution so that ingested data retains its original fidelity through research, simulation, and live execution without truncation.

At the event-ordering level, nanosecond precision means two events in the same microsecond may still have distinct timestamps, and the monotonic guarantee reduces the need for artificial secondary sequencing.

Why time matters in algorithmic trading

Time is part of trading logic, not only a bookkeeping detail.

As a decision-making input. Time-of-day effects influence liquidity and spread behavior. Session boundaries change the microstructure a strategy operates in. Sampling timers build bars, compute indicators, and feed feature extraction pipelines.

As a system control mechanism. Heartbeat timers monitor venue connectivity. Order timeout timers cancel stale orders. Scheduled timers manage strategy lifecycle: warming up indicators, throttling during volatile periods, shutting down before a session close.

As a boundary alert. Contract rollovers, expiry cutoffs, funding rate snapshots, and end-of-day risk resets all happen at known times. Time alerts let a strategy register a callback for a precise point in time and respond at the right moment rather than polling.

Because the clock and timers share a single interface across backtesting and live trading, all of this logic is testable against historical data using the same implementation that runs in real-time production.

Time as an injected dependency

NautilusTrader treats time as a dependency, not a global. Every component in the engine receives a clock through dependency injection and interacts with time exclusively through a trait interface. The data engine, execution engine, risk engine, portfolio, order emulator, throttlers, and ID generators all accept the same clock. So do user-facing components: actors and strategies receive the identical implementation, with the same interface and the same guarantees. The clock's concrete type determines the environment (TestClock for backtests, LiveClock for live trading), but the component holding it does not need to know which implementation it has.

Strategy code uses one clock interface. The engine supplies either a simulated or real-time implementation.

The Clock trait

pub trait Clock: Debug + Any {
    fn timestamp_ns(&self) -> UnixNanos;
    fn timestamp_us(&self) -> u64;
    fn timestamp_ms(&self) -> u64;
    fn timestamp(&self) -> f64;
    fn set_time_alert_ns(&mut self, ...) -> anyhow::Result<()>;
    fn set_timer_ns(&mut self, ...) -> anyhow::Result<()>;
    fn cancel_timer(&mut self, name: &str);
    fn cancel_timers(&mut self);
    // ...
}

The trait serves two roles. It provides timestamps (current time at nanosecond, microsecond, millisecond, or second resolution) and it manages timers. Timers come in two forms: a time alert fires once at a specific time, and a repeating timer fires at a fixed interval between an optional start and stop time. Both accept an optional callback per timer; if none is provided, the clock falls back to a registered default handler.

Deterministic time in backtests

TestClock stores time as a manually-controlled AtomicTime in static mode. Time advances only when the backtest engine tells it to via advance_time(to_time_ns), so the simulation path does not depend on wall-clock reads or system calls.

When time advances, the clock generates every TimeEvent that would have fired in the interval. TestTimer implements Rust's Iterator trait, generating events lazily on demand rather than pre-allocating them. Timer names use interned strings (Ustr) for O(1) equality checks, and TestClock stores timers in a BTreeMap for stable iteration order (a HashMap would introduce non-deterministic ordering). Events sort by ts_event before returning, so chained timers fire in the correct order.

The backtest engine's time flow:

  1. Set all clocks to the backtest start time.
  2. For each data point, advance time to that point's timestamp.
  3. Collect all pending timer events in a priority queue (BinaryHeap with reverse Ord so earlier timestamps pop first).
  4. Pop each event in timestamp order, set the clock to the event's exact ts_event, run the handler, and re-advance to capture any new timers the handler might have set.
  5. At the end of the run, flush remaining events at the final timestamp.

The re-advance step is what makes recursive timers work: a strategy that sets a timer inside a timer callback produces correct results in backtests because the engine re-checks for new events after every handler invocation.

Live clock and the Tokio runtime

LiveClock holds a &'static AtomicTime reference to the global ATOMIC_CLOCK_REALTIME and exposes the identical Clock trait interface. Where TestClock generates events synchronously on advance, LiveClock spawns each timer as a Tokio async task using tokio::time::interval_at(). The substitution is abstracted away from components that hold a dyn Clock reference: the same strategy and timer logic can run against both simulated and wall-clock time.

Monotonic unique nanosecond timestamps

AtomicTime::time_since_epoch() implements the timestamp guarantee as a lock-free compare-and-swap loop. Every call returns a value strictly greater than the previous, monotonic by at least 1 ns. The CAS loop means no two threads ever receive the same value, and even if the OS clock drifts backward the counter only advances. max(now, incremented) keeps the counter anchored to wall-clock time when possible, running ahead only when calls arrive faster than nanosecond resolution.

Under heavy concurrent access the CAS loop retries, but contention is self-resolving: system time naturally advances between iterations, and each iteration increments by at least 1 ns. The codebase validates this with a stress test running four threads at 100,000 iterations each.

The memory layout is #[repr(C)] for FFI compatibility:

#[repr(C)]
pub struct AtomicTime {
    pub realtime: AtomicBool,
    pub timestamp_ns: AtomicU64,
}

The foundation everything else depends on

Core engine events in NautilusTrader carry timestamps. Orders, fills, and position updates flow through an engine that assumes time is consistent, monotonic, and deterministic in backtests. If the clock is wrong, the error propagates through downstream components: the data engine sequences events incorrectly, the portfolio evaluates positions against the wrong time, and backtests lose reproducibility.

It is a small surface area with large consequences, and the current Rust implementation reflects years of production use, open-source iteration, and battle testing from real trading workloads.

Further reading

Learn more about NautilusTrader.

footer-logo

© 2026 Nautech Systems Pty Ltd. All rights reserved.

NautilusTrader™ is a product of Nautech Systems Pty Ltd (ABN 88 609 589 237). Nautech Systems provides algorithmic trading software only. We do not operate as a broker, dealer, or exchange, nor offer financial advisory services. Users are solely responsible for compliance with applicable laws and regulations. Subject to non-excludable consumer guarantees, we make no warranties and accept no liability for trading losses or regulatory violations arising from use of the software. Read full disclaimer.