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}