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}