orderbook-rs 0.7.0

A high-performance, lock-free price level implementation for limit order books in Rust. This library provides the building blocks for creating efficient trading systems with support for multiple order types and concurrent access patterns.
Documentation
//! Pluggable timestamp source for the matching core.
//!
//! The [`Clock`] trait abstracts the source of millisecond timestamps used
//! by the order book to stamp inbound orders, snapshots, and lifecycle
//! transitions. Two implementations are provided:
//!
//! - [`MonotonicClock`] — wraps [`crate::utils::current_time_millis`] for
//!   production deployments. Returns wall-clock milliseconds since the
//!   Unix epoch.
//! - [`StubClock`] — a deterministic counter-based clock for tests and
//!   byte-identical sequencer replay. Each call to `now_millis` advances
//!   an internal counter by a fixed `step` (default `1`).
//!
//! Both implementations are `Send + Sync` so an `Arc<dyn Clock>` can be
//! shared across threads. The trait is object-safe and is stored on
//! [`crate::orderbook::book::OrderBook`] as `Arc<dyn Clock>`.

use pricelevel::TimestampMs;
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};

/// A source of wall-clock or logical millisecond timestamps for the
/// matching core.
///
/// Implementations must be [`Send`] + [`Sync`] because an
/// `Arc<dyn Clock>` is held by the order book and potentially shared
/// across threads. The trait is object-safe: it is stored as
/// `Arc<dyn Clock>` on the book so different deployments (production,
/// replay, deterministic tests) can swap the implementation without
/// changing the generic parameterization of the order book.
pub trait Clock: Send + Sync + fmt::Debug {
    /// Current millisecond timestamp.
    ///
    /// Semantics depend on the implementation:
    /// - production ([`MonotonicClock`]): wall-clock milliseconds since
    ///   the Unix epoch.
    /// - replay / test ([`StubClock`]): a monotonic logical counter,
    ///   not wall-clock.
    fn now_millis(&self) -> TimestampMs;
}

/// Production clock wrapping [`crate::utils::current_time_millis`].
///
/// Returns wall-clock milliseconds since the Unix epoch. This is the
/// default clock installed on every [`crate::orderbook::book::OrderBook`]
/// constructed via [`crate::orderbook::book::OrderBook::new`] and its
/// friends.
#[derive(Debug, Default, Clone, Copy)]
pub struct MonotonicClock;

impl Clock for MonotonicClock {
    #[inline]
    fn now_millis(&self) -> TimestampMs {
        TimestampMs::new(crate::utils::current_time_millis())
    }
}

/// Deterministic stub clock. Each call to [`Clock::now_millis`] advances
/// an internal counter by `step` (default `1` millisecond). Intended for
/// sequencer replay, proptests, and snapshot tests that require
/// byte-identical timestamps across runs.
#[derive(Debug)]
pub struct StubClock {
    counter: AtomicU64,
    step: u64,
}

impl StubClock {
    /// Create a new stub clock starting at `0` with a step of `1` ms.
    #[must_use]
    pub fn new() -> Self {
        Self {
            counter: AtomicU64::new(0),
            step: 1,
        }
    }

    /// Create a new stub clock starting at `start` with a step of `1` ms.
    #[must_use]
    pub fn starting_at(start: u64) -> Self {
        Self {
            counter: AtomicU64::new(start),
            step: 1,
        }
    }

    /// Create a new stub clock starting at `start` with a custom step.
    ///
    /// Each call to [`Clock::now_millis`] advances the counter by `step`.
    /// A `step` of `0` is silently clamped to `1` so the clock stays
    /// strictly monotonic — the trait contract every caller relies on.
    #[must_use]
    pub fn with_step(start: u64, step: u64) -> Self {
        Self {
            counter: AtomicU64::new(start),
            step: step.max(1),
        }
    }

    /// Current counter value without advancing. Test-only helper.
    #[must_use]
    pub fn peek(&self) -> u64 {
        self.counter.load(Ordering::Relaxed)
    }
}

impl Default for StubClock {
    fn default() -> Self {
        Self::new()
    }
}

impl Clock for StubClock {
    #[inline]
    fn now_millis(&self) -> TimestampMs {
        let v = self.counter.fetch_add(self.step, Ordering::Relaxed);
        TimestampMs::new(v)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;
    use std::sync::Arc;
    use std::thread;

    #[test]
    fn test_monotonic_clock_returns_nonzero_ms() {
        let clock = MonotonicClock;
        let ts = clock.now_millis();
        assert!(ts.as_u64() > 0, "wall-clock must be after the epoch");
    }

    #[test]
    fn test_stub_clock_starts_at_zero_and_advances_by_one() {
        let clock = StubClock::new();
        assert_eq!(clock.now_millis().as_u64(), 0);
        assert_eq!(clock.now_millis().as_u64(), 1);
        assert_eq!(clock.now_millis().as_u64(), 2);
    }

    #[test]
    fn test_stub_clock_with_step() {
        let clock = StubClock::with_step(100, 5);
        assert_eq!(clock.now_millis().as_u64(), 100);
        assert_eq!(clock.now_millis().as_u64(), 105);
        assert_eq!(clock.now_millis().as_u64(), 110);
    }

    #[test]
    fn test_stub_clock_peek_does_not_advance() {
        let clock = StubClock::starting_at(42);
        assert_eq!(clock.peek(), 42);
        assert_eq!(clock.peek(), 42);
        let first = clock.now_millis();
        assert_eq!(first.as_u64(), 42);
        assert_eq!(clock.peek(), 43);
    }

    #[test]
    fn test_stub_clock_concurrent_advance_is_monotonic() {
        // 4 threads x 1000 calls each, step = 1, start = 0.
        // Collect every returned value across every thread; the full set
        // must contain exactly 4000 unique values and the max must equal
        // 4000 * step - 1 + start = 3999.
        let start: u64 = 0;
        let step: u64 = 1;
        let threads = 4usize;
        let per_thread = 1000usize;
        let total = threads.checked_mul(per_thread).expect("overflow");

        let clock = Arc::new(StubClock::with_step(start, step));
        let mut handles = Vec::with_capacity(threads);
        for _ in 0..threads {
            let c = Arc::clone(&clock);
            handles.push(thread::spawn(move || {
                let mut local = Vec::with_capacity(per_thread);
                for _ in 0..per_thread {
                    local.push(c.now_millis().as_u64());
                }
                local
            }));
        }

        let mut all: Vec<u64> = Vec::with_capacity(total);
        for h in handles {
            let part = h.join().expect("thread panicked");
            all.extend(part);
        }

        assert_eq!(all.len(), total);
        let set: HashSet<u64> = all.iter().copied().collect();
        assert_eq!(
            set.len(),
            total,
            "expected every observed tick to be unique"
        );

        let expected_max = start
            .checked_add(
                (total as u64)
                    .checked_mul(step)
                    .and_then(|v| v.checked_sub(1))
                    .expect("overflow"),
            )
            .expect("overflow");
        let observed_max = all.iter().copied().max().expect("non-empty");
        assert_eq!(observed_max, expected_max);
    }

    #[test]
    fn test_stub_clock_with_step_zero_clamps_to_one() {
        // `step = 0` would break the trait's monotonicity contract; the
        // constructor clamps to 1 so every call still advances.
        let clock = StubClock::with_step(10, 0);
        assert_eq!(clock.now_millis().as_u64(), 10);
        assert_eq!(clock.now_millis().as_u64(), 11);
        assert_eq!(clock.now_millis().as_u64(), 12);
    }
}