1use crate::error::FinError;
20use crate::types::{Price, Quantity, Side, Symbol};
21use rust_decimal::Decimal;
22use std::collections::BTreeMap;
23
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
26pub struct PriceLevel {
27 pub price: Price,
29 pub quantity: Quantity,
31}
32
33#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub enum DeltaAction {
36 Set,
38 Remove,
40}
41
42#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
44pub struct BookDelta {
45 pub side: Side,
47 pub price: Price,
49 pub quantity: Quantity,
51 pub action: DeltaAction,
53 pub sequence: u64,
55}
56
57#[derive(Debug, Clone)]
59pub struct OrderBook {
60 pub symbol: Symbol,
62 bids: BTreeMap<Decimal, Decimal>,
65 asks: BTreeMap<Decimal, Decimal>,
67 sequence: u64,
69}
70
71impl OrderBook {
72 pub fn new(symbol: Symbol) -> Self {
74 Self {
75 symbol,
76 bids: BTreeMap::new(),
77 asks: BTreeMap::new(),
78 sequence: 0,
79 }
80 }
81
82 #[allow(clippy::needless_pass_by_value)]
87 pub fn apply_delta(&mut self, delta: BookDelta) -> Result<(), FinError> {
88 let expected = self.sequence + 1;
89 if delta.sequence != expected {
90 return Err(FinError::SequenceMismatch {
91 expected,
92 got: delta.sequence,
93 });
94 }
95 let prev_val = match delta.side {
97 Side::Bid => self.bids.get(&delta.price.value()).copied(),
98 Side::Ask => self.asks.get(&delta.price.value()).copied(),
99 };
100
101 let book_side = match delta.side {
102 Side::Bid => &mut self.bids,
103 Side::Ask => &mut self.asks,
104 };
105 match delta.action {
106 DeltaAction::Set => {
107 book_side.insert(delta.price.value(), delta.quantity.value());
108 }
109 DeltaAction::Remove => {
110 book_side.remove(&delta.price.value());
111 }
112 }
113 self.sequence = delta.sequence;
114
115 let maybe_inversion = {
118 let best_bid_p = self.bids.keys().next_back().copied();
119 let best_ask_p = self.asks.keys().next().copied();
120 match (best_bid_p, best_ask_p) {
121 (Some(b), Some(a)) if b >= a => Some((b, a)),
122 _ => None,
123 }
124 };
125 if let Some((best_bid_p, best_ask_p)) = maybe_inversion {
126 let _ = (best_bid_p, best_ask_p, delta.sequence, &self.symbol);
131 match delta.action {
133 DeltaAction::Set => match delta.side {
134 Side::Bid => {
135 self.bids.remove(&delta.price.value());
136 }
137 Side::Ask => {
138 self.asks.remove(&delta.price.value());
139 }
140 },
141 DeltaAction::Remove => match delta.side {
144 Side::Bid => {
145 if let Some(qty) = prev_val {
146 self.bids.insert(delta.price.value(), qty);
147 }
148 }
149 Side::Ask => {
150 if let Some(qty) = prev_val {
151 self.asks.insert(delta.price.value(), qty);
152 }
153 }
154 },
155 }
156 self.sequence = expected - 1;
157 return Err(FinError::InvertedSpread {
158 best_bid: best_bid_p,
159 best_ask: best_ask_p,
160 });
161 }
162
163 Ok(())
164 }
165
166 pub fn best_bid(&self) -> Option<PriceLevel> {
171 self.bids.iter().next_back().and_then(|(p, q)| {
172 Some(PriceLevel {
173 price: Price::new(*p).ok()?,
174 quantity: Quantity::new(*q).unwrap_or_else(|_| Quantity::zero()),
175 })
176 })
177 }
178
179 pub fn best_quote(&self) -> Option<(PriceLevel, PriceLevel)> {
183 Some((self.best_bid()?, self.best_ask()?))
184 }
185
186 pub fn best_ask(&self) -> Option<PriceLevel> {
191 self.asks.iter().next().and_then(|(p, q)| {
192 Some(PriceLevel {
193 price: Price::new(*p).ok()?,
194 quantity: Quantity::new(*q).unwrap_or_else(|_| Quantity::zero()),
195 })
196 })
197 }
198
199 pub fn mid_price(&self) -> Option<Decimal> {
201 let bid = self.best_bid()?.price.value();
202 let ask = self.best_ask()?.price.value();
203 Some((bid + ask) / Decimal::TWO)
204 }
205
206 pub fn spread(&self) -> Option<Decimal> {
208 let bid = self.best_bid()?.price.value();
209 let ask = self.best_ask()?.price.value();
210 Some(ask - bid)
211 }
212
213 pub fn spread_pct(&self) -> Option<Decimal> {
217 let mid = self.mid_price()?;
218 if mid.is_zero() {
219 return None;
220 }
221 let spread = self.spread()?;
222 Some(spread / mid * Decimal::ONE_HUNDRED)
223 }
224
225 pub fn depth_at(&self, side: Side, price: Price) -> Option<Decimal> {
227 let key = price.value();
228 match side {
229 Side::Bid => self.bids.get(&key).copied(),
230 Side::Ask => self.asks.get(&key).copied(),
231 }
232 }
233
234 pub fn top_bids(&self, n: usize) -> Vec<PriceLevel> {
236 self.bids
237 .iter()
238 .rev()
239 .take(n)
240 .filter_map(|(p, q)| {
241 let price = Price::new(*p).ok()?;
242 let quantity = Quantity::new(*q).ok()?;
243 Some(PriceLevel { price, quantity })
244 })
245 .collect()
246 }
247
248 pub fn top_asks(&self, n: usize) -> Vec<PriceLevel> {
250 self.asks
251 .iter()
252 .take(n)
253 .filter_map(|(p, q)| {
254 let price = Price::new(*p).ok()?;
255 let quantity = Quantity::new(*q).ok()?;
256 Some(PriceLevel { price, quantity })
257 })
258 .collect()
259 }
260
261 pub fn vwap_for_qty(&self, side: Side, qty: Quantity) -> Result<Decimal, FinError> {
268 let target = qty.value();
269 if target <= Decimal::ZERO {
270 return Ok(Decimal::ZERO);
271 }
272 match side {
273 Side::Bid => Self::vwap_fill(self.bids.iter().rev(), target),
274 Side::Ask => Self::vwap_fill(self.asks.iter(), target),
275 }
276 }
277
278 fn vwap_fill<'a>(
279 levels: impl Iterator<Item = (&'a Decimal, &'a Decimal)>,
280 target: Decimal,
281 ) -> Result<Decimal, FinError> {
282 let mut remaining = target;
283 let mut total_cost = Decimal::ZERO;
284
285 for (price, avail_qty) in levels {
286 let fill = remaining.min(*avail_qty);
287 total_cost += fill * price;
288 remaining -= fill;
289 if remaining <= Decimal::ZERO {
290 break;
291 }
292 }
293
294 if remaining > Decimal::ZERO {
295 return Err(FinError::InsufficientLiquidity(target));
296 }
297
298 Ok(total_cost / target)
299 }
300
301 pub fn sequence(&self) -> u64 {
303 self.sequence
304 }
305
306 pub fn snapshot(&self, n: usize) -> (Vec<PriceLevel>, Vec<PriceLevel>) {
311 (self.top_bids(n), self.top_asks(n))
312 }
313
314 pub fn bid_count(&self) -> usize {
316 self.bids.len()
317 }
318
319 pub fn ask_count(&self) -> usize {
321 self.asks.len()
322 }
323
324 pub fn level_count(&self, side: Side) -> usize {
326 match side {
327 Side::Bid => self.bids.len(),
328 Side::Ask => self.asks.len(),
329 }
330 }
331
332 pub fn clear(&mut self) {
334 self.bids.clear();
335 self.asks.clear();
336 self.sequence = 0;
337 }
338
339 pub fn remove_all(&mut self, side: crate::types::Side) {
343 use crate::types::Side;
344 match side {
345 Side::Bid => self.bids.clear(),
346 Side::Ask => self.asks.clear(),
347 }
348 }
349
350 pub fn is_crossed(&self) -> bool {
356 match (self.best_bid(), self.best_ask()) {
357 (Some(bid), Some(ask)) => bid.price >= ask.price,
358 _ => false,
359 }
360 }
361
362 pub fn is_empty(&self) -> bool {
364 self.bids.is_empty() && self.asks.is_empty()
365 }
366
367 pub fn total_levels(&self) -> usize {
369 self.bids.len() + self.asks.len()
370 }
371
372 pub fn cumulative_depth(&self, side: Side, price: Price) -> Decimal {
379 let p = price.value();
380 match side {
381 Side::Bid => self
382 .bids
383 .range(p..)
384 .map(|(_, qty)| *qty)
385 .sum(),
386 Side::Ask => self
387 .asks
388 .range(..=p)
389 .map(|(_, qty)| *qty)
390 .sum(),
391 }
392 }
393
394 pub fn total_bid_volume(&self) -> Decimal {
396 self.bids.values().copied().sum()
397 }
398
399 pub fn total_ask_volume(&self) -> Decimal {
401 self.asks.values().copied().sum()
402 }
403
404 pub fn best_bid_price(&self) -> Option<Price> {
406 self.bids.keys().next_back().and_then(|p| Price::new(*p).ok())
407 }
408
409 pub fn best_ask_price(&self) -> Option<Price> {
411 self.asks.keys().next().and_then(|p| Price::new(*p).ok())
412 }
413
414 pub fn best_bid_qty(&self) -> Option<Quantity> {
416 self.bids
417 .values()
418 .next_back()
419 .and_then(|q| Quantity::new(*q).ok())
420 }
421
422 pub fn best_ask_qty(&self) -> Option<Quantity> {
424 self.asks
425 .values()
426 .next()
427 .and_then(|q| Quantity::new(*q).ok())
428 }
429
430 pub fn liquidity_at_pct(&self, side: Side, pct_from_mid: Decimal) -> Option<Decimal> {
435 let mid = self.mid_price()?;
436 let band = mid * pct_from_mid / Decimal::ONE_HUNDRED;
437 let (lo, hi) = match side {
438 Side::Bid => (mid - band, mid),
439 Side::Ask => (mid, mid + band),
440 };
441 let qty: Decimal = match side {
442 Side::Bid => self
443 .bids
444 .range(lo..=hi)
445 .map(|(_, q)| *q)
446 .sum(),
447 Side::Ask => self
448 .asks
449 .range(lo..=hi)
450 .map(|(_, q)| *q)
451 .sum(),
452 };
453 Some(qty)
454 }
455
456 pub fn has_price(&self, side: Side, price: Price) -> bool {
458 let key = price.value();
459 match side {
460 Side::Bid => self.bids.contains_key(&key),
461 Side::Ask => self.asks.contains_key(&key),
462 }
463 }
464
465 pub fn weighted_mid(&self) -> Option<Decimal> {
471 let bid = self.best_bid()?;
472 let ask = self.best_ask()?;
473 let bid_qty = bid.quantity.value();
474 let ask_qty = ask.quantity.value();
475 let total = bid_qty + ask_qty;
476 if total.is_zero() {
477 return None;
478 }
479 Some((bid.price.value() * ask_qty + ask.price.value() * bid_qty) / total)
480 }
481
482 pub fn imbalance(&self) -> Option<Decimal> {
487 let bid_vol = self.total_bid_volume();
488 let ask_vol = self.total_ask_volume();
489 let total = bid_vol + ask_vol;
490 if total == Decimal::ZERO {
491 return None;
492 }
493 Some((bid_vol - ask_vol) / total)
494 }
495
496 pub fn depth_ratio(&self, n: usize) -> Option<Decimal> {
501 let bid_vol: Decimal = self.bids.values().rev().take(n).copied().sum();
502 let ask_vol: Decimal = self.asks.values().take(n).copied().sum();
503 if ask_vol.is_zero() {
504 return None;
505 }
506 Some(bid_vol / ask_vol)
507 }
508
509 #[deprecated(since = "2.1.0", note = "Use `weighted_mid` instead")]
515 pub fn weighted_mid_price(&self) -> Option<Decimal> {
516 self.weighted_mid()
517 }
518
519 pub fn price_levels_between(&self, side: Side, lo: Price, hi: Price) -> Vec<PriceLevel> {
523 let lo_val = lo.value();
524 let hi_val = hi.value();
525 match side {
526 Side::Bid => self
527 .bids
528 .range(lo_val..=hi_val)
529 .map(|(p, q)| PriceLevel {
530 price: Price::new(*p).unwrap_or(lo),
531 quantity: crate::types::Quantity::new(*q).unwrap_or_else(|_| crate::types::Quantity::zero()),
532 })
533 .collect(),
534 Side::Ask => self
535 .asks
536 .range(lo_val..=hi_val)
537 .map(|(p, q)| PriceLevel {
538 price: Price::new(*p).unwrap_or(lo),
539 quantity: crate::types::Quantity::new(*q).unwrap_or_else(|_| crate::types::Quantity::zero()),
540 })
541 .collect(),
542 }
543 }
544
545 pub fn tick_size(&self) -> Option<Decimal> {
550 let bid_tick = self
551 .bids
552 .keys()
553 .collect::<Vec<_>>()
554 .windows(2)
555 .map(|w| (*w[1] - *w[0]).abs())
556 .filter(|d| !d.is_zero())
557 .reduce(Decimal::min);
558 let ask_tick = self
559 .asks
560 .keys()
561 .collect::<Vec<_>>()
562 .windows(2)
563 .map(|w| (*w[1] - *w[0]).abs())
564 .filter(|d| !d.is_zero())
565 .reduce(Decimal::min);
566 match (bid_tick, ask_tick) {
567 (Some(b), Some(a)) => Some(b.min(a)),
568 (Some(b), None) => Some(b),
569 (None, Some(a)) => Some(a),
570 (None, None) => None,
571 }
572 }
573
574 pub fn bid_ask_ratio(&self) -> Option<Decimal> {
579 let bid = self.total_bid_volume();
580 let ask = self.total_ask_volume();
581 if ask.is_zero() || bid.is_zero() {
582 return None;
583 }
584 Some(bid / ask)
585 }
586
587 pub fn price_impact(&self, side: crate::types::Side, qty: crate::types::Quantity) -> Option<Decimal> {
593 use crate::types::Side;
594 if qty.is_zero() {
595 return None;
596 }
597 let levels: Vec<_> = match side {
598 Side::Bid => {
599 let mut asks: Vec<_> = self.asks.iter().collect();
601 asks.sort_by(|a, b| a.0.cmp(b.0));
602 asks.into_iter().map(|(p, q)| (*p, *q)).collect()
603 }
604 Side::Ask => {
605 let mut bids: Vec<_> = self.bids.iter().collect();
607 bids.sort_by(|a, b| b.0.cmp(a.0));
608 bids.into_iter().map(|(p, q)| (*p, *q)).collect()
609 }
610 };
611 let target = qty.value();
612 let mut remaining = target;
613 let mut notional = Decimal::ZERO;
614 for (price, level_qty) in levels {
615 let fill = level_qty.min(remaining);
616 notional += price * fill;
617 remaining -= fill;
618 if remaining <= Decimal::ZERO {
619 break;
620 }
621 }
622 if remaining > Decimal::ZERO {
623 None } else {
625 Some(notional / target)
626 }
627 }
628
629 pub fn bid_depth(&self, n: usize) -> Vec<PriceLevel> {
633 self.bids
634 .iter()
635 .rev()
636 .take(n)
637 .map(|(price, qty)| PriceLevel {
638 price: Price::new(*price).unwrap(),
639 quantity: Quantity::new(*qty).unwrap(),
640 })
641 .collect()
642 }
643
644 pub fn ask_depth(&self, n: usize) -> Vec<PriceLevel> {
648 self.asks
649 .iter()
650 .take(n)
651 .map(|(price, qty)| PriceLevel {
652 price: Price::new(*price).unwrap(),
653 quantity: Quantity::new(*qty).unwrap(),
654 })
655 .collect()
656 }
657
658 pub fn depth_imbalance(&self) -> Option<Decimal> {
665 let bid_qty: Decimal = self.bids.values().sum();
666 let ask_qty: Decimal = self.asks.values().sum();
667 let total = bid_qty + ask_qty;
668 if total.is_zero() {
669 return None;
670 }
671 Some((bid_qty - ask_qty) / total)
672 }
673
674 pub fn ask_bid_ratio(&self) -> Option<Decimal> {
679 let bid_qty: Decimal = self.bids.values().sum();
680 let ask_qty: Decimal = self.asks.values().sum();
681 if bid_qty.is_zero() {
682 return None;
683 }
684 Some(ask_qty / bid_qty)
685 }
686
687 pub fn total_bid_depth(&self) -> Decimal {
689 self.bids.values().sum()
690 }
691
692 pub fn total_ask_depth(&self) -> Decimal {
694 self.asks.values().sum()
695 }
696
697 pub fn price_at_volume(&self, side: Side, target_qty: Decimal) -> Option<Price> {
706 if target_qty.is_zero() {
707 return None;
708 }
709 let mut remaining = target_qty;
710 let mut last_price: Option<Price> = None;
711
712 match side {
713 Side::Ask => {
714 for (&px, &qty) in &self.asks {
715 last_price = Price::new(px).ok();
716 if qty >= remaining {
717 return last_price;
718 }
719 remaining -= qty;
720 }
721 }
722 Side::Bid => {
723 for (&px, &qty) in self.bids.iter().rev() {
724 last_price = Price::new(px).ok();
725 if qty >= remaining {
726 return last_price;
727 }
728 remaining -= qty;
729 }
730 }
731 }
732 last_price
733 }
734
735 pub fn top_n_bid_levels(&self, n: usize) -> Vec<PriceLevel> {
739 if n == 0 {
740 return vec![];
741 }
742 self.bids
743 .iter()
744 .rev()
745 .take(n)
746 .filter_map(|(&px, &qty)| {
747 let price = Price::new(px).ok()?;
748 let quantity = Quantity::new(qty).ok()?;
749 Some(PriceLevel { price, quantity })
750 })
751 .collect()
752 }
753
754 pub fn top_n_ask_levels(&self, n: usize) -> Vec<PriceLevel> {
758 if n == 0 {
759 return vec![];
760 }
761 self.asks
762 .iter()
763 .take(n)
764 .filter_map(|(&px, &qty)| {
765 let price = Price::new(px).ok()?;
766 let quantity = Quantity::new(qty).ok()?;
767 Some(PriceLevel { price, quantity })
768 })
769 .collect()
770 }
771
772 pub fn cumulative_bid_qty(&self, n: usize) -> Decimal {
777 if n == 0 {
778 return Decimal::ZERO;
779 }
780 self.bids.iter().rev().take(n).map(|(_, &qty)| qty).sum()
781 }
782
783 pub fn bid_depth_skew(&self, n: usize) -> Option<Decimal> {
789 if n == 0 {
790 return None;
791 }
792 let bid_qty = self.cumulative_bid_qty(n);
793 let ask_qty = self.cumulative_ask_qty(n);
794 let total = bid_qty + ask_qty;
795 if total.is_zero() {
796 return None;
797 }
798 bid_qty.checked_div(total)
799 }
800
801 pub fn spread_bps(&self) -> Option<Decimal> {
806 let bid = self.best_bid()?.price.value();
807 let ask = self.best_ask()?.price.value();
808 let mid = (bid + ask) / Decimal::TWO;
809 if mid.is_zero() {
810 return None;
811 }
812 let spread = ask - bid;
813 spread.checked_div(mid).map(|r| r * Decimal::from(10_000u32))
814 }
815
816 pub fn cumulative_ask_qty(&self, n: usize) -> Decimal {
821 if n == 0 {
822 return Decimal::ZERO;
823 }
824 self.asks.iter().take(n).map(|(_, &qty)| qty).sum()
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831 use rust_decimal_macros::dec;
832
833 fn make_book() -> OrderBook {
834 OrderBook::new(Symbol::new("AAPL").unwrap())
835 }
836
837 fn set_delta(side: Side, price: &str, qty: &str, seq: u64) -> BookDelta {
838 BookDelta {
839 side,
840 price: Price::new(price.parse().unwrap()).unwrap(),
841 quantity: Quantity::new(qty.parse().unwrap()).unwrap(),
842 action: DeltaAction::Set,
843 sequence: seq,
844 }
845 }
846
847 fn remove_delta(side: Side, price: &str, seq: u64) -> BookDelta {
848 BookDelta {
849 side,
850 price: Price::new(price.parse().unwrap()).unwrap(),
851 quantity: Quantity::zero(),
852 action: DeltaAction::Remove,
853 sequence: seq,
854 }
855 }
856
857 #[test]
858 fn test_orderbook_apply_delta_updates_bid() {
859 let mut book = make_book();
860 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
861 .unwrap();
862 let best = book.best_bid().unwrap();
863 assert_eq!(best.price.value(), dec!(100));
864 assert_eq!(best.quantity.value(), dec!(10));
865 }
866
867 #[test]
868 fn test_orderbook_apply_delta_updates_ask() {
869 let mut book = make_book();
870 book.apply_delta(set_delta(Side::Ask, "101", "5", 1))
871 .unwrap();
872 let best = book.best_ask().unwrap();
873 assert_eq!(best.price.value(), dec!(101));
874 assert_eq!(best.quantity.value(), dec!(5));
875 }
876
877 #[test]
878 fn test_orderbook_sequence_mismatch_returns_error() {
879 let mut book = make_book();
880 let result = book.apply_delta(set_delta(Side::Bid, "100", "10", 2));
881 assert!(matches!(
882 result,
883 Err(FinError::SequenceMismatch {
884 expected: 1,
885 got: 2
886 })
887 ));
888 }
889
890 #[test]
891 fn test_orderbook_sequence_advances_correctly() {
892 let mut book = make_book();
893 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
894 .unwrap();
895 assert_eq!(book.sequence(), 1);
896 book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
897 .unwrap();
898 assert_eq!(book.sequence(), 2);
899 }
900
901 #[test]
902 fn test_orderbook_best_bid_max_price() {
903 let mut book = make_book();
904 book.apply_delta(set_delta(Side::Bid, "99", "10", 1))
905 .unwrap();
906 book.apply_delta(set_delta(Side::Bid, "100", "5", 2))
907 .unwrap();
908 book.apply_delta(set_delta(Side::Bid, "98", "20", 3))
909 .unwrap();
910 let best = book.best_bid().unwrap();
911 assert_eq!(best.price.value(), dec!(100));
912 }
913
914 #[test]
915 fn test_orderbook_best_ask_min_price() {
916 let mut book = make_book();
917 book.apply_delta(set_delta(Side::Ask, "102", "10", 1))
918 .unwrap();
919 book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
920 .unwrap();
921 book.apply_delta(set_delta(Side::Ask, "103", "20", 3))
922 .unwrap();
923 let best = book.best_ask().unwrap();
924 assert_eq!(best.price.value(), dec!(101));
925 }
926
927 #[test]
928 fn test_orderbook_spread_positive() {
929 let mut book = make_book();
930 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
931 .unwrap();
932 book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
933 .unwrap();
934 let spread = book.spread().unwrap();
935 assert_eq!(spread, dec!(1));
936 assert!(spread > Decimal::ZERO);
937 }
938
939 #[test]
940 fn test_orderbook_mid_price() {
941 let mut book = make_book();
942 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
943 .unwrap();
944 book.apply_delta(set_delta(Side::Ask, "102", "5", 2))
945 .unwrap();
946 let mid = book.mid_price().unwrap();
947 assert_eq!(mid, dec!(101));
948 }
949
950 #[test]
951 fn test_orderbook_spread_none_when_empty() {
952 let book = make_book();
953 assert!(book.spread().is_none());
954 }
955
956 #[test]
957 fn test_orderbook_vwap_insufficient_liquidity() {
958 let mut book = make_book();
959 book.apply_delta(set_delta(Side::Ask, "101", "5", 1))
960 .unwrap();
961 let result = book.vwap_for_qty(Side::Ask, Quantity::new(dec!(100)).unwrap());
962 assert!(matches!(result, Err(FinError::InsufficientLiquidity(_))));
963 }
964
965 #[test]
966 fn test_orderbook_vwap_single_level() {
967 let mut book = make_book();
968 book.apply_delta(set_delta(Side::Ask, "100", "10", 1))
969 .unwrap();
970 let vwap = book
971 .vwap_for_qty(Side::Ask, Quantity::new(dec!(5)).unwrap())
972 .unwrap();
973 assert_eq!(vwap, dec!(100));
974 }
975
976 #[test]
977 fn test_orderbook_vwap_multi_level() {
978 let mut book = make_book();
979 book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
980 .unwrap();
981 book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
982 .unwrap();
983 let vwap = book
985 .vwap_for_qty(Side::Ask, Quantity::new(dec!(10)).unwrap())
986 .unwrap();
987 assert_eq!(vwap, dec!(100.5));
988 }
989
990 #[test]
991 fn test_orderbook_remove_level_delta() {
992 let mut book = make_book();
993 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
994 .unwrap();
995 book.apply_delta(remove_delta(Side::Bid, "100", 2)).unwrap();
996 assert!(book.best_bid().is_none());
997 }
998
999 #[test]
1000 fn test_orderbook_top_bids_order() {
1001 let mut book = make_book();
1002 book.apply_delta(set_delta(Side::Bid, "98", "10", 1))
1003 .unwrap();
1004 book.apply_delta(set_delta(Side::Bid, "100", "5", 2))
1005 .unwrap();
1006 book.apply_delta(set_delta(Side::Bid, "99", "20", 3))
1007 .unwrap();
1008 let top = book.top_bids(2);
1009 assert_eq!(top[0].price.value(), dec!(100));
1010 assert_eq!(top[1].price.value(), dec!(99));
1011 }
1012
1013 #[test]
1014 fn test_orderbook_top_asks_order() {
1015 let mut book = make_book();
1016 book.apply_delta(set_delta(Side::Ask, "103", "10", 1))
1017 .unwrap();
1018 book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
1019 .unwrap();
1020 book.apply_delta(set_delta(Side::Ask, "102", "20", 3))
1021 .unwrap();
1022 let top = book.top_asks(2);
1023 assert_eq!(top[0].price.value(), dec!(101));
1024 assert_eq!(top[1].price.value(), dec!(102));
1025 }
1026
1027 #[test]
1028 fn test_orderbook_bid_count_ask_count() {
1029 let mut book = make_book();
1030 book.apply_delta(set_delta(Side::Bid, "100", "1", 1))
1031 .unwrap();
1032 book.apply_delta(set_delta(Side::Ask, "101", "1", 2))
1033 .unwrap();
1034 assert_eq!(book.bid_count(), 1);
1035 assert_eq!(book.ask_count(), 1);
1036 }
1037
1038 #[test]
1039 fn test_orderbook_vwap_zero_qty_returns_zero() {
1040 let mut book = make_book();
1041 book.apply_delta(set_delta(Side::Ask, "100", "10", 1))
1042 .unwrap();
1043 let vwap = book.vwap_for_qty(Side::Ask, Quantity::zero()).unwrap();
1044 assert_eq!(vwap, Decimal::ZERO);
1045 }
1046
1047 #[test]
1050 fn test_apply_delta_rejects_inverted_spread() {
1051 let mut book = make_book();
1052 book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1054 .unwrap();
1055 let result = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1057 assert!(
1058 matches!(result, Err(FinError::InvertedSpread { .. })),
1059 "expected InvertedSpread, got {:?}",
1060 result
1061 );
1062 }
1063
1064 #[test]
1065 fn test_apply_delta_inverted_spread_rolls_back_sequence() {
1066 let mut book = make_book();
1067 book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1068 .unwrap();
1069 assert_eq!(book.sequence(), 1);
1070 let _ = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1072 assert_eq!(
1073 book.sequence(),
1074 1,
1075 "sequence must not advance on rejected delta"
1076 );
1077 }
1078
1079 #[test]
1080 fn test_apply_delta_inverted_spread_rolled_back_book_state() {
1081 let mut book = make_book();
1082 book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1083 .unwrap();
1084 let _ = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1086 assert!(
1087 book.best_bid().is_none(),
1088 "rejected bid must not appear in book"
1089 );
1090 }
1091
1092 #[test]
1094 fn test_empty_book_mid_price_returns_none() {
1095 let book = make_book();
1096 assert!(
1097 book.mid_price().is_none(),
1098 "empty book mid_price must be None"
1099 );
1100 }
1101
1102 #[test]
1104 fn test_empty_book_best_bid_returns_none() {
1105 let book = make_book();
1106 assert!(book.best_bid().is_none());
1107 }
1108
1109 #[test]
1111 fn test_empty_book_best_ask_returns_none() {
1112 let book = make_book();
1113 assert!(book.best_ask().is_none());
1114 }
1115
1116 #[test]
1118 fn test_best_bid_after_many_inserts_and_removes() {
1119 let mut book = make_book();
1120 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
1121 .unwrap();
1122 book.apply_delta(set_delta(Side::Bid, "105", "5", 2))
1123 .unwrap();
1124 book.apply_delta(set_delta(Side::Bid, "103", "8", 3))
1125 .unwrap();
1126 book.apply_delta(remove_delta(Side::Bid, "105", 4)).unwrap();
1128 let best = book.best_bid().unwrap();
1129 assert_eq!(
1130 best.price.value(),
1131 dec!(103),
1132 "best bid after removing top level must be 103"
1133 );
1134 }
1135
1136 #[test]
1137 fn test_best_ask_after_many_inserts_and_removes() {
1138 let mut book = make_book();
1139 book.apply_delta(set_delta(Side::Ask, "110", "10", 1))
1140 .unwrap();
1141 book.apply_delta(set_delta(Side::Ask, "108", "5", 2))
1142 .unwrap();
1143 book.apply_delta(set_delta(Side::Ask, "109", "8", 3))
1144 .unwrap();
1145 book.apply_delta(remove_delta(Side::Ask, "108", 4)).unwrap();
1147 let best = book.best_ask().unwrap();
1148 assert_eq!(
1149 best.price.value(),
1150 dec!(109),
1151 "best ask after removing top level must be 109"
1152 );
1153 }
1154
1155 #[test]
1157 fn test_crossed_book_ask_at_bid_price_rejected() {
1158 let mut book = make_book();
1159 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
1160 .unwrap();
1161 let result = book.apply_delta(set_delta(Side::Ask, "100", "5", 2));
1162 assert!(
1163 matches!(result, Err(FinError::InvertedSpread { .. })),
1164 "ask at bid price must produce InvertedSpread"
1165 );
1166 }
1167
1168 #[test]
1170 fn test_empty_book_spread_returns_none() {
1171 let book = make_book();
1172 assert!(book.spread().is_none());
1173 }
1174
1175 #[test]
1176 fn test_orderbook_snapshot_returns_top_n_both_sides() {
1177 let mut book = make_book();
1178 book.apply_delta(set_delta(Side::Bid, "99", "10", 1)).unwrap();
1179 book.apply_delta(set_delta(Side::Bid, "100", "5", 2)).unwrap();
1180 book.apply_delta(set_delta(Side::Ask, "101", "3", 3)).unwrap();
1181 book.apply_delta(set_delta(Side::Ask, "102", "7", 4)).unwrap();
1182 let (bids, asks) = book.snapshot(2);
1183 assert_eq!(bids.len(), 2);
1184 assert_eq!(asks.len(), 2);
1185 assert_eq!(bids[0].price.value(), dec!(100));
1186 assert_eq!(asks[0].price.value(), dec!(101));
1187 }
1188
1189 #[test]
1190 fn test_orderbook_snapshot_empty_book() {
1191 let book = make_book();
1192 let (bids, asks) = book.snapshot(5);
1193 assert!(bids.is_empty());
1194 assert!(asks.is_empty());
1195 }
1196
1197 #[test]
1198 fn test_orderbook_clear_removes_all_levels() {
1199 let mut book = make_book();
1200 book.apply_delta(set_delta(Side::Bid, "99", "10", 1)).unwrap();
1201 book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1202 assert_eq!(book.bid_count(), 1);
1203 assert_eq!(book.ask_count(), 1);
1204 book.clear();
1205 assert_eq!(book.bid_count(), 0);
1206 assert_eq!(book.ask_count(), 0);
1207 assert_eq!(book.sequence(), 0);
1208 }
1209
1210 #[test]
1211 fn test_orderbook_clear_allows_fresh_deltas() {
1212 let mut book = make_book();
1213 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1214 book.clear();
1215 assert!(book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).is_ok());
1217 }
1218
1219 #[test]
1220 fn test_orderbook_total_bid_volume() {
1221 let mut book = make_book();
1222 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1223 book.apply_delta(set_delta(Side::Bid, "99", "3", 2)).unwrap();
1224 assert_eq!(book.total_bid_volume(), dec!(8));
1225 }
1226
1227 #[test]
1228 fn test_orderbook_total_ask_volume() {
1229 let mut book = make_book();
1230 book.apply_delta(set_delta(Side::Ask, "101", "4", 1)).unwrap();
1231 book.apply_delta(set_delta(Side::Ask, "102", "6", 2)).unwrap();
1232 assert_eq!(book.total_ask_volume(), dec!(10));
1233 }
1234
1235 #[test]
1236 fn test_orderbook_total_bid_volume_empty() {
1237 let book = make_book();
1238 assert_eq!(book.total_bid_volume(), dec!(0));
1239 }
1240
1241 #[test]
1242 fn test_orderbook_imbalance_balanced() {
1243 let mut book = make_book();
1244 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1245 book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1246 assert_eq!(book.imbalance().unwrap(), dec!(0));
1247 }
1248
1249 #[test]
1250 fn test_orderbook_imbalance_bid_heavy() {
1251 let mut book = make_book();
1252 book.apply_delta(set_delta(Side::Bid, "100", "9", 1)).unwrap();
1253 book.apply_delta(set_delta(Side::Ask, "101", "1", 2)).unwrap();
1254 assert_eq!(book.imbalance().unwrap(), dec!(0.8));
1256 }
1257
1258 #[test]
1259 fn test_orderbook_imbalance_ask_heavy() {
1260 let mut book = make_book();
1261 book.apply_delta(set_delta(Side::Bid, "100", "1", 1)).unwrap();
1262 book.apply_delta(set_delta(Side::Ask, "101", "9", 2)).unwrap();
1263 assert_eq!(book.imbalance().unwrap(), dec!(-0.8));
1265 }
1266
1267 #[test]
1268 fn test_orderbook_imbalance_empty_returns_none() {
1269 let book = make_book();
1270 assert!(book.imbalance().is_none());
1271 }
1272
1273 #[test]
1274 fn test_orderbook_has_price_bid_present() {
1275 let mut book = make_book();
1276 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1277 let price = Price::new(dec!(100)).unwrap();
1278 assert!(book.has_price(Side::Bid, price));
1279 assert!(!book.has_price(Side::Ask, price));
1280 }
1281
1282 #[test]
1283 fn test_orderbook_has_price_ask_present() {
1284 let mut book = make_book();
1285 book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1286 let price = Price::new(dec!(101)).unwrap();
1287 assert!(book.has_price(Side::Ask, price));
1288 assert!(!book.has_price(Side::Bid, price));
1289 }
1290
1291 #[test]
1292 fn test_orderbook_has_price_absent() {
1293 let book = make_book();
1294 let price = Price::new(dec!(100)).unwrap();
1295 assert!(!book.has_price(Side::Bid, price));
1296 assert!(!book.has_price(Side::Ask, price));
1297 }
1298
1299 #[test]
1300 fn test_orderbook_has_price_false_after_remove() {
1301 let mut book = make_book();
1302 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1303 book.apply_delta(BookDelta {
1304 side: Side::Bid,
1305 price: Price::new(dec!(100)).unwrap(),
1306 quantity: Quantity::zero(),
1307 action: DeltaAction::Remove,
1308 sequence: 2,
1309 })
1310 .unwrap();
1311 let price = Price::new(dec!(100)).unwrap();
1312 assert!(!book.has_price(Side::Bid, price));
1313 }
1314
1315 #[test]
1316 fn test_orderbook_level_count_bids() {
1317 let mut book = make_book();
1318 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1319 book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1320 assert_eq!(book.level_count(Side::Bid), 2);
1321 assert_eq!(book.level_count(Side::Ask), 0);
1322 }
1323
1324 #[test]
1325 fn test_orderbook_level_count_asks() {
1326 let mut book = make_book();
1327 book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1328 assert_eq!(book.level_count(Side::Ask), 1);
1329 assert_eq!(book.level_count(Side::Bid), 0);
1330 }
1331
1332 #[test]
1333 fn test_orderbook_weighted_mid_equal_qty() {
1334 let mut book = make_book();
1335 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1336 book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1337 assert_eq!(book.weighted_mid().unwrap(), dec!(101));
1339 }
1340
1341 #[test]
1342 fn test_orderbook_weighted_mid_bid_heavy() {
1343 let mut book = make_book();
1344 book.apply_delta(set_delta(Side::Bid, "100", "9", 1)).unwrap();
1345 book.apply_delta(set_delta(Side::Ask, "110", "1", 2)).unwrap();
1346 assert_eq!(book.weighted_mid().unwrap(), dec!(109));
1348 }
1349
1350 #[test]
1351 fn test_orderbook_weighted_mid_empty_returns_none() {
1352 let book = make_book();
1353 assert!(book.weighted_mid().is_none());
1354 }
1355
1356 #[test]
1357 fn test_orderbook_bid_ask_ratio_equal_volumes() {
1358 let mut book = make_book();
1359 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1360 book.apply_delta(set_delta(Side::Ask, "101", "10", 2)).unwrap();
1361 assert_eq!(book.bid_ask_ratio().unwrap(), dec!(1));
1362 }
1363
1364 #[test]
1365 fn test_orderbook_bid_ask_ratio_bid_heavy() {
1366 let mut book = make_book();
1367 book.apply_delta(set_delta(Side::Bid, "100", "20", 1)).unwrap();
1368 book.apply_delta(set_delta(Side::Ask, "101", "10", 2)).unwrap();
1369 assert_eq!(book.bid_ask_ratio().unwrap(), dec!(2));
1370 }
1371
1372 #[test]
1373 fn test_orderbook_bid_ask_ratio_empty_returns_none() {
1374 let book = make_book();
1375 assert!(book.bid_ask_ratio().is_none());
1376 }
1377
1378 #[test]
1379 fn test_orderbook_price_impact_buy_single_level() {
1380 let mut book = make_book();
1381 book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1382 let qty = Quantity::new(dec!(5)).unwrap();
1383 let avg = book.price_impact(Side::Bid, qty).unwrap();
1384 assert_eq!(avg, dec!(101));
1385 }
1386
1387 #[test]
1388 fn test_orderbook_price_impact_buy_spans_two_levels() {
1389 let mut book = make_book();
1390 book.apply_delta(set_delta(Side::Ask, "100", "5", 1)).unwrap();
1391 book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1392 let qty = Quantity::new(dec!(10)).unwrap();
1394 let avg = book.price_impact(Side::Bid, qty).unwrap();
1395 assert_eq!(avg, dec!(101));
1396 }
1397
1398 #[test]
1399 fn test_orderbook_price_impact_insufficient_depth_returns_none() {
1400 let mut book = make_book();
1401 book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1402 let qty = Quantity::new(dec!(10)).unwrap();
1403 assert!(book.price_impact(Side::Bid, qty).is_none());
1404 }
1405
1406 #[test]
1407 fn test_orderbook_price_impact_zero_qty_returns_none() {
1408 let mut book = make_book();
1409 book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1410 let qty = Quantity::zero();
1411 assert!(book.price_impact(Side::Bid, qty).is_none());
1412 }
1413
1414 #[test]
1415 fn test_orderbook_depth_at_existing_bid_level() {
1416 let mut book = make_book();
1417 book.apply_delta(set_delta(Side::Bid, "99", "5", 1)).unwrap();
1419 let price = Price::new(dec!(99)).unwrap();
1420 assert_eq!(book.depth_at(Side::Bid, price), Some(dec!(5)));
1421 }
1422
1423 #[test]
1424 fn test_orderbook_depth_at_absent_level_returns_none() {
1425 let book = make_book();
1426 let price = Price::new(dec!(50)).unwrap();
1427 assert!(book.depth_at(Side::Bid, price).is_none());
1428 assert!(book.depth_at(Side::Ask, price).is_none());
1429 }
1430
1431 #[test]
1432 fn test_orderbook_bid_depth_returns_top_n_descending() {
1433 let mut book = make_book();
1434 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1435 book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1436 book.apply_delta(set_delta(Side::Bid, "98", "3", 3)).unwrap();
1437 let levels = book.bid_depth(2);
1438 assert_eq!(levels.len(), 2);
1439 assert_eq!(levels[0].price.value(), dec!(100)); assert_eq!(levels[1].price.value(), dec!(99));
1441 }
1442
1443 #[test]
1444 fn test_orderbook_ask_depth_returns_top_n_ascending() {
1445 let mut book = make_book();
1446 book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1447 book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1448 book.apply_delta(set_delta(Side::Ask, "103", "3", 3)).unwrap();
1449 let levels = book.ask_depth(2);
1450 assert_eq!(levels.len(), 2);
1451 assert_eq!(levels[0].price.value(), dec!(101)); assert_eq!(levels[1].price.value(), dec!(102));
1453 }
1454
1455 #[test]
1456 fn test_orderbook_bid_depth_fewer_than_n() {
1457 let mut book = make_book();
1458 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1459 let levels = book.bid_depth(5);
1460 assert_eq!(levels.len(), 1);
1461 }
1462
1463 #[test]
1464 fn test_orderbook_ask_depth_empty_book() {
1465 let book = make_book();
1466 assert!(book.ask_depth(3).is_empty());
1467 }
1468
1469 #[test]
1470 fn test_orderbook_remove_all_bids_clears_bid_side() {
1471 let mut book = make_book();
1472 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1473 book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1474 book.remove_all(Side::Bid);
1475 assert!(book.best_bid().is_none());
1476 }
1477
1478 #[test]
1479 fn test_orderbook_remove_all_bids_leaves_asks_intact() {
1480 let mut book = make_book();
1481 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1482 book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1483 book.remove_all(Side::Bid);
1484 assert!(book.best_bid().is_none());
1485 assert!(book.best_ask().is_some());
1486 }
1487
1488 #[test]
1489 fn test_orderbook_remove_all_asks_clears_ask_side() {
1490 let mut book = make_book();
1491 book.apply_delta(set_delta(Side::Ask, "101", "5", 1)).unwrap();
1492 book.apply_delta(set_delta(Side::Ask, "102", "3", 2)).unwrap();
1493 book.remove_all(Side::Ask);
1494 assert!(book.best_ask().is_none());
1495 }
1496
1497 #[test]
1498 fn test_orderbook_total_levels_sums_both_sides() {
1499 let mut book = make_book();
1500 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1501 book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1502 book.apply_delta(set_delta(Side::Ask, "101", "8", 3)).unwrap();
1503 assert_eq!(book.total_levels(), 3);
1504 }
1505
1506 #[test]
1507 fn test_orderbook_total_levels_empty_book() {
1508 let book = make_book();
1509 assert_eq!(book.total_levels(), 0);
1510 }
1511}