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 match delta.action {
128 DeltaAction::Set => match delta.side {
129 Side::Bid => {
130 self.bids.remove(&delta.price.value());
131 }
132 Side::Ask => {
133 self.asks.remove(&delta.price.value());
134 }
135 },
136 DeltaAction::Remove => match delta.side {
139 Side::Bid => {
140 if let Some(qty) = prev_val {
141 self.bids.insert(delta.price.value(), qty);
142 }
143 }
144 Side::Ask => {
145 if let Some(qty) = prev_val {
146 self.asks.insert(delta.price.value(), qty);
147 }
148 }
149 },
150 }
151 self.sequence = expected - 1;
152 return Err(FinError::InvertedSpread {
153 best_bid: best_bid_p,
154 best_ask: best_ask_p,
155 });
156 }
157
158 Ok(())
159 }
160
161 pub fn best_bid(&self) -> Option<PriceLevel> {
166 self.bids.iter().next_back().and_then(|(p, q)| {
167 Some(PriceLevel {
168 price: Price::new(*p).ok()?,
169 quantity: Quantity::new(*q).unwrap_or_else(|_| Quantity::zero()),
170 })
171 })
172 }
173
174 pub fn best_quote(&self) -> Option<(PriceLevel, PriceLevel)> {
178 Some((self.best_bid()?, self.best_ask()?))
179 }
180
181 pub fn best_ask(&self) -> Option<PriceLevel> {
186 self.asks.iter().next().and_then(|(p, q)| {
187 Some(PriceLevel {
188 price: Price::new(*p).ok()?,
189 quantity: Quantity::new(*q).unwrap_or_else(|_| Quantity::zero()),
190 })
191 })
192 }
193
194 pub fn mid_price(&self) -> Option<Decimal> {
196 let bid = self.best_bid()?.price.value();
197 let ask = self.best_ask()?.price.value();
198 Some((bid + ask) / Decimal::TWO)
199 }
200
201 pub fn spread(&self) -> Option<Decimal> {
203 let bid = self.best_bid()?.price.value();
204 let ask = self.best_ask()?.price.value();
205 Some(ask - bid)
206 }
207
208 pub fn spread_pct(&self) -> Option<Decimal> {
212 let mid = self.mid_price()?;
213 if mid.is_zero() {
214 return None;
215 }
216 let spread = self.spread()?;
217 Some(spread / mid * Decimal::ONE_HUNDRED)
218 }
219
220 pub fn depth_at(&self, side: Side, price: Price) -> Option<Decimal> {
222 let key = price.value();
223 match side {
224 Side::Bid => self.bids.get(&key).copied(),
225 Side::Ask => self.asks.get(&key).copied(),
226 }
227 }
228
229 pub fn top_bids(&self, n: usize) -> Vec<PriceLevel> {
231 self.bids
232 .iter()
233 .rev()
234 .take(n)
235 .filter_map(|(p, q)| {
236 let price = Price::new(*p).ok()?;
237 let quantity = Quantity::new(*q).ok()?;
238 Some(PriceLevel { price, quantity })
239 })
240 .collect()
241 }
242
243 pub fn top_asks(&self, n: usize) -> Vec<PriceLevel> {
245 self.asks
246 .iter()
247 .take(n)
248 .filter_map(|(p, q)| {
249 let price = Price::new(*p).ok()?;
250 let quantity = Quantity::new(*q).ok()?;
251 Some(PriceLevel { price, quantity })
252 })
253 .collect()
254 }
255
256 pub fn vwap_for_qty(&self, side: Side, qty: Quantity) -> Result<Decimal, FinError> {
263 let target = qty.value();
264 if target <= Decimal::ZERO {
265 return Ok(Decimal::ZERO);
266 }
267 match side {
268 Side::Bid => Self::vwap_fill(self.bids.iter().rev(), target),
269 Side::Ask => Self::vwap_fill(self.asks.iter(), target),
270 }
271 }
272
273 fn vwap_fill<'a>(
274 levels: impl Iterator<Item = (&'a Decimal, &'a Decimal)>,
275 target: Decimal,
276 ) -> Result<Decimal, FinError> {
277 let mut remaining = target;
278 let mut total_cost = Decimal::ZERO;
279
280 for (price, avail_qty) in levels {
281 let fill = remaining.min(*avail_qty);
282 total_cost += fill * price;
283 remaining -= fill;
284 if remaining <= Decimal::ZERO {
285 break;
286 }
287 }
288
289 if remaining > Decimal::ZERO {
290 return Err(FinError::InsufficientLiquidity(target));
291 }
292
293 Ok(total_cost / target)
294 }
295
296 pub fn sequence(&self) -> u64 {
298 self.sequence
299 }
300
301 pub fn snapshot(&self, n: usize) -> (Vec<PriceLevel>, Vec<PriceLevel>) {
306 (self.top_bids(n), self.top_asks(n))
307 }
308
309 pub fn bid_count(&self) -> usize {
311 self.bids.len()
312 }
313
314 pub fn ask_count(&self) -> usize {
316 self.asks.len()
317 }
318
319 pub fn level_count(&self, side: Side) -> usize {
321 match side {
322 Side::Bid => self.bids.len(),
323 Side::Ask => self.asks.len(),
324 }
325 }
326
327 pub fn clear(&mut self) {
329 self.bids.clear();
330 self.asks.clear();
331 self.sequence = 0;
332 }
333
334 pub fn remove_all(&mut self, side: crate::types::Side) {
338 use crate::types::Side;
339 match side {
340 Side::Bid => self.bids.clear(),
341 Side::Ask => self.asks.clear(),
342 }
343 }
344
345 pub fn is_crossed(&self) -> bool {
351 match (self.best_bid(), self.best_ask()) {
352 (Some(bid), Some(ask)) => bid.price >= ask.price,
353 _ => false,
354 }
355 }
356
357 pub fn is_empty(&self) -> bool {
359 self.bids.is_empty() && self.asks.is_empty()
360 }
361
362 pub fn total_levels(&self) -> usize {
364 self.bids.len() + self.asks.len()
365 }
366
367 pub fn cumulative_depth(&self, side: Side, price: Price) -> Decimal {
374 let p = price.value();
375 match side {
376 Side::Bid => self
377 .bids
378 .range(p..)
379 .map(|(_, qty)| *qty)
380 .sum(),
381 Side::Ask => self
382 .asks
383 .range(..=p)
384 .map(|(_, qty)| *qty)
385 .sum(),
386 }
387 }
388
389 pub fn total_bid_volume(&self) -> Decimal {
391 self.bids.values().copied().sum()
392 }
393
394 pub fn total_ask_volume(&self) -> Decimal {
396 self.asks.values().copied().sum()
397 }
398
399 pub fn best_bid_price(&self) -> Option<Price> {
401 self.bids.keys().next_back().and_then(|p| Price::new(*p).ok())
402 }
403
404 pub fn best_ask_price(&self) -> Option<Price> {
406 self.asks.keys().next().and_then(|p| Price::new(*p).ok())
407 }
408
409 pub fn best_bid_qty(&self) -> Option<Quantity> {
411 self.bids
412 .values()
413 .next_back()
414 .and_then(|q| Quantity::new(*q).ok())
415 }
416
417 pub fn best_ask_qty(&self) -> Option<Quantity> {
419 self.asks
420 .values()
421 .next()
422 .and_then(|q| Quantity::new(*q).ok())
423 }
424
425 pub fn liquidity_at_pct(&self, side: Side, pct_from_mid: Decimal) -> Option<Decimal> {
430 let mid = self.mid_price()?;
431 let band = mid * pct_from_mid / Decimal::ONE_HUNDRED;
432 let (lo, hi) = match side {
433 Side::Bid => (mid - band, mid),
434 Side::Ask => (mid, mid + band),
435 };
436 let qty: Decimal = match side {
437 Side::Bid => self
438 .bids
439 .range(lo..=hi)
440 .map(|(_, q)| *q)
441 .sum(),
442 Side::Ask => self
443 .asks
444 .range(lo..=hi)
445 .map(|(_, q)| *q)
446 .sum(),
447 };
448 Some(qty)
449 }
450
451 pub fn has_price(&self, side: Side, price: Price) -> bool {
453 let key = price.value();
454 match side {
455 Side::Bid => self.bids.contains_key(&key),
456 Side::Ask => self.asks.contains_key(&key),
457 }
458 }
459
460 pub fn weighted_mid(&self) -> Option<Decimal> {
466 let bid = self.best_bid()?;
467 let ask = self.best_ask()?;
468 let bid_qty = bid.quantity.value();
469 let ask_qty = ask.quantity.value();
470 let total = bid_qty + ask_qty;
471 if total.is_zero() {
472 return None;
473 }
474 Some((bid.price.value() * ask_qty + ask.price.value() * bid_qty) / total)
475 }
476
477 pub fn imbalance(&self) -> Option<Decimal> {
482 let bid_vol = self.total_bid_volume();
483 let ask_vol = self.total_ask_volume();
484 let total = bid_vol + ask_vol;
485 if total == Decimal::ZERO {
486 return None;
487 }
488 Some((bid_vol - ask_vol) / total)
489 }
490
491 pub fn depth_ratio(&self, n: usize) -> Option<Decimal> {
496 let bid_vol: Decimal = self.bids.values().rev().take(n).copied().sum();
497 let ask_vol: Decimal = self.asks.values().take(n).copied().sum();
498 if ask_vol.is_zero() {
499 return None;
500 }
501 Some(bid_vol / ask_vol)
502 }
503
504 #[deprecated(since = "2.1.0", note = "Use `weighted_mid` instead")]
510 pub fn weighted_mid_price(&self) -> Option<Decimal> {
511 self.weighted_mid()
512 }
513
514 pub fn price_levels_between(&self, side: Side, lo: Price, hi: Price) -> Vec<PriceLevel> {
518 let lo_val = lo.value();
519 let hi_val = hi.value();
520 match side {
521 Side::Bid => self
522 .bids
523 .range(lo_val..=hi_val)
524 .map(|(p, q)| PriceLevel {
525 price: Price::new(*p).unwrap_or(lo),
526 quantity: crate::types::Quantity::new(*q).unwrap_or_else(|_| crate::types::Quantity::zero()),
527 })
528 .collect(),
529 Side::Ask => self
530 .asks
531 .range(lo_val..=hi_val)
532 .map(|(p, q)| PriceLevel {
533 price: Price::new(*p).unwrap_or(lo),
534 quantity: crate::types::Quantity::new(*q).unwrap_or_else(|_| crate::types::Quantity::zero()),
535 })
536 .collect(),
537 }
538 }
539
540 pub fn tick_size(&self) -> Option<Decimal> {
545 let bid_tick = self
546 .bids
547 .keys()
548 .collect::<Vec<_>>()
549 .windows(2)
550 .map(|w| (*w[1] - *w[0]).abs())
551 .filter(|d| !d.is_zero())
552 .reduce(Decimal::min);
553 let ask_tick = self
554 .asks
555 .keys()
556 .collect::<Vec<_>>()
557 .windows(2)
558 .map(|w| (*w[1] - *w[0]).abs())
559 .filter(|d| !d.is_zero())
560 .reduce(Decimal::min);
561 match (bid_tick, ask_tick) {
562 (Some(b), Some(a)) => Some(b.min(a)),
563 (Some(b), None) => Some(b),
564 (None, Some(a)) => Some(a),
565 (None, None) => None,
566 }
567 }
568
569 pub fn bid_ask_ratio(&self) -> Option<Decimal> {
574 let bid = self.total_bid_volume();
575 let ask = self.total_ask_volume();
576 if ask.is_zero() || bid.is_zero() {
577 return None;
578 }
579 Some(bid / ask)
580 }
581
582 pub fn price_impact(&self, side: crate::types::Side, qty: crate::types::Quantity) -> Option<Decimal> {
588 use crate::types::Side;
589 if qty.is_zero() {
590 return None;
591 }
592 let levels: Vec<_> = match side {
593 Side::Bid => {
594 let mut asks: Vec<_> = self.asks.iter().collect();
596 asks.sort_by(|a, b| a.0.cmp(b.0));
597 asks.into_iter().map(|(p, q)| (*p, *q)).collect()
598 }
599 Side::Ask => {
600 let mut bids: Vec<_> = self.bids.iter().collect();
602 bids.sort_by(|a, b| b.0.cmp(a.0));
603 bids.into_iter().map(|(p, q)| (*p, *q)).collect()
604 }
605 };
606 let target = qty.value();
607 let mut remaining = target;
608 let mut notional = Decimal::ZERO;
609 for (price, level_qty) in levels {
610 let fill = level_qty.min(remaining);
611 notional += price * fill;
612 remaining -= fill;
613 if remaining <= Decimal::ZERO {
614 break;
615 }
616 }
617 if remaining > Decimal::ZERO {
618 None } else {
620 Some(notional / target)
621 }
622 }
623
624 pub fn bid_depth(&self, n: usize) -> Vec<PriceLevel> {
628 self.bids
629 .iter()
630 .rev()
631 .take(n)
632 .map(|(price, qty)| PriceLevel {
633 price: Price::new(*price).unwrap(),
634 quantity: Quantity::new(*qty).unwrap(),
635 })
636 .collect()
637 }
638
639 pub fn ask_depth(&self, n: usize) -> Vec<PriceLevel> {
643 self.asks
644 .iter()
645 .take(n)
646 .map(|(price, qty)| PriceLevel {
647 price: Price::new(*price).unwrap(),
648 quantity: Quantity::new(*qty).unwrap(),
649 })
650 .collect()
651 }
652
653 pub fn depth_imbalance(&self) -> Option<Decimal> {
660 let bid_qty: Decimal = self.bids.values().sum();
661 let ask_qty: Decimal = self.asks.values().sum();
662 let total = bid_qty + ask_qty;
663 if total.is_zero() {
664 return None;
665 }
666 Some((bid_qty - ask_qty) / total)
667 }
668
669 pub fn ask_bid_ratio(&self) -> Option<Decimal> {
674 let bid_qty: Decimal = self.bids.values().sum();
675 let ask_qty: Decimal = self.asks.values().sum();
676 if bid_qty.is_zero() {
677 return None;
678 }
679 Some(ask_qty / bid_qty)
680 }
681
682 pub fn total_bid_depth(&self) -> Decimal {
684 self.bids.values().sum()
685 }
686
687 pub fn total_ask_depth(&self) -> Decimal {
689 self.asks.values().sum()
690 }
691
692 pub fn price_at_volume(&self, side: Side, target_qty: Decimal) -> Option<Price> {
701 if target_qty.is_zero() {
702 return None;
703 }
704 let mut remaining = target_qty;
705 let mut last_price: Option<Price> = None;
706
707 match side {
708 Side::Ask => {
709 for (&px, &qty) in &self.asks {
710 last_price = Price::new(px).ok();
711 if qty >= remaining {
712 return last_price;
713 }
714 remaining -= qty;
715 }
716 }
717 Side::Bid => {
718 for (&px, &qty) in self.bids.iter().rev() {
719 last_price = Price::new(px).ok();
720 if qty >= remaining {
721 return last_price;
722 }
723 remaining -= qty;
724 }
725 }
726 }
727 last_price
728 }
729
730 pub fn top_n_bid_levels(&self, n: usize) -> Vec<PriceLevel> {
734 if n == 0 {
735 return vec![];
736 }
737 self.bids
738 .iter()
739 .rev()
740 .take(n)
741 .filter_map(|(&px, &qty)| {
742 let price = Price::new(px).ok()?;
743 let quantity = Quantity::new(qty).ok()?;
744 Some(PriceLevel { price, quantity })
745 })
746 .collect()
747 }
748
749 pub fn top_n_ask_levels(&self, n: usize) -> Vec<PriceLevel> {
753 if n == 0 {
754 return vec![];
755 }
756 self.asks
757 .iter()
758 .take(n)
759 .filter_map(|(&px, &qty)| {
760 let price = Price::new(px).ok()?;
761 let quantity = Quantity::new(qty).ok()?;
762 Some(PriceLevel { price, quantity })
763 })
764 .collect()
765 }
766
767 pub fn cumulative_bid_qty(&self, n: usize) -> Decimal {
772 if n == 0 {
773 return Decimal::ZERO;
774 }
775 self.bids.iter().rev().take(n).map(|(_, &qty)| qty).sum()
776 }
777
778 pub fn bid_depth_skew(&self, n: usize) -> Option<Decimal> {
784 if n == 0 {
785 return None;
786 }
787 let bid_qty = self.cumulative_bid_qty(n);
788 let ask_qty = self.cumulative_ask_qty(n);
789 let total = bid_qty + ask_qty;
790 if total.is_zero() {
791 return None;
792 }
793 bid_qty.checked_div(total)
794 }
795
796 pub fn spread_bps(&self) -> Option<Decimal> {
801 let bid = self.best_bid()?.price.value();
802 let ask = self.best_ask()?.price.value();
803 let mid = (bid + ask) / Decimal::TWO;
804 if mid.is_zero() {
805 return None;
806 }
807 let spread = ask - bid;
808 spread.checked_div(mid).map(|r| r * Decimal::from(10_000u32))
809 }
810
811 pub fn cumulative_ask_qty(&self, n: usize) -> Decimal {
816 if n == 0 {
817 return Decimal::ZERO;
818 }
819 self.asks.iter().take(n).map(|(_, &qty)| qty).sum()
820 }
821}
822
823#[cfg(test)]
824mod tests {
825 use super::*;
826 use rust_decimal_macros::dec;
827
828 fn make_book() -> OrderBook {
829 OrderBook::new(Symbol::new("AAPL").unwrap())
830 }
831
832 fn set_delta(side: Side, price: &str, qty: &str, seq: u64) -> BookDelta {
833 BookDelta {
834 side,
835 price: Price::new(price.parse().unwrap()).unwrap(),
836 quantity: Quantity::new(qty.parse().unwrap()).unwrap(),
837 action: DeltaAction::Set,
838 sequence: seq,
839 }
840 }
841
842 fn remove_delta(side: Side, price: &str, seq: u64) -> BookDelta {
843 BookDelta {
844 side,
845 price: Price::new(price.parse().unwrap()).unwrap(),
846 quantity: Quantity::zero(),
847 action: DeltaAction::Remove,
848 sequence: seq,
849 }
850 }
851
852 #[test]
853 fn test_orderbook_apply_delta_updates_bid() {
854 let mut book = make_book();
855 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
856 .unwrap();
857 let best = book.best_bid().unwrap();
858 assert_eq!(best.price.value(), dec!(100));
859 assert_eq!(best.quantity.value(), dec!(10));
860 }
861
862 #[test]
863 fn test_orderbook_apply_delta_updates_ask() {
864 let mut book = make_book();
865 book.apply_delta(set_delta(Side::Ask, "101", "5", 1))
866 .unwrap();
867 let best = book.best_ask().unwrap();
868 assert_eq!(best.price.value(), dec!(101));
869 assert_eq!(best.quantity.value(), dec!(5));
870 }
871
872 #[test]
873 fn test_orderbook_sequence_mismatch_returns_error() {
874 let mut book = make_book();
875 let result = book.apply_delta(set_delta(Side::Bid, "100", "10", 2));
876 assert!(matches!(
877 result,
878 Err(FinError::SequenceMismatch {
879 expected: 1,
880 got: 2
881 })
882 ));
883 }
884
885 #[test]
886 fn test_orderbook_sequence_advances_correctly() {
887 let mut book = make_book();
888 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
889 .unwrap();
890 assert_eq!(book.sequence(), 1);
891 book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
892 .unwrap();
893 assert_eq!(book.sequence(), 2);
894 }
895
896 #[test]
897 fn test_orderbook_best_bid_max_price() {
898 let mut book = make_book();
899 book.apply_delta(set_delta(Side::Bid, "99", "10", 1))
900 .unwrap();
901 book.apply_delta(set_delta(Side::Bid, "100", "5", 2))
902 .unwrap();
903 book.apply_delta(set_delta(Side::Bid, "98", "20", 3))
904 .unwrap();
905 let best = book.best_bid().unwrap();
906 assert_eq!(best.price.value(), dec!(100));
907 }
908
909 #[test]
910 fn test_orderbook_best_ask_min_price() {
911 let mut book = make_book();
912 book.apply_delta(set_delta(Side::Ask, "102", "10", 1))
913 .unwrap();
914 book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
915 .unwrap();
916 book.apply_delta(set_delta(Side::Ask, "103", "20", 3))
917 .unwrap();
918 let best = book.best_ask().unwrap();
919 assert_eq!(best.price.value(), dec!(101));
920 }
921
922 #[test]
923 fn test_orderbook_spread_positive() {
924 let mut book = make_book();
925 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
926 .unwrap();
927 book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
928 .unwrap();
929 let spread = book.spread().unwrap();
930 assert_eq!(spread, dec!(1));
931 assert!(spread > Decimal::ZERO);
932 }
933
934 #[test]
935 fn test_orderbook_mid_price() {
936 let mut book = make_book();
937 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
938 .unwrap();
939 book.apply_delta(set_delta(Side::Ask, "102", "5", 2))
940 .unwrap();
941 let mid = book.mid_price().unwrap();
942 assert_eq!(mid, dec!(101));
943 }
944
945 #[test]
946 fn test_orderbook_spread_none_when_empty() {
947 let book = make_book();
948 assert!(book.spread().is_none());
949 }
950
951 #[test]
952 fn test_orderbook_vwap_insufficient_liquidity() {
953 let mut book = make_book();
954 book.apply_delta(set_delta(Side::Ask, "101", "5", 1))
955 .unwrap();
956 let result = book.vwap_for_qty(Side::Ask, Quantity::new(dec!(100)).unwrap());
957 assert!(matches!(result, Err(FinError::InsufficientLiquidity(_))));
958 }
959
960 #[test]
961 fn test_orderbook_vwap_single_level() {
962 let mut book = make_book();
963 book.apply_delta(set_delta(Side::Ask, "100", "10", 1))
964 .unwrap();
965 let vwap = book
966 .vwap_for_qty(Side::Ask, Quantity::new(dec!(5)).unwrap())
967 .unwrap();
968 assert_eq!(vwap, dec!(100));
969 }
970
971 #[test]
972 fn test_orderbook_vwap_multi_level() {
973 let mut book = make_book();
974 book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
975 .unwrap();
976 book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
977 .unwrap();
978 let vwap = book
980 .vwap_for_qty(Side::Ask, Quantity::new(dec!(10)).unwrap())
981 .unwrap();
982 assert_eq!(vwap, dec!(100.5));
983 }
984
985 #[test]
986 fn test_orderbook_remove_level_delta() {
987 let mut book = make_book();
988 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
989 .unwrap();
990 book.apply_delta(remove_delta(Side::Bid, "100", 2)).unwrap();
991 assert!(book.best_bid().is_none());
992 }
993
994 #[test]
995 fn test_orderbook_top_bids_order() {
996 let mut book = make_book();
997 book.apply_delta(set_delta(Side::Bid, "98", "10", 1))
998 .unwrap();
999 book.apply_delta(set_delta(Side::Bid, "100", "5", 2))
1000 .unwrap();
1001 book.apply_delta(set_delta(Side::Bid, "99", "20", 3))
1002 .unwrap();
1003 let top = book.top_bids(2);
1004 assert_eq!(top[0].price.value(), dec!(100));
1005 assert_eq!(top[1].price.value(), dec!(99));
1006 }
1007
1008 #[test]
1009 fn test_orderbook_top_asks_order() {
1010 let mut book = make_book();
1011 book.apply_delta(set_delta(Side::Ask, "103", "10", 1))
1012 .unwrap();
1013 book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
1014 .unwrap();
1015 book.apply_delta(set_delta(Side::Ask, "102", "20", 3))
1016 .unwrap();
1017 let top = book.top_asks(2);
1018 assert_eq!(top[0].price.value(), dec!(101));
1019 assert_eq!(top[1].price.value(), dec!(102));
1020 }
1021
1022 #[test]
1023 fn test_orderbook_bid_count_ask_count() {
1024 let mut book = make_book();
1025 book.apply_delta(set_delta(Side::Bid, "100", "1", 1))
1026 .unwrap();
1027 book.apply_delta(set_delta(Side::Ask, "101", "1", 2))
1028 .unwrap();
1029 assert_eq!(book.bid_count(), 1);
1030 assert_eq!(book.ask_count(), 1);
1031 }
1032
1033 #[test]
1034 fn test_orderbook_vwap_zero_qty_returns_zero() {
1035 let mut book = make_book();
1036 book.apply_delta(set_delta(Side::Ask, "100", "10", 1))
1037 .unwrap();
1038 let vwap = book.vwap_for_qty(Side::Ask, Quantity::zero()).unwrap();
1039 assert_eq!(vwap, Decimal::ZERO);
1040 }
1041
1042 #[test]
1045 fn test_apply_delta_rejects_inverted_spread() {
1046 let mut book = make_book();
1047 book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1049 .unwrap();
1050 let result = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1052 assert!(
1053 matches!(result, Err(FinError::InvertedSpread { .. })),
1054 "expected InvertedSpread, got {:?}",
1055 result
1056 );
1057 }
1058
1059 #[test]
1060 fn test_apply_delta_inverted_spread_rolls_back_sequence() {
1061 let mut book = make_book();
1062 book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1063 .unwrap();
1064 assert_eq!(book.sequence(), 1);
1065 let _ = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1067 assert_eq!(
1068 book.sequence(),
1069 1,
1070 "sequence must not advance on rejected delta"
1071 );
1072 }
1073
1074 #[test]
1075 fn test_apply_delta_inverted_spread_rolled_back_book_state() {
1076 let mut book = make_book();
1077 book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1078 .unwrap();
1079 let _ = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1081 assert!(
1082 book.best_bid().is_none(),
1083 "rejected bid must not appear in book"
1084 );
1085 }
1086
1087 #[test]
1089 fn test_empty_book_mid_price_returns_none() {
1090 let book = make_book();
1091 assert!(
1092 book.mid_price().is_none(),
1093 "empty book mid_price must be None"
1094 );
1095 }
1096
1097 #[test]
1099 fn test_empty_book_best_bid_returns_none() {
1100 let book = make_book();
1101 assert!(book.best_bid().is_none());
1102 }
1103
1104 #[test]
1106 fn test_empty_book_best_ask_returns_none() {
1107 let book = make_book();
1108 assert!(book.best_ask().is_none());
1109 }
1110
1111 #[test]
1113 fn test_best_bid_after_many_inserts_and_removes() {
1114 let mut book = make_book();
1115 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
1116 .unwrap();
1117 book.apply_delta(set_delta(Side::Bid, "105", "5", 2))
1118 .unwrap();
1119 book.apply_delta(set_delta(Side::Bid, "103", "8", 3))
1120 .unwrap();
1121 book.apply_delta(remove_delta(Side::Bid, "105", 4)).unwrap();
1123 let best = book.best_bid().unwrap();
1124 assert_eq!(
1125 best.price.value(),
1126 dec!(103),
1127 "best bid after removing top level must be 103"
1128 );
1129 }
1130
1131 #[test]
1132 fn test_best_ask_after_many_inserts_and_removes() {
1133 let mut book = make_book();
1134 book.apply_delta(set_delta(Side::Ask, "110", "10", 1))
1135 .unwrap();
1136 book.apply_delta(set_delta(Side::Ask, "108", "5", 2))
1137 .unwrap();
1138 book.apply_delta(set_delta(Side::Ask, "109", "8", 3))
1139 .unwrap();
1140 book.apply_delta(remove_delta(Side::Ask, "108", 4)).unwrap();
1142 let best = book.best_ask().unwrap();
1143 assert_eq!(
1144 best.price.value(),
1145 dec!(109),
1146 "best ask after removing top level must be 109"
1147 );
1148 }
1149
1150 #[test]
1152 fn test_crossed_book_ask_at_bid_price_rejected() {
1153 let mut book = make_book();
1154 book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
1155 .unwrap();
1156 let result = book.apply_delta(set_delta(Side::Ask, "100", "5", 2));
1157 assert!(
1158 matches!(result, Err(FinError::InvertedSpread { .. })),
1159 "ask at bid price must produce InvertedSpread"
1160 );
1161 }
1162
1163 #[test]
1165 fn test_empty_book_spread_returns_none() {
1166 let book = make_book();
1167 assert!(book.spread().is_none());
1168 }
1169
1170 #[test]
1171 fn test_orderbook_snapshot_returns_top_n_both_sides() {
1172 let mut book = make_book();
1173 book.apply_delta(set_delta(Side::Bid, "99", "10", 1)).unwrap();
1174 book.apply_delta(set_delta(Side::Bid, "100", "5", 2)).unwrap();
1175 book.apply_delta(set_delta(Side::Ask, "101", "3", 3)).unwrap();
1176 book.apply_delta(set_delta(Side::Ask, "102", "7", 4)).unwrap();
1177 let (bids, asks) = book.snapshot(2);
1178 assert_eq!(bids.len(), 2);
1179 assert_eq!(asks.len(), 2);
1180 assert_eq!(bids[0].price.value(), dec!(100));
1181 assert_eq!(asks[0].price.value(), dec!(101));
1182 }
1183
1184 #[test]
1185 fn test_orderbook_snapshot_empty_book() {
1186 let book = make_book();
1187 let (bids, asks) = book.snapshot(5);
1188 assert!(bids.is_empty());
1189 assert!(asks.is_empty());
1190 }
1191
1192 #[test]
1193 fn test_orderbook_clear_removes_all_levels() {
1194 let mut book = make_book();
1195 book.apply_delta(set_delta(Side::Bid, "99", "10", 1)).unwrap();
1196 book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1197 assert_eq!(book.bid_count(), 1);
1198 assert_eq!(book.ask_count(), 1);
1199 book.clear();
1200 assert_eq!(book.bid_count(), 0);
1201 assert_eq!(book.ask_count(), 0);
1202 assert_eq!(book.sequence(), 0);
1203 }
1204
1205 #[test]
1206 fn test_orderbook_clear_allows_fresh_deltas() {
1207 let mut book = make_book();
1208 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1209 book.clear();
1210 assert!(book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).is_ok());
1212 }
1213
1214 #[test]
1215 fn test_orderbook_total_bid_volume() {
1216 let mut book = make_book();
1217 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1218 book.apply_delta(set_delta(Side::Bid, "99", "3", 2)).unwrap();
1219 assert_eq!(book.total_bid_volume(), dec!(8));
1220 }
1221
1222 #[test]
1223 fn test_orderbook_total_ask_volume() {
1224 let mut book = make_book();
1225 book.apply_delta(set_delta(Side::Ask, "101", "4", 1)).unwrap();
1226 book.apply_delta(set_delta(Side::Ask, "102", "6", 2)).unwrap();
1227 assert_eq!(book.total_ask_volume(), dec!(10));
1228 }
1229
1230 #[test]
1231 fn test_orderbook_total_bid_volume_empty() {
1232 let book = make_book();
1233 assert_eq!(book.total_bid_volume(), dec!(0));
1234 }
1235
1236 #[test]
1237 fn test_orderbook_imbalance_balanced() {
1238 let mut book = make_book();
1239 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1240 book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1241 assert_eq!(book.imbalance().unwrap(), dec!(0));
1242 }
1243
1244 #[test]
1245 fn test_orderbook_imbalance_bid_heavy() {
1246 let mut book = make_book();
1247 book.apply_delta(set_delta(Side::Bid, "100", "9", 1)).unwrap();
1248 book.apply_delta(set_delta(Side::Ask, "101", "1", 2)).unwrap();
1249 assert_eq!(book.imbalance().unwrap(), dec!(0.8));
1251 }
1252
1253 #[test]
1254 fn test_orderbook_imbalance_ask_heavy() {
1255 let mut book = make_book();
1256 book.apply_delta(set_delta(Side::Bid, "100", "1", 1)).unwrap();
1257 book.apply_delta(set_delta(Side::Ask, "101", "9", 2)).unwrap();
1258 assert_eq!(book.imbalance().unwrap(), dec!(-0.8));
1260 }
1261
1262 #[test]
1263 fn test_orderbook_imbalance_empty_returns_none() {
1264 let book = make_book();
1265 assert!(book.imbalance().is_none());
1266 }
1267
1268 #[test]
1269 fn test_orderbook_has_price_bid_present() {
1270 let mut book = make_book();
1271 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1272 let price = Price::new(dec!(100)).unwrap();
1273 assert!(book.has_price(Side::Bid, price));
1274 assert!(!book.has_price(Side::Ask, price));
1275 }
1276
1277 #[test]
1278 fn test_orderbook_has_price_ask_present() {
1279 let mut book = make_book();
1280 book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1281 let price = Price::new(dec!(101)).unwrap();
1282 assert!(book.has_price(Side::Ask, price));
1283 assert!(!book.has_price(Side::Bid, price));
1284 }
1285
1286 #[test]
1287 fn test_orderbook_has_price_absent() {
1288 let book = make_book();
1289 let price = Price::new(dec!(100)).unwrap();
1290 assert!(!book.has_price(Side::Bid, price));
1291 assert!(!book.has_price(Side::Ask, price));
1292 }
1293
1294 #[test]
1295 fn test_orderbook_has_price_false_after_remove() {
1296 let mut book = make_book();
1297 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1298 book.apply_delta(BookDelta {
1299 side: Side::Bid,
1300 price: Price::new(dec!(100)).unwrap(),
1301 quantity: Quantity::zero(),
1302 action: DeltaAction::Remove,
1303 sequence: 2,
1304 })
1305 .unwrap();
1306 let price = Price::new(dec!(100)).unwrap();
1307 assert!(!book.has_price(Side::Bid, price));
1308 }
1309
1310 #[test]
1311 fn test_orderbook_level_count_bids() {
1312 let mut book = make_book();
1313 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1314 book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1315 assert_eq!(book.level_count(Side::Bid), 2);
1316 assert_eq!(book.level_count(Side::Ask), 0);
1317 }
1318
1319 #[test]
1320 fn test_orderbook_level_count_asks() {
1321 let mut book = make_book();
1322 book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1323 assert_eq!(book.level_count(Side::Ask), 1);
1324 assert_eq!(book.level_count(Side::Bid), 0);
1325 }
1326
1327 #[test]
1328 fn test_orderbook_weighted_mid_equal_qty() {
1329 let mut book = make_book();
1330 book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1331 book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1332 assert_eq!(book.weighted_mid().unwrap(), dec!(101));
1334 }
1335
1336 #[test]
1337 fn test_orderbook_weighted_mid_bid_heavy() {
1338 let mut book = make_book();
1339 book.apply_delta(set_delta(Side::Bid, "100", "9", 1)).unwrap();
1340 book.apply_delta(set_delta(Side::Ask, "110", "1", 2)).unwrap();
1341 assert_eq!(book.weighted_mid().unwrap(), dec!(109));
1343 }
1344
1345 #[test]
1346 fn test_orderbook_weighted_mid_empty_returns_none() {
1347 let book = make_book();
1348 assert!(book.weighted_mid().is_none());
1349 }
1350
1351 #[test]
1352 fn test_orderbook_bid_ask_ratio_equal_volumes() {
1353 let mut book = make_book();
1354 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1355 book.apply_delta(set_delta(Side::Ask, "101", "10", 2)).unwrap();
1356 assert_eq!(book.bid_ask_ratio().unwrap(), dec!(1));
1357 }
1358
1359 #[test]
1360 fn test_orderbook_bid_ask_ratio_bid_heavy() {
1361 let mut book = make_book();
1362 book.apply_delta(set_delta(Side::Bid, "100", "20", 1)).unwrap();
1363 book.apply_delta(set_delta(Side::Ask, "101", "10", 2)).unwrap();
1364 assert_eq!(book.bid_ask_ratio().unwrap(), dec!(2));
1365 }
1366
1367 #[test]
1368 fn test_orderbook_bid_ask_ratio_empty_returns_none() {
1369 let book = make_book();
1370 assert!(book.bid_ask_ratio().is_none());
1371 }
1372
1373 #[test]
1374 fn test_orderbook_price_impact_buy_single_level() {
1375 let mut book = make_book();
1376 book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1377 let qty = Quantity::new(dec!(5)).unwrap();
1378 let avg = book.price_impact(Side::Bid, qty).unwrap();
1379 assert_eq!(avg, dec!(101));
1380 }
1381
1382 #[test]
1383 fn test_orderbook_price_impact_buy_spans_two_levels() {
1384 let mut book = make_book();
1385 book.apply_delta(set_delta(Side::Ask, "100", "5", 1)).unwrap();
1386 book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1387 let qty = Quantity::new(dec!(10)).unwrap();
1389 let avg = book.price_impact(Side::Bid, qty).unwrap();
1390 assert_eq!(avg, dec!(101));
1391 }
1392
1393 #[test]
1394 fn test_orderbook_price_impact_insufficient_depth_returns_none() {
1395 let mut book = make_book();
1396 book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1397 let qty = Quantity::new(dec!(10)).unwrap();
1398 assert!(book.price_impact(Side::Bid, qty).is_none());
1399 }
1400
1401 #[test]
1402 fn test_orderbook_price_impact_zero_qty_returns_none() {
1403 let mut book = make_book();
1404 book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1405 let qty = Quantity::zero();
1406 assert!(book.price_impact(Side::Bid, qty).is_none());
1407 }
1408
1409 #[test]
1410 fn test_orderbook_depth_at_existing_bid_level() {
1411 let mut book = make_book();
1412 book.apply_delta(set_delta(Side::Bid, "99", "5", 1)).unwrap();
1414 let price = Price::new(dec!(99)).unwrap();
1415 assert_eq!(book.depth_at(Side::Bid, price), Some(dec!(5)));
1416 }
1417
1418 #[test]
1419 fn test_orderbook_depth_at_absent_level_returns_none() {
1420 let book = make_book();
1421 let price = Price::new(dec!(50)).unwrap();
1422 assert!(book.depth_at(Side::Bid, price).is_none());
1423 assert!(book.depth_at(Side::Ask, price).is_none());
1424 }
1425
1426 #[test]
1427 fn test_orderbook_bid_depth_returns_top_n_descending() {
1428 let mut book = make_book();
1429 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1430 book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1431 book.apply_delta(set_delta(Side::Bid, "98", "3", 3)).unwrap();
1432 let levels = book.bid_depth(2);
1433 assert_eq!(levels.len(), 2);
1434 assert_eq!(levels[0].price.value(), dec!(100)); assert_eq!(levels[1].price.value(), dec!(99));
1436 }
1437
1438 #[test]
1439 fn test_orderbook_ask_depth_returns_top_n_ascending() {
1440 let mut book = make_book();
1441 book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1442 book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1443 book.apply_delta(set_delta(Side::Ask, "103", "3", 3)).unwrap();
1444 let levels = book.ask_depth(2);
1445 assert_eq!(levels.len(), 2);
1446 assert_eq!(levels[0].price.value(), dec!(101)); assert_eq!(levels[1].price.value(), dec!(102));
1448 }
1449
1450 #[test]
1451 fn test_orderbook_bid_depth_fewer_than_n() {
1452 let mut book = make_book();
1453 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1454 let levels = book.bid_depth(5);
1455 assert_eq!(levels.len(), 1);
1456 }
1457
1458 #[test]
1459 fn test_orderbook_ask_depth_empty_book() {
1460 let book = make_book();
1461 assert!(book.ask_depth(3).is_empty());
1462 }
1463
1464 #[test]
1465 fn test_orderbook_remove_all_bids_clears_bid_side() {
1466 let mut book = make_book();
1467 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1468 book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1469 book.remove_all(Side::Bid);
1470 assert!(book.best_bid().is_none());
1471 }
1472
1473 #[test]
1474 fn test_orderbook_remove_all_bids_leaves_asks_intact() {
1475 let mut book = make_book();
1476 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1477 book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1478 book.remove_all(Side::Bid);
1479 assert!(book.best_bid().is_none());
1480 assert!(book.best_ask().is_some());
1481 }
1482
1483 #[test]
1484 fn test_orderbook_remove_all_asks_clears_ask_side() {
1485 let mut book = make_book();
1486 book.apply_delta(set_delta(Side::Ask, "101", "5", 1)).unwrap();
1487 book.apply_delta(set_delta(Side::Ask, "102", "3", 2)).unwrap();
1488 book.remove_all(Side::Ask);
1489 assert!(book.best_ask().is_none());
1490 }
1491
1492 #[test]
1493 fn test_orderbook_total_levels_sums_both_sides() {
1494 let mut book = make_book();
1495 book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1496 book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1497 book.apply_delta(set_delta(Side::Ask, "101", "8", 3)).unwrap();
1498 assert_eq!(book.total_levels(), 3);
1499 }
1500
1501 #[test]
1502 fn test_orderbook_total_levels_empty_book() {
1503 let book = make_book();
1504 assert_eq!(book.total_levels(), 0);
1505 }
1506}