Skip to main content

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}