rustrade_execution/client/venue_clock.rs
1//! An estimate of a venue's clock, read from the times the venue stamps on its events.
2//!
3//! A venue's timestamps and this host's wall clock differ by an unknown amount that changes: the
4//! host clock can be skewed, drift, or step, as on an NTP step or a manual change. A span read
5//! from the venue by time, such as the fills missed while a stream was down, is a span on the
6//! venue's clock, so a bound taken from the host's wall clock can miss part of it.
7//!
8//! [`VenueClock`] does not read the host's wall clock. It keeps one time the venue stamped,
9//! paired with the moment the event arrived on tokio's monotonic clock, and counts on from there
10//! on that clock, which a step of the wall clock does not move.
11//!
12//! A read of a span the venue stamped, such as the fills missed while a stream was down, is
13//! placed with [`VenueClock::span_start`] and [`VenueClock::span_end`], from the [`HostMoment`]s
14//! that bound it on this host.
15
16use chrono::{DateTime, TimeDelta, Utc};
17use std::time::Duration;
18use tokio::time::Instant;
19
20/// The most the monotonic clock is assumed to run fast or slow against the venue's: 1 part in
21/// this many, 100 ppm. An undisciplined quartz oscillator stays within about 50 ppm, and a clock
22/// NTP disciplines much closer, so this is a wide margin. It costs a read that starts 0.36 s
23/// earlier for each hour since the sample.
24const DRIFT_DIVISOR: u32 = 10_000;
25
26/// The earliest a venue's clock can read at a given moment, from the times it stamped on events
27/// this host received.
28///
29/// Each [`observe`](Self::observe) gives a stamp and the [`Instant`] its event arrived. The event
30/// was stamped before it arrived, so at that instant the venue's clock read the stamp or later:
31/// the delay in between only makes the stamp earlier. The clock keeps the one sample that bounds
32/// the venue's time the tightest, allowing for drift since it was taken, and counts on from it on
33/// tokio's clock. An old event delivered late, such as a fill a venue replays when a stream
34/// resubscribes, gives a loose bound and so never displaces a better one.
35///
36/// # Caller obligations
37///
38/// - Observe only times the venue itself stamped. A time filled in from this host's clock, for
39/// an event that arrived without one, is not the venue's: if this host's clock is ahead, it
40/// reads as a venue clock that is ahead too, and every bound taken from it is late. A
41/// [`Trade`](crate::trade::Trade) says which it is in its
42/// [`time_source`](crate::trade::Trade::time_source).
43/// - Pass the instant the event arrived, read from [`Instant::now`]. That is tokio's clock, so a
44/// test runtime with a paused clock controls it.
45///
46/// # Known limitations
47///
48/// - A stamp the venue sends from ahead of its own clock, such as one read in the wrong time
49/// zone, is kept: it bounds the venue's time later than it is until a later sample bounds it
50/// later still. Compare the result with this host's clock (see
51/// [`host_lead`](Self::host_lead)) to see such a stamp.
52/// - A venue clock that steps back is followed only as fast as the drift allowance lets the old
53/// sample go, about 0.36 s an hour.
54/// - Before the first sample there is no bound: [`earliest_at`](Self::earliest_at) is `None`.
55/// - The monotonic clock is not free of the host's time keeping. On some platforms, such as
56/// Linux, it does not advance while the host is suspended, which makes the bound earlier than
57/// it could be: safe for a read's start, but no help for its end. NTP can also slew it, up to
58/// 500 ppm or more while it corrects an offset, beyond the drift allowance. A slew that speeds it up corrects a wall clock that is
59/// behind, so a caller that also bounds a read's start by the wall clock, taking the earlier of
60/// the two, stays covered.
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
62pub struct VenueClock {
63 sample: Option<Sample>,
64}
65
66/// A moment on this host: the time its wall clock read then, and the same moment on tokio's
67/// monotonic clock.
68///
69/// The instant lets a span bounded by the moment be counted on from it on the monotonic clock, so
70/// a step of the wall clock after it does not move the span (see [`VenueClock::span_end`]).
71#[non_exhaustive]
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct HostMoment {
74 /// This host's wall-clock time at the moment.
75 pub time: DateTime<Utc>,
76 /// The same moment on tokio's clock.
77 pub instant: Instant,
78}
79
80impl HostMoment {
81 /// The moment this host's wall clock read `time` and tokio's clock read `instant`.
82 pub const fn new(time: DateTime<Utc>, instant: Instant) -> Self {
83 Self { time, instant }
84 }
85
86 /// Now, by [`Utc::now`] and [`Instant::now`].
87 pub fn now() -> Self {
88 Self::new(Utc::now(), Instant::now())
89 }
90}
91
92/// A time the venue stamped and the moment its event arrived.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94struct Sample {
95 stamp: DateTime<Utc>,
96 received: Instant,
97}
98
99impl VenueClock {
100 /// A clock with no sample yet.
101 pub const fn new() -> Self {
102 Self { sample: None }
103 }
104
105 /// Records `stamp`, a time the venue stamped on an event that arrived at `received`.
106 ///
107 /// Kept when it bounds the venue's time at `received` later than the sample held, and
108 /// otherwise dropped.
109 pub fn observe(&mut self, stamp: DateTime<Utc>, received: Instant) {
110 if self
111 .earliest_at(received)
112 .is_none_or(|earliest| stamp > earliest)
113 {
114 self.sample = Some(Sample { stamp, received });
115 }
116 }
117
118 /// Records the sample `other` holds, as [`observe`](Self::observe) would, so that this clock
119 /// learns from another source of the same venue's times, such as a read of its server time.
120 #[cfg(feature = "binance")]
121 pub(crate) fn merge(&mut self, other: &VenueClock) {
122 if let Some(Sample { stamp, received }) = other.sample {
123 self.observe(stamp, received);
124 }
125 }
126
127 /// The earliest the venue's clock can read at `at`, or `None` before any sample.
128 ///
129 /// Counts from the sample on tokio's clock, less the drift allowance, so the venue's clock
130 /// reads this or later at `at`. `at` may be before the sample.
131 pub fn earliest_at(&self, at: Instant) -> Option<DateTime<Utc>> {
132 let Sample { stamp, received } = self.sample?;
133 Some(if at >= received {
134 let elapsed = at - received;
135 to_delta(elapsed - elapsed / DRIFT_DIVISOR)
136 .and_then(|delta| stamp.checked_add_signed(delta))
137 // Beyond chrono's range: the sample's own stamp is still a bound.
138 .unwrap_or(stamp)
139 } else {
140 let before = received - at;
141 to_delta(before.saturating_add(before / DRIFT_DIVISOR))
142 .and_then(|delta| stamp.checked_sub_signed(delta))
143 .unwrap_or(DateTime::<Utc>::MIN_UTC)
144 })
145 }
146
147 /// The most this host's clock can lead the venue's, given it read `host_time` at `at`, or
148 /// `None` before any sample. Negative when this host's clock is behind the venue's.
149 ///
150 /// An upper bound: it is the true lead plus the delay of the sample held and the drift
151 /// allowed for since. A large negative value says the venue's clock is ahead, or that a stamp
152 /// was (see the type's known limitations).
153 pub fn host_lead(&self, host_time: DateTime<Utc>, at: Instant) -> Option<TimeDelta> {
154 self.earliest_at(at)
155 .map(|earliest| host_time.signed_duration_since(earliest))
156 }
157
158 /// Where a read of what the venue stamped from `moment` on starts: the earliest the venue's
159 /// clock can read at `moment`, or this host's time then if that is earlier, as on a host clock
160 /// behind the venue's. `None` before any sample, when the caller has to allow for the
161 /// difference between the clocks itself.
162 ///
163 /// A host clock ahead of the venue's does not make the start late, and one behind it costs a
164 /// read that starts that much earlier. Less a caller's own margin, for the delay between an
165 /// event and its arrival, this is where a read of what was missed after `moment` begins.
166 pub fn span_start(&self, moment: HostMoment) -> Option<DateTime<Utc>> {
167 self.earliest_at(moment.instant)
168 .map(|venue| venue.min(moment.time))
169 }
170
171 /// Where a read that has to reach `now` ends, for a span that began at `since`: the latest of
172 /// this host's time at `now`, the earliest the venue's clock can read then, and `since`'s host
173 /// time counted on to `now` on tokio's clock. Before any sample the venue's time is left out.
174 ///
175 /// So neither a host clock behind the venue's nor one stepped back since `since` ends the read
176 /// early. The venue's clock can still read later than this, as an estimate is a lower bound:
177 /// add a margin for that.
178 pub fn span_end(&self, since: HostMoment, now: HostMoment) -> DateTime<Utc> {
179 let counted_on = to_delta(now.instant.saturating_duration_since(since.instant))
180 .and_then(|delta| since.time.checked_add_signed(delta))
181 .unwrap_or(DateTime::<Utc>::MAX_UTC);
182 let venue = self.earliest_at(now.instant).unwrap_or(now.time);
183 venue.max(now.time).max(counted_on)
184 }
185}
186
187/// `duration` as a [`TimeDelta`], or `None` beyond its range.
188fn to_delta(duration: Duration) -> Option<TimeDelta> {
189 TimeDelta::from_std(duration).ok()
190}
191
192#[cfg(test)]
193#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
194mod tests {
195 use super::*;
196
197 fn at(secs: i64) -> DateTime<Utc> {
198 DateTime::<Utc>::from_timestamp(1_800_000_000 + secs, 0).unwrap()
199 }
200
201 fn secs(secs: u64) -> Duration {
202 Duration::from_secs(secs)
203 }
204
205 #[test]
206 fn a_clock_with_no_sample_gives_no_bound() {
207 let clock = VenueClock::new();
208
209 assert_eq!(clock.earliest_at(Instant::now()), None);
210 assert_eq!(clock.host_lead(at(0), Instant::now()), None);
211 }
212
213 #[test]
214 fn the_bound_counts_on_from_the_sample_less_the_drift_allowance() {
215 let base = Instant::now();
216 let mut clock = VenueClock::new();
217 clock.observe(at(100), base);
218
219 assert_eq!(clock.earliest_at(base), Some(at(100)));
220 // 10 000 s on, less 1 s of drift at 100 ppm.
221 assert_eq!(clock.earliest_at(base + secs(10_000)), Some(at(10_099)));
222 }
223
224 #[test]
225 fn the_bound_before_the_sample_counts_back_with_the_drift_allowance_added() {
226 let base = Instant::now();
227 let mut clock = VenueClock::new();
228 clock.observe(at(20_000), base + secs(10_000));
229
230 // 10 000 s back, plus 1 s of drift at 100 ppm.
231 assert_eq!(clock.earliest_at(base), Some(at(9_999)));
232 }
233
234 #[test]
235 fn a_sample_that_bounds_the_venue_later_replaces_the_one_held() {
236 let base = Instant::now();
237 let mut clock = VenueClock::new();
238 clock.observe(at(100), base);
239 // Stamped 10 s later and arrived 1 ms sooner: later than the held sample allows for.
240 let received = base + secs(10) - Duration::from_millis(1);
241 clock.observe(at(110), received);
242
243 assert_eq!(clock.earliest_at(received), Some(at(110)));
244 }
245
246 #[test]
247 fn a_late_old_event_does_not_displace_a_tighter_sample() {
248 let base = Instant::now();
249 let mut clock = VenueClock::new();
250 clock.observe(at(100), base);
251 // A replayed fill from an hour ago, arriving now.
252 clock.observe(at(100 - 3_600), base + secs(1));
253
254 assert_eq!(clock.earliest_at(base), Some(at(100)));
255 }
256
257 #[test]
258 fn a_sample_equal_to_the_bound_held_keeps_the_older_one() {
259 let base = Instant::now();
260 let mut clock = VenueClock::new();
261 clock.observe(at(100), base);
262 // Exactly the held sample's bound 10 000 s on: 10 000 s less 1 s of drift.
263 clock.observe(at(10_099), base + secs(10_000));
264
265 // The held sample still bounds the time it arrived at; the new one would bound it 2 s
266 // earlier, counting back with the drift allowance added.
267 assert_eq!(clock.earliest_at(base), Some(at(100)));
268 }
269
270 #[test]
271 #[cfg(feature = "binance")]
272 fn a_merged_clock_keeps_whichever_sample_bounds_the_venue_later() {
273 let base = Instant::now();
274 let mut tighter = VenueClock::new();
275 tighter.observe(at(110), base);
276 let mut looser = VenueClock::new();
277 looser.observe(at(100), base);
278
279 let mut clock = looser;
280 clock.merge(&tighter);
281 assert_eq!(clock.earliest_at(base), Some(at(110)));
282
283 let mut clock = tighter;
284 clock.merge(&looser);
285 clock.merge(&VenueClock::new());
286 assert_eq!(clock.earliest_at(base), Some(at(110)));
287 }
288
289 #[test]
290 fn a_span_with_no_sample_has_no_start_and_ends_at_the_later_host_time() {
291 let base = Instant::now();
292 let clock = VenueClock::new();
293
294 assert_eq!(clock.span_start(HostMoment::new(at(100), base)), None);
295 // 10 s on by tokio's clock, but the wall clock stepped back an hour.
296 let since = HostMoment::new(at(100), base);
297 let now = HostMoment::new(at(110 - 3_600), base + secs(10));
298 assert_eq!(clock.span_end(since, now), at(110));
299 }
300
301 #[test]
302 fn a_span_from_a_host_clock_ahead_of_the_venue_starts_at_the_venues_time() {
303 let base = Instant::now();
304 let mut clock = VenueClock::new();
305 clock.observe(at(100), base);
306
307 // The host reads 60 s ahead of the venue.
308 assert_eq!(
309 clock.span_start(HostMoment::new(at(160), base)),
310 Some(at(100))
311 );
312 }
313
314 #[test]
315 fn a_span_from_a_host_clock_behind_the_venue_starts_at_the_hosts_time() {
316 let base = Instant::now();
317 let mut clock = VenueClock::new();
318 clock.observe(at(100), base);
319
320 assert_eq!(
321 clock.span_start(HostMoment::new(at(40), base)),
322 Some(at(40))
323 );
324 }
325
326 #[test]
327 fn a_span_ends_at_the_latest_of_the_host_the_venue_and_the_start_counted_on() {
328 let base = Instant::now();
329 let mut clock = VenueClock::new();
330 clock.observe(at(100), base);
331 let since = HostMoment::new(at(100), base);
332
333 // A host clock behind the venue's: the venue's time, 10 s on.
334 let behind = HostMoment::new(at(50), base + secs(10));
335 assert_eq!(clock.span_end(since, behind), at(110));
336 // A host clock ahead of both: its own time.
337 let ahead = HostMoment::new(at(200), base + secs(10));
338 assert_eq!(clock.span_end(since, ahead), at(200));
339 }
340
341 #[test]
342 fn a_span_end_counts_on_from_its_start_when_the_venue_estimate_is_earlier() {
343 let base = Instant::now();
344 let mut clock = VenueClock::new();
345 // A venue 50 s behind the host's start, and a host clock stepped back since.
346 clock.observe(at(50), base);
347 let since = HostMoment::new(at(100), base);
348 let now = HostMoment::new(at(0), base + secs(10));
349
350 assert_eq!(clock.span_end(since, now), at(110));
351 }
352
353 #[test]
354 fn the_host_lead_is_the_host_time_less_the_bound() {
355 let base = Instant::now();
356 let mut clock = VenueClock::new();
357 clock.observe(at(100), base);
358
359 assert_eq!(clock.host_lead(at(160), base), Some(TimeDelta::seconds(60)));
360 assert_eq!(clock.host_lead(at(40), base), Some(TimeDelta::seconds(-60)));
361 }
362}