Skip to main content

rustrade_risk/
clock.rs

1//! Injectable clock abstraction for time-dependent risk primitives.
2//!
3//! [`CircuitBreaker`](crate::CircuitBreaker) and
4//! [`SessionPnl`](crate::SessionPnl) both depend on wall-clock time —
5//! the breaker's rolling loss window, the PnL session's 00:00 UTC
6//! rollover. Real code uses the [`SystemClock`]; tests can substitute
7//! [`ManualClock`] (or any other [`Clock`] impl) so they don't have to
8//! sleep for hours to exercise rollover boundaries.
9//!
10//! ```
11//! use std::sync::Arc;
12//! use rustrade_risk::clock::{Clock, ManualClock};
13//!
14//! let clock = Arc::new(ManualClock::new(1_000));
15//! assert_eq!(clock.now_unix_secs(), 1_000);
16//! clock.advance_secs(86_400);
17//! assert_eq!(clock.now_unix_secs(), 87_400);
18//! ```
19
20use std::sync::Arc;
21use std::sync::atomic::{AtomicU64, Ordering};
22use std::time::{SystemTime, UNIX_EPOCH};
23
24/// Source of wall-clock time, in whole UNIX seconds.
25///
26/// The trait is intentionally minimal: every risk primitive in this crate
27/// needs seconds-since-epoch and the derived day number, and nothing else.
28///
29/// `Debug` is a supertrait so types that store an `Arc<dyn Clock>` (like
30/// [`CircuitBreaker`](crate::CircuitBreaker) and
31/// [`SessionPnl`](crate::SessionPnl)) can still `#[derive(Debug)]`.
32///
33/// # Example
34///
35/// A frozen clock — useful when a test wants every primitive to see the
36/// exact same time without any drift between calls.
37///
38/// ```
39/// use rustrade_risk::clock::Clock;
40///
41/// #[derive(Debug)]
42/// struct FrozenClock(u64);
43///
44/// impl Clock for FrozenClock {
45///     fn now_unix_secs(&self) -> u64 { self.0 }
46/// }
47///
48/// let clock = FrozenClock(1_700_000_000);
49/// assert_eq!(clock.now_unix_secs(), 1_700_000_000);
50/// assert_eq!(clock.utc_day_number(), 1_700_000_000 / 86_400);
51/// ```
52pub trait Clock: std::fmt::Debug + Send + Sync + 'static {
53    /// Current time in seconds since the UNIX epoch.
54    fn now_unix_secs(&self) -> u64;
55
56    /// Whole days since the UNIX epoch. Used by
57    /// [`SessionPnl`](crate::SessionPnl) to detect 00:00 UTC rollover.
58    fn utc_day_number(&self) -> u64 {
59        self.now_unix_secs() / 86_400
60    }
61}
62
63/// The default clock — reads the OS wall clock via [`SystemTime`].
64///
65/// All production code uses this.
66#[derive(Debug, Default, Clone, Copy)]
67pub struct SystemClock;
68
69impl Clock for SystemClock {
70    fn now_unix_secs(&self) -> u64 {
71        SystemTime::now()
72            .duration_since(UNIX_EPOCH)
73            .expect("system clock is before UNIX epoch")
74            .as_secs()
75    }
76}
77
78/// A manually-advanceable clock for tests.
79///
80/// Stored as an `AtomicU64` so it's `Send + Sync` and cheap to clone via
81/// `Arc<ManualClock>`. Both [`Self::set`] and [`Self::advance_secs`] are
82/// available; pick whichever reads better at the call site.
83#[derive(Debug, Default)]
84pub struct ManualClock {
85    now: AtomicU64,
86}
87
88impl ManualClock {
89    /// Create a clock starting at the given UNIX seconds.
90    pub fn new(start_unix_secs: u64) -> Self {
91        Self {
92            now: AtomicU64::new(start_unix_secs),
93        }
94    }
95
96    /// Set the clock to an absolute time.
97    pub fn set(&self, unix_secs: u64) {
98        self.now.store(unix_secs, Ordering::SeqCst);
99    }
100
101    /// Move the clock forward by `secs`. Returns the new time.
102    pub fn advance_secs(&self, secs: u64) -> u64 {
103        self.now.fetch_add(secs, Ordering::SeqCst) + secs
104    }
105}
106
107impl Clock for ManualClock {
108    fn now_unix_secs(&self) -> u64 {
109        self.now.load(Ordering::SeqCst)
110    }
111}
112
113/// `Arc<ManualClock>` implements [`Clock`] too — so callers can share a
114/// single clock between a test harness and the risk primitive under test
115/// without losing the manual-advance methods.
116impl<C: Clock + ?Sized> Clock for Arc<C> {
117    fn now_unix_secs(&self) -> u64 {
118        (**self).now_unix_secs()
119    }
120    fn utc_day_number(&self) -> u64 {
121        (**self).utc_day_number()
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn system_clock_advances() {
131        let c = SystemClock;
132        let t1 = c.now_unix_secs();
133        std::thread::sleep(std::time::Duration::from_millis(1100));
134        let t2 = c.now_unix_secs();
135        assert!(t2 > t1, "expected clock to advance, t1={t1} t2={t2}");
136    }
137
138    #[test]
139    fn manual_clock_set_and_advance() {
140        let c = ManualClock::new(100);
141        assert_eq!(c.now_unix_secs(), 100);
142
143        let after = c.advance_secs(50);
144        assert_eq!(after, 150);
145        assert_eq!(c.now_unix_secs(), 150);
146
147        c.set(1_000);
148        assert_eq!(c.now_unix_secs(), 1_000);
149    }
150
151    #[test]
152    fn manual_clock_default_day_number() {
153        let c = ManualClock::new(86_400 * 3 + 42);
154        assert_eq!(c.utc_day_number(), 3);
155    }
156
157    #[test]
158    fn arc_clock_delegates() {
159        let c = Arc::new(ManualClock::new(500));
160        assert_eq!(c.now_unix_secs(), 500);
161        c.advance_secs(1);
162        // Calling Clock methods on the Arc directly:
163        assert_eq!(Clock::now_unix_secs(&c), 501);
164    }
165}