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    /// See Docs: <https://www.quantstart.com/articles/high-frequency-trading-ii-limit-order-book>
231    pub fn volume_weighed_mid_price(&self) -> Option<Decimal> {
232        match (self.bids.best(), self.asks.best()) {
233            (Some(best_bid), Some(best_ask)) => {
234                Some(volume_weighted_mid_price(*best_bid, *best_ask))
235            }
236            (Some(best_bid), None) => Some(best_bid.price),
237            (None, Some(best_ask)) => Some(best_ask.price),
238            (None, None) => None,
239        }
240    }
241}
242
243/// Normalised Barter [`Level`]s for one `Side` of the [`OrderBook`].
244#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
245pub struct OrderBookSide<Side> {
246    #[serde(skip_serializing)]
247    pub side: Side,
248    levels: Vec<Level>,
249}
250
251/// Unit type to tag an [`OrderBookSide`] as the bid Side (ie/ buyers) of an [`OrderBook`].
252#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Display)]
253pub struct Bids;
254
255/// Unit type to tag an [`OrderBookSide`] as the ask Side (ie/ sellers) of an [`OrderBook`].
256#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Display)]
257pub struct Asks;
258
259impl OrderBookSide<Bids> {
260    /// Construct a new [`OrderBookSide<Bids>`] from the provided [`Level`]s.
261    pub fn bids<Iter, L>(levels: Iter) -> Self
262    where
263        Iter: IntoIterator<Item = L>,
264        L: Into<Level>,
265    {
266        let mut levels = levels.into_iter().map(L::into).collect::<Vec<_>>();
267        levels.sort_unstable_by(|a, b| a.price.cmp(&b.price).reverse());
268
269        Self { side: Bids, levels }
270    }
271
272    /// Upsert bid [`Level`]s into this [`OrderBookSide<Bids>`].
273    pub fn upsert<L>(&mut self, levels: &[L])
274    where
275        L: Into<Level> + Copy,
276    {
277        levels.iter().for_each(|upsert| {
278            let upsert: Level = (*upsert).into();
279            self.upsert_single(upsert, |existing| {
280                existing.price.cmp(&upsert.price).reverse()
281            })
282        })
283    }
284}
285
286impl OrderBookSide<Asks> {
287    /// Construct a new [`OrderBookSide<Asks>`] from the provided [`Level`]s.
288    pub fn asks<Iter, L>(levels: Iter) -> Self
289    where
290        Iter: IntoIterator<Item = L>,
291        L: Into<Level>,
292    {
293        let mut levels = levels.into_iter().map(L::into).collect::<Vec<_>>();
294        levels.sort_unstable_by_key(|a| a.price);
295
296        Self { side: Asks, levels }
297    }
298
299    /// Upsert ask [`Level`]s into this [`OrderBookSide<Asks>`].
300    pub fn upsert<L>(&mut self, levels: &[L])
301    where
302        L: Into<Level> + Copy,
303    {
304        levels.iter().for_each(|upsert| {
305            let upsert = (*upsert).into();
306            self.upsert_single(upsert, |existing| existing.price.cmp(&upsert.price))
307        })
308    }
309}
310
311impl<Side> OrderBookSide<Side>
312where
313    Side: std::fmt::Display + std::fmt::Debug,
314{
315    /// Get best [`Level`] on the [`OrderBookSide`].
316    pub fn best(&self) -> Option<&Level> {
317        self.levels.first()
318    }
319
320    /// Return a reference to the [`OrderBookSide`] levels.
321    pub fn levels(&self) -> &[Level] {
322        &self.levels
323    }
324
325    /// Upsert a single [`Level`] into this [`OrderBookSide`].
326    ///
327    /// ### Upsert Scenarios
328    /// #### 1 Level Already Exists
329    /// 1a) New value is 0, remove the level
330    /// 1b) New value is > 0, replace the level
331    ///
332    /// #### 2 Level Does Not Exist
333    /// 2a) New value is 0, log warn and continue
334    /// 2b) New value is > 0, insert new level
335    pub fn upsert_single<FnOrd>(&mut self, new_level: Level, fn_ord: FnOrd)
336    where
337        FnOrd: Fn(&Level) -> Ordering,
338    {
339        match (self.levels.binary_search_by(fn_ord), new_level.amount) {
340            (Ok(index), new_amount) => {
341                if new_amount.is_zero() {
342                    // Scenario 1a: Level exists & new value is 0 => remove level
343                    let _removed = self.levels.remove(index);
344                } else {
345                    // Scenario 1b: Level exists & new value is > 0 => replace level
346                    self.levels[index].amount = new_amount;
347                }
348            }
349            (Err(index), new_amount) => {
350                if new_amount.is_zero() {
351                    // Scenario 2a: Level does not exist & new value is 0 => log & continue
352                    debug!(
353                        ?new_level,
354                        side = %self.side,
355                        "received upsert Level with zero amount (to remove) that was not found"
356                    );
357                } else {
358                    // Scenario 2b: Level does not exist & new value > 0 => insert new level
359                    self.levels.insert(index, new_level);
360                }
361            }
362        }
363    }
364}
365
366impl Default for OrderBookSide<Bids> {
367    fn default() -> Self {
368        Self {
369            side: Bids,
370            levels: vec![],
371        }
372    }
373}
374
375impl Default for OrderBookSide<Asks> {
376    fn default() -> Self {
377        Self {
378            side: Asks,
379            levels: vec![],
380        }
381    }
382}
383
384/// Normalised Barter OrderBook [`Level`].
385#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Hash, Default, Deserialize, Serialize)]
386pub struct Level {
387    pub price: Decimal,
388    pub amount: Decimal,
389}
390
391impl<T> From<(T, T)> for Level
392where
393    T: Into<Decimal>,
394{
395    fn from((price, amount): (T, T)) -> Self {
396        Self::new(price, amount)
397    }
398}
399
400impl Eq for Level {}
401
402impl Level {
403    pub fn new<T>(price: T, amount: T) -> Self
404    where
405        T: Into<Decimal>,
406    {
407        Self {
408            price: price.into(),
409            amount: amount.into(),
410        }
411    }
412}
413
414/// Calculate the mid-price by taking the average of the best bid and ask prices.
415///
416/// See Docs: <https://www.quantstart.com/articles/high-frequency-trading-ii-limit-order-book>
417pub fn mid_price(best_bid_price: Decimal, best_ask_price: Decimal) -> Decimal {
418    (best_bid_price + best_ask_price) / Decimal::TWO
419}
420
421/// Calculate the volume weighted mid-price (micro-price), weighing the best bid and ask prices
422/// with their associated amount.
423///
424/// See Docs: <https://www.quantstart.com/articles/high-frequency-trading-ii-limit-order-book>
425pub fn volume_weighted_mid_price(best_bid: Level, best_ask: Level) -> Decimal {
426    ((best_bid.price * best_ask.amount) + (best_ask.price * best_bid.amount))
427        / (best_bid.amount + best_ask.amount)
428}
429
430#[cfg(test)]
431#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
432mod tests {
433    use super::*;
434
435    mod order_book_l1 {
436        use super::*;
437        use crate::subscription::book::OrderBookL1;
438        use rust_decimal_macros::dec;
439
440        #[test]
441        fn test_mid_price() {
442            struct TestCase {
443                input: OrderBookL1,
444                expected: Option<Decimal>,
445            }
446
447            let tests = vec![
448                TestCase {
449                    // TC0
450                    input: OrderBookL1 {
451                        last_update_time: Default::default(),
452                        best_bid: Some(Level::new(100, 999999)),
453                        best_ask: Some(Level::new(200, 1)),
454                    },
455                    expected: Some(dec!(150.0)),
456                },
457                TestCase {
458                    // TC1
459                    input: OrderBookL1 {
460                        last_update_time: Default::default(),
461                        best_bid: Some(Level::new(50, 1)),
462                        best_ask: Some(Level::new(250, 999999)),
463                    },
464                    expected: Some(dec!(150.0)),
465                },
466                TestCase {
467                    // TC2
468                    input: OrderBookL1 {
469                        last_update_time: Default::default(),
470                        best_bid: Some(Level::new(10, 999999)),
471                        best_ask: Some(Level::new(250, 999999)),
472                    },
473                    expected: Some(dec!(130.0)),
474                },
475                TestCase {
476                    // TC3
477                    input: OrderBookL1 {
478                        last_update_time: Default::default(),
479                        best_bid: Some(Level::new(10, 999999)),
480                        best_ask: None,
481                    },
482                    expected: None,
483                },
484                TestCase {
485                    // TC4
486                    input: OrderBookL1 {
487                        last_update_time: Default::default(),
488                        best_bid: None,
489                        best_ask: Some(Level::new(250, 999999)),
490                    },
491                    expected: None,
492                },
493            ];
494
495            for (index, test) in tests.into_iter().enumerate() {
496                assert_eq!(test.input.mid_price(), test.expected, "TC{index} failed")
497            }
498        }
499
500        #[test]
501        fn test_volume_weighted_mid_price() {
502            struct TestCase {
503                input: OrderBookL1,
504                expected: Option<Decimal>,
505            }
506
507            let tests = vec![
508                TestCase {
509                    // TC0: volume the same so should be equal to non-weighted mid price
510                    input: OrderBookL1 {
511                        last_update_time: Default::default(),
512                        best_bid: Some(Level::new(100, 100)),
513                        best_ask: Some(Level::new(200, 100)),
514                    },
515                    expected: Some(dec!(150.0)),
516                },
517                TestCase {
518                    // TC1: volume affects mid-price
519                    input: OrderBookL1 {
520                        last_update_time: Default::default(),
521                        best_bid: Some(Level::new(100, 600)),
522                        best_ask: Some(Level::new(200, 1000)),
523                    },
524                    expected: Some(dec!(137.5)),
525                },
526                TestCase {
527                    // TC2: volume the same and price the same
528                    input: OrderBookL1 {
529                        last_update_time: Default::default(),
530                        best_bid: Some(Level::new(1000, 999999)),
531                        best_ask: Some(Level::new(1000, 999999)),
532                    },
533                    expected: Some(dec!(1000.0)),
534                },
535                TestCase {
536                    // TC3: best ask is None
537                    input: OrderBookL1 {
538                        last_update_time: Default::default(),
539                        best_bid: Some(Level::new(1000, 999999)),
540                        best_ask: None,
541                    },
542                    expected: None,
543                },
544                TestCase {
545                    // TC4: best bid is None
546                    input: OrderBookL1 {
547                        last_update_time: Default::default(),
548                        best_bid: None,
549                        best_ask: Some(Level::new(1000, 999999)),
550                    },
551                    expected: None,
552                },
553            ];
554
555            for (index, test) in tests.into_iter().enumerate() {
556                assert_eq!(
557                    test.input.volume_weighed_mid_price(),
558                    test.expected,
559                    "TC{index} failed"
560                )
561            }
562        }
563    }
564
565    mod order_book {
566        use super::*;
567        use rust_decimal_macros::dec;
568
569        fn times(received_ms: i64) -> OrderBookTimes {
570            OrderBookTimes {
571                time_engine: Some(DateTime::from_timestamp_millis(1).unwrap()),
572                time_exchange: Some(DateTime::from_timestamp_millis(2).unwrap()),
573                time_received: DateTime::from_timestamp_millis(received_ms).unwrap(),
574            }
575        }
576
577        #[test]
578        fn test_update_replaces_times_wholesale() {
579            let mut book = OrderBook::new(1, times(100), vec![Level::new(50, 1)], vec![]);
580
581            let newer = OrderBookTimes {
582                time_engine: None,
583                time_exchange: Some(DateTime::from_timestamp_millis(999).unwrap()),
584                time_received: DateTime::from_timestamp_millis(1000).unwrap(),
585            };
586            let update = OrderBook::new(2, newer, vec![Level::new(50, 1)], vec![]);
587            book.update(&OrderBookEvent::Update(update));
588
589            assert_eq!(book.time_engine(), newer.time_engine);
590            assert_eq!(book.time_exchange(), newer.time_exchange);
591            assert_eq!(book.time_received(), newer.time_received);
592        }
593
594        #[test]
595        fn test_update_snapshot_propagates_times_wholesale() {
596            // The `Snapshot` arm replaces the live book via `*self = snapshot.clone()`,
597            // so it must carry the snapshot's `times` (not retain the old ones).
598            let mut book = OrderBook::new(1, times(100), vec![Level::new(50, 1)], vec![]);
599
600            let newer = OrderBookTimes {
601                time_engine: None,
602                time_exchange: Some(DateTime::from_timestamp_millis(999).unwrap()),
603                time_received: DateTime::from_timestamp_millis(1000).unwrap(),
604            };
605            let snapshot = OrderBook::new(2, newer, vec![Level::new(60, 1)], vec![]);
606            book.update(&OrderBookEvent::Snapshot(snapshot));
607
608            assert_eq!(book.time_engine(), newer.time_engine);
609            assert_eq!(book.time_exchange(), newer.time_exchange);
610            assert_eq!(book.time_received(), newer.time_received);
611        }
612
613        #[test]
614        fn test_snapshot_copies_times() {
615            let book = OrderBook::new(1, times(100), vec![Level::new(50, 1)], vec![]);
616            let snap = book.snapshot(10);
617            assert_eq!(snap.time_engine(), book.time_engine());
618            assert_eq!(snap.time_exchange(), book.time_exchange());
619            assert_eq!(snap.time_received(), book.time_received());
620        }
621
622        #[test]
623        fn test_equality_reflects_timestamps() {
624            // Two content-identical books with different `time_received` are unequal.
625            let a = OrderBook::new(1, times(100), vec![Level::new(50, 1)], vec![]);
626            let b = OrderBook::new(1, times(200), vec![Level::new(50, 1)], vec![]);
627            assert_ne!(a, b);
628        }
629
630        #[test]
631        fn test_mid_price() {
632            struct TestCase {
633                input: OrderBook,
634                expected: Option<Decimal>,
635            }
636
637            let tests = vec![
638                TestCase {
639                    // TC0: no levels so 0.0 mid-price
640                    input: OrderBook::new::<Vec<_>, Vec<_>, Level>(
641                        0,
642                        Default::default(),
643                        vec![],
644                        vec![],
645                    ),
646                    expected: None,
647                },
648                TestCase {
649                    // TC1: no asks in the books so take best bid price
650                    input: OrderBook::new(
651                        0,
652                        Default::default(),
653                        vec![
654                            Level::new(dec!(100.0), dec!(100.0)),
655                            Level::new(dec!(50.0), dec!(100.0)),
656                        ],
657                        vec![],
658                    ),
659                    expected: Some(dec!(100.0)),
660                },
661                TestCase {
662                    // TC2: no bids in the books so take ask price
663                    input: OrderBook::new(
664                        0,
665                        Default::default(),
666                        vec![],
667                        vec![
668                            Level::new(dec!(50.0), dec!(100.0)),
669                            Level::new(dec!(100.0), dec!(100.0)),
670                        ],
671                    ),
672                    expected: Some(dec!(50.0)),
673                },
674                TestCase {
675                    // TC3: best bid and ask amount is the same, so regular mid-price
676                    input: OrderBook::new(
677                        0,
678                        Default::default(),
679                        vec![
680                            Level::new(dec!(100.0), dec!(100.0)),
681                            Level::new(dec!(50.0), dec!(100.0)),
682                        ],
683                        vec![
684                            Level::new(dec!(200.0), dec!(100.0)),
685                            Level::new(dec!(300.0), dec!(100.0)),
686                        ],
687                    ),
688                    expected: Some(dec!(150.0)),
689                },
690            ];
691
692            for (index, test) in tests.into_iter().enumerate() {
693                assert_eq!(test.input.mid_price(), test.expected, "TC{index} failed")
694            }
695        }
696
697        #[test]
698        fn test_volume_weighted_mid_price() {
699            struct TestCase {
700                input: OrderBook,
701                expected: Option<Decimal>,
702            }
703
704            let tests = vec![
705                TestCase {
706                    // TC0: no levels so 0.0 mid-price
707                    input: OrderBook::new::<Vec<_>, Vec<_>, Level>(
708                        0,
709                        Default::default(),
710                        vec![],
711                        vec![],
712                    ),
713                    expected: None,
714                },
715                TestCase {
716                    // TC1: no asks in the books so take best bid price
717                    input: OrderBook::new(
718                        0,
719                        Default::default(),
720                        vec![
721                            Level::new(dec!(100.0), dec!(100.0)),
722                            Level::new(dec!(50.0), dec!(100.0)),
723                        ],
724                        vec![],
725                    ),
726                    expected: Some(dec!(100.0)),
727                },
728                TestCase {
729                    // TC2: no bids in the books so take ask price
730                    input: OrderBook::new(
731                        0,
732                        Default::default(),
733                        vec![],
734                        vec![
735                            Level::new(dec!(50.0), dec!(100.0)),
736                            Level::new(dec!(100.0), dec!(100.0)),
737                        ],
738                    ),
739                    expected: Some(dec!(50.0)),
740                },
741                TestCase {
742                    // TC3: best bid and ask amount is the same, so regular mid-price
743                    input: OrderBook::new(
744                        0,
745                        Default::default(),
746                        vec![
747                            Level::new(dec!(100.0), dec!(100.0)),
748                            Level::new(dec!(50.0), dec!(100.0)),
749                        ],
750                        vec![
751                            Level::new(dec!(200.0), dec!(100.0)),
752                            Level::new(dec!(300.0), dec!(100.0)),
753                        ],
754                    ),
755                    expected: Some(dec!(150.0)),
756                },
757                TestCase {
758                    // TC4: valid volume weighted mid-price
759                    input: OrderBook::new(
760                        0,
761                        Default::default(),
762                        vec![
763                            Level::new(dec!(100.0), dec!(3000.0)),
764                            Level::new(dec!(50.0), dec!(100.0)),
765                        ],
766                        vec![
767                            Level::new(dec!(200.0), dec!(1000.0)),
768                            Level::new(dec!(300.0), dec!(100.0)),
769                        ],
770                    ),
771                    expected: Some(dec!(175.0)),
772                },
773            ];
774
775            for (index, test) in tests.into_iter().enumerate() {
776                assert_eq!(
777                    test.input.volume_weighed_mid_price(),
778                    test.expected,
779                    "TC{index} failed"
780                )
781            }
782        }
783    }
784
785    mod order_book_side {
786        use super::*;
787        use rust_decimal_macros::dec;
788
789        #[test]
790        fn test_upsert_single() {
791            struct TestCase {
792                book_side: OrderBookSide<Bids>,
793                new_level: Level,
794                expected: OrderBookSide<Bids>,
795            }
796
797            let tests = vec![
798                TestCase {
799                    // TC0: Level exists & new value is 0 => remove Level
800                    book_side: OrderBookSide::bids(vec![
801                        Level::new(dec!(80), dec!(1)),
802                        Level::new(dec!(90), dec!(1)),
803                        Level::new(dec!(100), dec!(1)),
804                    ]),
805                    new_level: Level::new(dec!(100), dec!(0)),
806                    expected: OrderBookSide::bids(vec![
807                        Level::new(dec!(80), dec!(1)),
808                        Level::new(dec!(90), dec!(1)),
809                    ]),
810                },
811                TestCase {
812                    // TC1: Level exists & new value is > 0 => replace Level
813                    book_side: OrderBookSide::bids(vec![
814                        Level::new(dec!(80), dec!(1)),
815                        Level::new(dec!(90), dec!(1)),
816                        Level::new(dec!(100), dec!(1)),
817                    ]),
818                    new_level: Level::new(dec!(100), dec!(10)),
819                    expected: OrderBookSide::bids(vec![
820                        Level::new(dec!(80), dec!(1)),
821                        Level::new(dec!(90), dec!(1)),
822                        Level::new(dec!(100), dec!(10)),
823                    ]),
824                },
825                TestCase {
826                    // TC2: Level does not exist & new value > 0 => insert new Level
827                    book_side: OrderBookSide::bids(vec![
828                        Level::new(dec!(80), dec!(1)),
829                        Level::new(dec!(90), dec!(1)),
830                        Level::new(dec!(100), dec!(1)),
831                    ]),
832                    new_level: Level::new(dec!(110), dec!(1)),
833                    expected: OrderBookSide::bids(vec![
834                        Level::new(dec!(80), dec!(1)),
835                        Level::new(dec!(90), dec!(1)),
836                        Level::new(dec!(100), dec!(1)),
837                        Level::new(dec!(110), dec!(1)),
838                    ]),
839                },
840                TestCase {
841                    // TC3: Level does not exist & new value is 0 => no change
842                    book_side: OrderBookSide::bids(vec![
843                        Level::new(dec!(80), dec!(1)),
844                        Level::new(dec!(90), dec!(1)),
845                        Level::new(dec!(100), dec!(1)),
846                    ]),
847                    new_level: Level::new(dec!(110), dec!(0)),
848                    expected: OrderBookSide::bids(vec![
849                        Level::new(dec!(80), dec!(1)),
850                        Level::new(dec!(90), dec!(1)),
851                        Level::new(dec!(100), dec!(1)),
852                    ]),
853                },
854            ];
855
856            for (index, mut test) in tests.into_iter().enumerate() {
857                test.book_side.upsert_single(test.new_level, |existing| {
858                    existing.price.cmp(&test.new_level.price).reverse()
859                });
860                assert_eq!(test.book_side, test.expected, "TC{} failed", index);
861            }
862        }
863    }
864}