Skip to main content

rustrade_data/books/
mod.rs

1use crate::subscription::book::OrderBookEvent;
2use chrono::{DateTime, Utc};
3use derive_more::Display;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::cmp::Ordering;
7use tracing::debug;
8
9/// Provides a [`OrderBookL2Manager`](manager::OrderBookL2Manager) for maintaining a set of local
10/// L2 [`OrderBook`]s.
11pub mod manager;
12
13/// Provides an abstract collection of cheaply cloneable shared-state [`OrderBook`].
14pub mod map;
15
16/// Venue + local timestamps for an [`OrderBook`] revision.
17///
18/// Serves double duty as both the constructor argument
19/// ([`OrderBook::new`]/[`OrderBook::from_sides`]) and the stored field, so
20/// `update`/`snapshot` are one-line copies. The named fields prevent
21/// transposition of the two same-typed `Option<DateTime<Utc>>` times.
22///
23/// Each timestamp carries exactly one honest meaning; staleness *policy* is left
24/// to the consumer (compose over the accessors).
25#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize)]
26pub struct OrderBookTimes {
27    /// Matching-engine time (`"T"` on Binance futures). `None` when the venue
28    /// doesn't supply it (Binance spot, IBKR, ...). Latency/engine audit — not a
29    /// liveness signal.
30    pub time_engine: Option<DateTime<Utc>>,
31    /// Venue event/broadcast time (`"E"` on Binance, `ts` on Bybit). `None` when
32    /// the venue supplies none (IBKR; Binance spot REST seed). Feed-lag-aware
33    /// liveness where present.
34    pub time_exchange: Option<DateTime<Utc>>,
35    /// Local ingestion wall-clock — ALWAYS present once a revision is applied.
36    /// Universal liveness floor; use when `time_exchange` is `None` (e.g. IBKR).
37    ///
38    /// NOTE: a default/pre-population book ([`OrderBook::default`], used as the
39    /// manager placeholder before the first event) carries the epoch (1970) here
40    /// — desirable for a liveness floor (reads as stale → consumer fail-closes
41    /// until the first revision), but it is NOT a real ingestion time.
42    pub time_received: DateTime<Utc>,
43}
44
45/// Normalised Barter [`OrderBook`] snapshot.
46///
47/// ### Equality
48/// `PartialEq`/`Eq` is derived over **all** fields, including the [`OrderBookTimes`]
49/// timestamps. Because `time_received` is stamped per construction, two
50/// content-identical books observed at different instants compare **unequal**. For
51/// content comparison use the accessors (`sequence()`/`bids()`/`asks()`).
52#[derive(Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize)]
53pub struct OrderBook {
54    sequence: u64,
55    times: OrderBookTimes,
56    bids: OrderBookSide<Bids>,
57    asks: OrderBookSide<Asks>,
58}
59
60impl OrderBook {
61    /// Construct a new sorted [`OrderBook`].
62    ///
63    /// Note that the passed bid and asks levels do not need to be pre-sorted.
64    pub fn new<IterBids, IterAsks, L>(
65        sequence: u64,
66        times: OrderBookTimes,
67        bids: IterBids,
68        asks: IterAsks,
69    ) -> Self
70    where
71        IterBids: IntoIterator<Item = L>,
72        IterAsks: IntoIterator<Item = L>,
73        L: Into<Level>,
74    {
75        Self {
76            sequence,
77            times,
78            bids: OrderBookSide::bids(bids),
79            asks: OrderBookSide::asks(asks),
80        }
81    }
82
83    /// Construct an [`OrderBook`] from pre-sorted [`OrderBookSide`]s.
84    ///
85    /// Use this when you already have sorted sides to avoid re-sorting overhead.
86    /// Caller must ensure sides are correctly sorted (bids descending, asks ascending).
87    pub fn from_sides(
88        sequence: u64,
89        times: OrderBookTimes,
90        bids: OrderBookSide<Bids>,
91        asks: OrderBookSide<Asks>,
92    ) -> Self {
93        debug_assert!(
94            bids.levels().windows(2).all(|w| w[0].price >= w[1].price),
95            "bids must be sorted descending by price"
96        );
97        debug_assert!(
98            asks.levels().windows(2).all(|w| w[0].price <= w[1].price),
99            "asks must be sorted ascending by price"
100        );
101        Self {
102            sequence,
103            times,
104            bids,
105            asks,
106        }
107    }
108
109    /// Current `u64` sequence number associated with the [`OrderBook`].
110    pub fn sequence(&self) -> u64 {
111        self.sequence
112    }
113
114    /// Matching-engine time associated with the [`OrderBook`] (`"T"` on Binance
115    /// futures).
116    ///
117    /// `None` when the venue doesn't supply matching-engine time (Binance spot,
118    /// IBKR, ...). This is for latency / engine audit — **not** a liveness signal;
119    /// use [`OrderBook::time_exchange`]/[`OrderBook::time_received`] for staleness.
120    pub fn time_engine(&self) -> Option<DateTime<Utc>> {
121        self.times.time_engine
122    }
123
124    /// Venue event/broadcast time associated with the [`OrderBook`] (`"E"` on
125    /// Binance, `ts` on Bybit).
126    ///
127    /// Feed-lag-aware liveness where present: `now - time_exchange` catches data
128    /// that is old despite being just received. `None` when the venue supplies no
129    /// broadcast timestamp (IBKR; Binance spot REST seed before the first diff).
130    /// `None` is a capability signal, not a defect — fall back to
131    /// [`OrderBook::time_received`].
132    ///
133    /// Note the asymmetry with `MarketEvent::time_exchange` (non-`Option`, with a
134    /// local fallback): here `None` means "the venue gave nothing".
135    pub fn time_exchange(&self) -> Option<DateTime<Utc>> {
136        self.times.time_exchange
137    }
138
139    /// Local ingestion wall-clock associated with the [`OrderBook`].
140    ///
141    /// The **universal liveness floor** — always present once a revision is
142    /// applied, on every venue (including IBKR, where it is the only liveness
143    /// signal). Skew-immune: `now - time_received` is a same-clock comparison.
144    /// Prefer it as the fallback when [`OrderBook::time_exchange`] is `None`.
145    ///
146    /// A default/pre-population book ([`OrderBook::default`]) reports the epoch
147    /// (1970) here, so it reads as stale until the first revision — the intended
148    /// liveness-floor behaviour.
149    pub fn time_received(&self) -> DateTime<Utc> {
150        self.times.time_received
151    }
152
153    /// All revision timestamps for this [`OrderBook`] as a single
154    /// [`OrderBookTimes`].
155    ///
156    /// Convenience over the individual `time_engine`/`time_exchange`/
157    /// `time_received` accessors for forwarding the whole set in one (`Copy`) move
158    /// — e.g. reconstructing a book that shares this revision's times.
159    pub fn times(&self) -> OrderBookTimes {
160        self.times
161    }
162
163    /// Generate a sorted [`OrderBook`] snapshot with a maximum depth.
164    ///
165    /// The returned snapshot carries the same [`OrderBookTimes`] as the source
166    /// book (timestamps are not reset).
167    pub fn snapshot(&self, depth: usize) -> Self {
168        Self {
169            sequence: self.sequence,
170            times: self.times,
171            bids: OrderBookSide::bids(self.bids.levels.iter().take(depth).copied()),
172            asks: OrderBookSide::asks(self.asks.levels.iter().take(depth).copied()),
173        }
174    }
175
176    /// Update the local [`OrderBook`] from a new [`OrderBookEvent`].
177    ///
178    /// `Update` advances the book's [`OrderBookTimes`] wholesale (alongside
179    /// upserting the changed levels); `Snapshot` replaces the book in its
180    /// entirety, including its times.
181    pub fn update(&mut self, event: &OrderBookEvent) {
182        match event {
183            OrderBookEvent::Snapshot(snapshot) => {
184                *self = snapshot.clone();
185            }
186            OrderBookEvent::Update(update) => {
187                self.sequence = update.sequence;
188                self.times = update.times;
189                self.upsert_bids(&update.bids);
190                self.upsert_asks(&update.asks);
191            }
192        }
193    }
194
195    /// Update the local [`OrderBook`] by upserting the levels in an [`OrderBookSide`].
196    fn upsert_bids(&mut self, update: &OrderBookSide<Bids>) {
197        self.bids.upsert(&update.levels)
198    }
199
200    /// Update the local [`OrderBook`] by upserting the levels in an [`OrderBookSide`].
201    fn upsert_asks(&mut self, update: &OrderBookSide<Asks>) {
202        self.asks.upsert(&update.levels)
203    }
204
205    /// Return a reference to this [`OrderBook`]s bids.
206    pub fn bids(&self) -> &OrderBookSide<Bids> {
207        &self.bids
208    }
209
210    /// Return a reference to this [`OrderBook`]s asks.
211    pub fn asks(&self) -> &OrderBookSide<Asks> {
212        &self.asks
213    }
214
215    /// Calculate the mid-price by taking the average of the best bid and ask prices.
216    ///
217    /// See Docs: <https://www.quantstart.com/articles/high-frequency-trading-ii-limit-order-book>
218    pub fn mid_price(&self) -> Option<Decimal> {
219        match (self.bids.best(), self.asks.best()) {
220            (Some(best_bid), Some(best_ask)) => Some(mid_price(best_bid.price, best_ask.price)),
221            (Some(best_bid), None) => Some(best_bid.price),
222            (None, Some(best_ask)) => Some(best_ask.price),
223            (None, None) => None,
224        }
225    }
226
227    /// Calculate the volume weighted mid-price (micro-price), weighing the best bid and ask prices
228    /// with their associated amount.
229    ///
230    /// `None` when the book is empty, and — for a two-sided book — when both best levels carry a
231    /// zero amount, because the weighting is undefined there. See [`volume_weighted_mid_price`].
232    pub fn volume_weighed_mid_price(&self) -> Option<Decimal> {
233        match (self.bids.best(), self.asks.best()) {
234            (Some(best_bid), Some(best_ask)) => volume_weighted_mid_price(*best_bid, *best_ask),
235            (Some(best_bid), None) => Some(best_bid.price),
236            (None, Some(best_ask)) => Some(best_ask.price),
237            (None, None) => None,
238        }
239    }
240}
241
242/// Normalised Barter [`Level`]s for one `Side` of the [`OrderBook`].
243#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
244pub struct OrderBookSide<Side> {
245    #[serde(skip_serializing)]
246    pub side: Side,
247    levels: Vec<Level>,
248}
249
250/// Unit type to tag an [`OrderBookSide`] as the bid Side (ie/ buyers) of an [`OrderBook`].
251#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Display)]
252pub struct Bids;
253
254/// Unit type to tag an [`OrderBookSide`] as the ask Side (ie/ sellers) of an [`OrderBook`].
255#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Display)]
256pub struct Asks;
257
258impl OrderBookSide<Bids> {
259    /// Construct a new [`OrderBookSide<Bids>`] from the provided [`Level`]s.
260    pub fn bids<Iter, L>(levels: Iter) -> Self
261    where
262        Iter: IntoIterator<Item = L>,
263        L: Into<Level>,
264    {
265        let mut levels = levels.into_iter().map(L::into).collect::<Vec<_>>();
266        levels.sort_unstable_by(|a, b| a.price.cmp(&b.price).reverse());
267
268        Self { side: Bids, levels }
269    }
270
271    /// Upsert bid [`Level`]s into this [`OrderBookSide<Bids>`].
272    pub fn upsert<L>(&mut self, levels: &[L])
273    where
274        L: Into<Level> + Copy,
275    {
276        levels.iter().for_each(|upsert| {
277            let upsert: Level = (*upsert).into();
278            self.upsert_single(upsert, |existing| {
279                existing.price.cmp(&upsert.price).reverse()
280            })
281        })
282    }
283}
284
285impl OrderBookSide<Asks> {
286    /// Construct a new [`OrderBookSide<Asks>`] from the provided [`Level`]s.
287    pub fn asks<Iter, L>(levels: Iter) -> Self
288    where
289        Iter: IntoIterator<Item = L>,
290        L: Into<Level>,
291    {
292        let mut levels = levels.into_iter().map(L::into).collect::<Vec<_>>();
293        levels.sort_unstable_by_key(|a| a.price);
294
295        Self { side: Asks, levels }
296    }
297
298    /// Upsert ask [`Level`]s into this [`OrderBookSide<Asks>`].
299    pub fn upsert<L>(&mut self, levels: &[L])
300    where
301        L: Into<Level> + Copy,
302    {
303        levels.iter().for_each(|upsert| {
304            let upsert = (*upsert).into();
305            self.upsert_single(upsert, |existing| existing.price.cmp(&upsert.price))
306        })
307    }
308}
309
310impl<Side> OrderBookSide<Side>
311where
312    Side: std::fmt::Display + std::fmt::Debug,
313{
314    /// Get best [`Level`] on the [`OrderBookSide`].
315    pub fn best(&self) -> Option<&Level> {
316        self.levels.first()
317    }
318
319    /// Return a reference to the [`OrderBookSide`] levels.
320    pub fn levels(&self) -> &[Level] {
321        &self.levels
322    }
323
324    /// Upsert a single [`Level`] into this [`OrderBookSide`].
325    ///
326    /// ### Upsert Scenarios
327    /// #### 1 Level Already Exists
328    /// 1a) New value is 0, remove the level
329    /// 1b) New value is > 0, replace the level
330    ///
331    /// #### 2 Level Does Not Exist
332    /// 2a) New value is 0, log warn and continue
333    /// 2b) New value is > 0, insert new level
334    pub fn upsert_single<FnOrd>(&mut self, new_level: Level, fn_ord: FnOrd)
335    where
336        FnOrd: Fn(&Level) -> Ordering,
337    {
338        match (self.levels.binary_search_by(fn_ord), new_level.amount) {
339            (Ok(index), new_amount) => {
340                if new_amount.is_zero() {
341                    // Scenario 1a: Level exists & new value is 0 => remove level
342                    let _removed = self.levels.remove(index);
343                } else {
344                    // Scenario 1b: Level exists & new value is > 0 => replace level
345                    self.levels[index].amount = new_amount;
346                }
347            }
348            (Err(index), new_amount) => {
349                if new_amount.is_zero() {
350                    // Scenario 2a: Level does not exist & new value is 0 => log & continue
351                    debug!(
352                        ?new_level,
353                        side = %self.side,
354                        "received upsert Level with zero amount (to remove) that was not found"
355                    );
356                } else {
357                    // Scenario 2b: Level does not exist & new value > 0 => insert new level
358                    self.levels.insert(index, new_level);
359                }
360            }
361        }
362    }
363}
364
365impl Default for OrderBookSide<Bids> {
366    fn default() -> Self {
367        Self {
368            side: Bids,
369            levels: vec![],
370        }
371    }
372}
373
374impl Default for OrderBookSide<Asks> {
375    fn default() -> Self {
376        Self {
377            side: Asks,
378            levels: vec![],
379        }
380    }
381}
382
383/// Normalised Barter OrderBook [`Level`].
384#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Hash, Default, Deserialize, Serialize)]
385pub struct Level {
386    pub price: Decimal,
387    pub amount: Decimal,
388}
389
390impl<T> From<(T, T)> for Level
391where
392    T: Into<Decimal>,
393{
394    fn from((price, amount): (T, T)) -> Self {
395        Self::new(price, amount)
396    }
397}
398
399impl Eq for Level {}
400
401impl Level {
402    pub fn new<T>(price: T, amount: T) -> Self
403    where
404        T: Into<Decimal>,
405    {
406        Self {
407            price: price.into(),
408            amount: amount.into(),
409        }
410    }
411}
412
413/// Calculate the mid-price by taking the average of the best bid and ask prices.
414///
415/// See Docs: <https://www.quantstart.com/articles/high-frequency-trading-ii-limit-order-book>
416pub fn mid_price(best_bid_price: Decimal, best_ask_price: Decimal) -> Decimal {
417    (best_bid_price + best_ask_price) / Decimal::TWO
418}
419
420/// Calculate the volume weighted mid-price (micro-price), weighing the best bid and ask prices
421/// with their associated amount.
422///
423/// Returns `None` when the two amounts sum to zero, which is not an edge case to be assumed away: a
424/// feed that publishes prices without sizes (a bulk export, an FX quote tape) yields exactly that
425/// book, and the weighting is undefined for it. `Decimal`'s `Div` **panics** on a zero divisor, so
426/// returning `Option` is what keeps a size-less book from taking down the caller. Use
427/// [`mid_price`] when a price is needed regardless of size.
428///
429/// See Docs: <https://www.quantstart.com/articles/high-frequency-trading-ii-limit-order-book>
430pub fn volume_weighted_mid_price(best_bid: Level, best_ask: Level) -> Option<Decimal> {
431    let total_amount = best_bid.amount + best_ask.amount;
432    if total_amount.is_zero() {
433        return None;
434    }
435
436    Some(((best_bid.price * best_ask.amount) + (best_ask.price * best_bid.amount)) / total_amount)
437}
438
439#[cfg(test)]
440#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
441mod tests {
442    use super::*;
443
444    mod order_book_l1 {
445        use super::*;
446        use crate::subscription::book::OrderBookL1;
447        use rust_decimal_macros::dec;
448
449        #[test]
450        fn test_mid_price() {
451            struct TestCase {
452                input: OrderBookL1,
453                expected: Option<Decimal>,
454            }
455
456            let tests = vec![
457                TestCase {
458                    // TC0
459                    input: OrderBookL1 {
460                        last_update_time: Default::default(),
461                        best_bid: Some(Level::new(100, 999999)),
462                        best_ask: Some(Level::new(200, 1)),
463                    },
464                    expected: Some(dec!(150.0)),
465                },
466                TestCase {
467                    // TC1
468                    input: OrderBookL1 {
469                        last_update_time: Default::default(),
470                        best_bid: Some(Level::new(50, 1)),
471                        best_ask: Some(Level::new(250, 999999)),
472                    },
473                    expected: Some(dec!(150.0)),
474                },
475                TestCase {
476                    // TC2
477                    input: OrderBookL1 {
478                        last_update_time: Default::default(),
479                        best_bid: Some(Level::new(10, 999999)),
480                        best_ask: Some(Level::new(250, 999999)),
481                    },
482                    expected: Some(dec!(130.0)),
483                },
484                TestCase {
485                    // TC3
486                    input: OrderBookL1 {
487                        last_update_time: Default::default(),
488                        best_bid: Some(Level::new(10, 999999)),
489                        best_ask: None,
490                    },
491                    expected: None,
492                },
493                TestCase {
494                    // TC4
495                    input: OrderBookL1 {
496                        last_update_time: Default::default(),
497                        best_bid: None,
498                        best_ask: Some(Level::new(250, 999999)),
499                    },
500                    expected: None,
501                },
502            ];
503
504            for (index, test) in tests.into_iter().enumerate() {
505                assert_eq!(test.input.mid_price(), test.expected, "TC{index} failed")
506            }
507        }
508
509        #[test]
510        fn test_volume_weighted_mid_price() {
511            struct TestCase {
512                input: OrderBookL1,
513                expected: Option<Decimal>,
514            }
515
516            let tests = vec![
517                TestCase {
518                    // TC0: volume the same so should be equal to non-weighted mid price
519                    input: OrderBookL1 {
520                        last_update_time: Default::default(),
521                        best_bid: Some(Level::new(100, 100)),
522                        best_ask: Some(Level::new(200, 100)),
523                    },
524                    expected: Some(dec!(150.0)),
525                },
526                TestCase {
527                    // TC1: volume affects mid-price
528                    input: OrderBookL1 {
529                        last_update_time: Default::default(),
530                        best_bid: Some(Level::new(100, 600)),
531                        best_ask: Some(Level::new(200, 1000)),
532                    },
533                    expected: Some(dec!(137.5)),
534                },
535                TestCase {
536                    // TC2: volume the same and price the same
537                    input: OrderBookL1 {
538                        last_update_time: Default::default(),
539                        best_bid: Some(Level::new(1000, 999999)),
540                        best_ask: Some(Level::new(1000, 999999)),
541                    },
542                    expected: Some(dec!(1000.0)),
543                },
544                TestCase {
545                    // TC3: best ask is None
546                    input: OrderBookL1 {
547                        last_update_time: Default::default(),
548                        best_bid: Some(Level::new(1000, 999999)),
549                        best_ask: None,
550                    },
551                    expected: None,
552                },
553                TestCase {
554                    // TC4: best bid is None
555                    input: OrderBookL1 {
556                        last_update_time: Default::default(),
557                        best_bid: None,
558                        best_ask: Some(Level::new(1000, 999999)),
559                    },
560                    expected: None,
561                },
562                TestCase {
563                    // TC5: two-sided book with NO sizes -- what a prices-only feed publishes. The
564                    // weighting is undefined; `Decimal` division would panic on the zero divisor.
565                    input: OrderBookL1 {
566                        last_update_time: Default::default(),
567                        best_bid: Some(Level::new(100, 0)),
568                        best_ask: Some(Level::new(200, 0)),
569                    },
570                    expected: None,
571                },
572                TestCase {
573                    // TC6: one side sized, the other not -- still weightable, and the sized side
574                    // takes the whole weight.
575                    input: OrderBookL1 {
576                        last_update_time: Default::default(),
577                        best_bid: Some(Level::new(100, 0)),
578                        best_ask: Some(Level::new(200, 50)),
579                    },
580                    expected: Some(dec!(100)),
581                },
582            ];
583
584            for (index, test) in tests.into_iter().enumerate() {
585                assert_eq!(
586                    test.input.volume_weighed_mid_price(),
587                    test.expected,
588                    "TC{index} failed"
589                )
590            }
591        }
592
593        #[test]
594        fn test_zero_size_book_falls_back_to_plain_mid() {
595            // The pairing the engine's price resolution relies on: a prices-only book has no
596            // volume-weighted mid, but its plain mid is well defined.
597            let book = OrderBookL1 {
598                last_update_time: Default::default(),
599                best_bid: Some(Level::new(100, 0)),
600                best_ask: Some(Level::new(200, 0)),
601            };
602
603            assert_eq!(book.volume_weighed_mid_price(), None);
604            assert_eq!(book.mid_price(), Some(dec!(150)));
605        }
606    }
607
608    mod order_book {
609        use super::*;
610        use rust_decimal_macros::dec;
611
612        fn times(received_ms: i64) -> OrderBookTimes {
613            OrderBookTimes {
614                time_engine: Some(DateTime::from_timestamp_millis(1).unwrap()),
615                time_exchange: Some(DateTime::from_timestamp_millis(2).unwrap()),
616                time_received: DateTime::from_timestamp_millis(received_ms).unwrap(),
617            }
618        }
619
620        #[test]
621        fn test_update_replaces_times_wholesale() {
622            let mut book = OrderBook::new(1, times(100), vec![Level::new(50, 1)], vec![]);
623
624            let newer = OrderBookTimes {
625                time_engine: None,
626                time_exchange: Some(DateTime::from_timestamp_millis(999).unwrap()),
627                time_received: DateTime::from_timestamp_millis(1000).unwrap(),
628            };
629            let update = OrderBook::new(2, newer, vec![Level::new(50, 1)], vec![]);
630            book.update(&OrderBookEvent::Update(update));
631
632            assert_eq!(book.time_engine(), newer.time_engine);
633            assert_eq!(book.time_exchange(), newer.time_exchange);
634            assert_eq!(book.time_received(), newer.time_received);
635        }
636
637        #[test]
638        fn test_update_snapshot_propagates_times_wholesale() {
639            // The `Snapshot` arm replaces the live book via `*self = snapshot.clone()`,
640            // so it must carry the snapshot's `times` (not retain the old ones).
641            let mut book = OrderBook::new(1, times(100), vec![Level::new(50, 1)], vec![]);
642
643            let newer = OrderBookTimes {
644                time_engine: None,
645                time_exchange: Some(DateTime::from_timestamp_millis(999).unwrap()),
646                time_received: DateTime::from_timestamp_millis(1000).unwrap(),
647            };
648            let snapshot = OrderBook::new(2, newer, vec![Level::new(60, 1)], vec![]);
649            book.update(&OrderBookEvent::Snapshot(snapshot));
650
651            assert_eq!(book.time_engine(), newer.time_engine);
652            assert_eq!(book.time_exchange(), newer.time_exchange);
653            assert_eq!(book.time_received(), newer.time_received);
654        }
655
656        #[test]
657        fn test_snapshot_copies_times() {
658            let book = OrderBook::new(1, times(100), vec![Level::new(50, 1)], vec![]);
659            let snap = book.snapshot(10);
660            assert_eq!(snap.time_engine(), book.time_engine());
661            assert_eq!(snap.time_exchange(), book.time_exchange());
662            assert_eq!(snap.time_received(), book.time_received());
663        }
664
665        #[test]
666        fn test_equality_reflects_timestamps() {
667            // Two content-identical books with different `time_received` are unequal.
668            let a = OrderBook::new(1, times(100), vec![Level::new(50, 1)], vec![]);
669            let b = OrderBook::new(1, times(200), vec![Level::new(50, 1)], vec![]);
670            assert_ne!(a, b);
671        }
672
673        #[test]
674        fn test_mid_price() {
675            struct TestCase {
676                input: OrderBook,
677                expected: Option<Decimal>,
678            }
679
680            let tests = vec![
681                TestCase {
682                    // TC0: no levels so 0.0 mid-price
683                    input: OrderBook::new::<Vec<_>, Vec<_>, Level>(
684                        0,
685                        Default::default(),
686                        vec![],
687                        vec![],
688                    ),
689                    expected: None,
690                },
691                TestCase {
692                    // TC1: no asks in the books so take best bid price
693                    input: OrderBook::new(
694                        0,
695                        Default::default(),
696                        vec![
697                            Level::new(dec!(100.0), dec!(100.0)),
698                            Level::new(dec!(50.0), dec!(100.0)),
699                        ],
700                        vec![],
701                    ),
702                    expected: Some(dec!(100.0)),
703                },
704                TestCase {
705                    // TC2: no bids in the books so take ask price
706                    input: OrderBook::new(
707                        0,
708                        Default::default(),
709                        vec![],
710                        vec![
711                            Level::new(dec!(50.0), dec!(100.0)),
712                            Level::new(dec!(100.0), dec!(100.0)),
713                        ],
714                    ),
715                    expected: Some(dec!(50.0)),
716                },
717                TestCase {
718                    // TC3: best bid and ask amount is the same, so regular mid-price
719                    input: OrderBook::new(
720                        0,
721                        Default::default(),
722                        vec![
723                            Level::new(dec!(100.0), dec!(100.0)),
724                            Level::new(dec!(50.0), dec!(100.0)),
725                        ],
726                        vec![
727                            Level::new(dec!(200.0), dec!(100.0)),
728                            Level::new(dec!(300.0), dec!(100.0)),
729                        ],
730                    ),
731                    expected: Some(dec!(150.0)),
732                },
733            ];
734
735            for (index, test) in tests.into_iter().enumerate() {
736                assert_eq!(test.input.mid_price(), test.expected, "TC{index} failed")
737            }
738        }
739
740        #[test]
741        fn test_volume_weighted_mid_price() {
742            struct TestCase {
743                input: OrderBook,
744                expected: Option<Decimal>,
745            }
746
747            let tests = vec![
748                TestCase {
749                    // TC0: no levels so 0.0 mid-price
750                    input: OrderBook::new::<Vec<_>, Vec<_>, Level>(
751                        0,
752                        Default::default(),
753                        vec![],
754                        vec![],
755                    ),
756                    expected: None,
757                },
758                TestCase {
759                    // TC1: no asks in the books so take best bid price
760                    input: OrderBook::new(
761                        0,
762                        Default::default(),
763                        vec![
764                            Level::new(dec!(100.0), dec!(100.0)),
765                            Level::new(dec!(50.0), dec!(100.0)),
766                        ],
767                        vec![],
768                    ),
769                    expected: Some(dec!(100.0)),
770                },
771                TestCase {
772                    // TC2: no bids in the books so take ask price
773                    input: OrderBook::new(
774                        0,
775                        Default::default(),
776                        vec![],
777                        vec![
778                            Level::new(dec!(50.0), dec!(100.0)),
779                            Level::new(dec!(100.0), dec!(100.0)),
780                        ],
781                    ),
782                    expected: Some(dec!(50.0)),
783                },
784                TestCase {
785                    // TC3: best bid and ask amount is the same, so regular mid-price
786                    input: OrderBook::new(
787                        0,
788                        Default::default(),
789                        vec![
790                            Level::new(dec!(100.0), dec!(100.0)),
791                            Level::new(dec!(50.0), dec!(100.0)),
792                        ],
793                        vec![
794                            Level::new(dec!(200.0), dec!(100.0)),
795                            Level::new(dec!(300.0), dec!(100.0)),
796                        ],
797                    ),
798                    expected: Some(dec!(150.0)),
799                },
800                TestCase {
801                    // TC4: valid volume weighted mid-price
802                    input: OrderBook::new(
803                        0,
804                        Default::default(),
805                        vec![
806                            Level::new(dec!(100.0), dec!(3000.0)),
807                            Level::new(dec!(50.0), dec!(100.0)),
808                        ],
809                        vec![
810                            Level::new(dec!(200.0), dec!(1000.0)),
811                            Level::new(dec!(300.0), dec!(100.0)),
812                        ],
813                    ),
814                    expected: Some(dec!(175.0)),
815                },
816            ];
817
818            for (index, test) in tests.into_iter().enumerate() {
819                assert_eq!(
820                    test.input.volume_weighed_mid_price(),
821                    test.expected,
822                    "TC{index} failed"
823                )
824            }
825        }
826    }
827
828    mod order_book_side {
829        use super::*;
830        use rust_decimal_macros::dec;
831
832        #[test]
833        fn test_upsert_single() {
834            struct TestCase {
835                book_side: OrderBookSide<Bids>,
836                new_level: Level,
837                expected: OrderBookSide<Bids>,
838            }
839
840            let tests = vec![
841                TestCase {
842                    // TC0: Level exists & new value is 0 => remove Level
843                    book_side: OrderBookSide::bids(vec![
844                        Level::new(dec!(80), dec!(1)),
845                        Level::new(dec!(90), dec!(1)),
846                        Level::new(dec!(100), dec!(1)),
847                    ]),
848                    new_level: Level::new(dec!(100), dec!(0)),
849                    expected: OrderBookSide::bids(vec![
850                        Level::new(dec!(80), dec!(1)),
851                        Level::new(dec!(90), dec!(1)),
852                    ]),
853                },
854                TestCase {
855                    // TC1: Level exists & new value is > 0 => replace Level
856                    book_side: OrderBookSide::bids(vec![
857                        Level::new(dec!(80), dec!(1)),
858                        Level::new(dec!(90), dec!(1)),
859                        Level::new(dec!(100), dec!(1)),
860                    ]),
861                    new_level: Level::new(dec!(100), dec!(10)),
862                    expected: OrderBookSide::bids(vec![
863                        Level::new(dec!(80), dec!(1)),
864                        Level::new(dec!(90), dec!(1)),
865                        Level::new(dec!(100), dec!(10)),
866                    ]),
867                },
868                TestCase {
869                    // TC2: Level does not exist & new value > 0 => insert new Level
870                    book_side: OrderBookSide::bids(vec![
871                        Level::new(dec!(80), dec!(1)),
872                        Level::new(dec!(90), dec!(1)),
873                        Level::new(dec!(100), dec!(1)),
874                    ]),
875                    new_level: Level::new(dec!(110), dec!(1)),
876                    expected: OrderBookSide::bids(vec![
877                        Level::new(dec!(80), dec!(1)),
878                        Level::new(dec!(90), dec!(1)),
879                        Level::new(dec!(100), dec!(1)),
880                        Level::new(dec!(110), dec!(1)),
881                    ]),
882                },
883                TestCase {
884                    // TC3: Level does not exist & new value is 0 => no change
885                    book_side: OrderBookSide::bids(vec![
886                        Level::new(dec!(80), dec!(1)),
887                        Level::new(dec!(90), dec!(1)),
888                        Level::new(dec!(100), dec!(1)),
889                    ]),
890                    new_level: Level::new(dec!(110), dec!(0)),
891                    expected: OrderBookSide::bids(vec![
892                        Level::new(dec!(80), dec!(1)),
893                        Level::new(dec!(90), dec!(1)),
894                        Level::new(dec!(100), dec!(1)),
895                    ]),
896                },
897            ];
898
899            for (index, mut test) in tests.into_iter().enumerate() {
900                test.book_side.upsert_single(test.new_level, |existing| {
901                    existing.price.cmp(&test.new_level.price).reverse()
902                });
903                assert_eq!(test.book_side, test.expected, "TC{} failed", index);
904            }
905        }
906    }
907}