1use crate::error::StreamError;
16use rust_decimal::Decimal;
17use std::collections::BTreeMap;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
21pub enum BookSide {
22 Bid,
24 Ask,
26}
27
28#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
30pub struct PriceLevel {
31 pub price: Decimal,
33 pub quantity: Decimal,
35}
36
37impl PriceLevel {
38 pub fn new(price: Decimal, quantity: Decimal) -> Self {
40 Self { price, quantity }
41 }
42
43 pub fn notional(&self) -> Decimal {
45 self.price * self.quantity
46 }
47}
48
49#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
51pub struct BookDelta {
52 pub symbol: String,
54 pub side: BookSide,
56 pub price: Decimal,
58 pub quantity: Decimal,
60 pub sequence: Option<u64>,
62}
63
64impl BookDelta {
65 pub fn new(
70 symbol: impl Into<String>,
71 side: BookSide,
72 price: Decimal,
73 quantity: Decimal,
74 ) -> Self {
75 Self {
76 symbol: symbol.into(),
77 side,
78 price,
79 quantity,
80 sequence: None,
81 }
82 }
83
84 pub fn with_sequence(mut self, seq: u64) -> Self {
86 self.sequence = Some(seq);
87 self
88 }
89
90 pub fn is_delete(&self) -> bool {
96 self.quantity.is_zero()
97 }
98
99 pub fn is_add(&self) -> bool {
103 !self.is_delete()
104 }
105}
106
107impl std::fmt::Display for BookDelta {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 let side = match self.side {
110 BookSide::Bid => "Bid",
111 BookSide::Ask => "Ask",
112 };
113 match self.sequence {
114 Some(seq) => write!(
115 f,
116 "{} {} {} x {} seq={}",
117 self.symbol, side, self.price, self.quantity, seq
118 ),
119 None => write!(
120 f,
121 "{} {} {} x {}",
122 self.symbol, side, self.price, self.quantity
123 ),
124 }
125 }
126}
127
128pub struct OrderBook {
130 symbol: String,
131 bids: BTreeMap<Decimal, Decimal>, asks: BTreeMap<Decimal, Decimal>, last_sequence: Option<u64>,
134}
135
136impl OrderBook {
137 pub fn new(symbol: impl Into<String>) -> Self {
139 Self {
140 symbol: symbol.into(),
141 bids: BTreeMap::new(),
142 asks: BTreeMap::new(),
143 last_sequence: None,
144 }
145 }
146
147 #[must_use = "errors from apply() must be handled to avoid missed gaps or crossed-book state"]
158 pub fn apply(&mut self, delta: BookDelta) -> Result<(), StreamError> {
159 if delta.symbol != self.symbol {
160 return Err(StreamError::BookReconstructionFailed {
161 symbol: self.symbol.clone(),
162 reason: format!(
163 "delta symbol '{}' does not match book '{}'",
164 delta.symbol, self.symbol
165 ),
166 });
167 }
168
169 if let (Some(last), Some(incoming)) = (self.last_sequence, delta.sequence) {
172 let expected = last + 1;
173 if incoming != expected {
174 return Err(StreamError::SequenceGap {
175 symbol: self.symbol.clone(),
176 expected,
177 got: incoming,
178 });
179 }
180 }
181
182 let map = match delta.side {
183 BookSide::Bid => &mut self.bids,
184 BookSide::Ask => &mut self.asks,
185 };
186 if delta.quantity.is_zero() {
187 map.remove(&delta.price);
188 } else {
189 map.insert(delta.price, delta.quantity);
190 }
191 if let Some(seq) = delta.sequence {
192 self.last_sequence = Some(seq);
193 }
194 self.check_crossed()
195 }
196
197 #[must_use = "errors from reset() indicate a crossed snapshot and must be handled"]
199 pub fn reset(
200 &mut self,
201 bids: Vec<PriceLevel>,
202 asks: Vec<PriceLevel>,
203 ) -> Result<(), StreamError> {
204 self.bids.clear();
205 self.asks.clear();
206 self.last_sequence = None;
207 for lvl in bids {
208 if !lvl.quantity.is_zero() {
209 self.bids.insert(lvl.price, lvl.quantity);
210 }
211 }
212 for lvl in asks {
213 if !lvl.quantity.is_zero() {
214 self.asks.insert(lvl.price, lvl.quantity);
215 }
216 }
217 self.check_crossed()
218 }
219
220 pub fn best_bid(&self) -> Option<PriceLevel> {
222 self.bids
223 .iter()
224 .next_back()
225 .map(|(p, q)| PriceLevel::new(*p, *q))
226 }
227
228 pub fn best_ask(&self) -> Option<PriceLevel> {
230 self.asks
231 .iter()
232 .next()
233 .map(|(p, q)| PriceLevel::new(*p, *q))
234 }
235
236 pub fn best_bid_qty(&self) -> Option<Decimal> {
240 self.best_bid().map(|l| l.quantity)
241 }
242
243 pub fn best_ask_qty(&self) -> Option<Decimal> {
247 self.best_ask().map(|l| l.quantity)
248 }
249
250 pub fn mid_price(&self) -> Option<Decimal> {
252 let bid = self.best_bid()?.price;
253 let ask = self.best_ask()?.price;
254 Some((bid + ask) / Decimal::from(2))
255 }
256
257 pub fn weighted_mid_price(&self) -> Option<Decimal> {
265 let bid = self.best_bid()?;
266 let ask = self.best_ask()?;
267 let total_qty = bid.quantity + ask.quantity;
268 if total_qty.is_zero() {
269 return None;
270 }
271 Some((bid.price * ask.quantity + ask.price * bid.quantity) / total_qty)
272 }
273
274 pub fn top_of_book(&self) -> Option<(PriceLevel, PriceLevel)> {
276 Some((self.best_bid()?, self.best_ask()?))
277 }
278
279 pub fn price_range(&self) -> Option<Decimal> {
284 let worst_bid = *self.bids.iter().next()?.0; let best_ask = self.best_ask_price()?;
286 Some(best_ask - worst_bid)
287 }
288
289 pub fn spread(&self) -> Option<Decimal> {
291 let bid = self.best_bid()?.price;
292 let ask = self.best_ask()?.price;
293 Some(ask - bid)
294 }
295
296 pub fn is_empty(&self) -> bool {
298 self.bids.is_empty() && self.asks.is_empty()
299 }
300
301 pub fn total_notional_both_sides(&self) -> Decimal {
303 self.total_notional(BookSide::Bid) + self.total_notional(BookSide::Ask)
304 }
305
306 pub fn price_level_exists(&self, side: BookSide, price: Decimal) -> bool {
308 match side {
309 BookSide::Bid => self.bids.contains_key(&price),
310 BookSide::Ask => self.asks.contains_key(&price),
311 }
312 }
313
314 pub fn clear(&mut self) {
319 self.bids.clear();
320 self.asks.clear();
321 self.last_sequence = None;
322 }
323
324 pub fn bid_depth(&self) -> usize {
326 self.bids.len()
327 }
328
329 pub fn ask_depth(&self) -> usize {
331 self.asks.len()
332 }
333
334 pub fn bid_volume_total(&self) -> Decimal {
336 self.bids.values().copied().sum()
337 }
338
339 pub fn ask_volume_total(&self) -> Decimal {
341 self.asks.values().copied().sum()
342 }
343
344 pub fn total_notional(&self, side: BookSide) -> Decimal {
350 match side {
351 BookSide::Bid => self.bids.iter().map(|(p, q)| *p * *q).sum(),
352 BookSide::Ask => self.asks.iter().map(|(p, q)| *p * *q).sum(),
353 }
354 }
355
356 pub fn is_one_sided(&self) -> bool {
359 (self.bid_depth() > 0) != (self.ask_depth() > 0)
360 }
361
362 pub fn level_count_imbalance(&self) -> Option<f64> {
367 let total = self.bid_depth() + self.ask_depth();
368 if total == 0 {
369 return None;
370 }
371 let diff = self.bid_depth() as f64 - self.ask_depth() as f64;
372 Some(diff / total as f64)
373 }
374
375 pub fn bid_ask_spread_bps(&self) -> Option<f64> {
379 use rust_decimal::prelude::ToPrimitive;
380 let mid = self.mid_price()?;
381 if mid.is_zero() {
382 return None;
383 }
384 let spread_f = self.spread()?.to_f64()?;
385 let mid_f = mid.to_f64()?;
386 Some(spread_f / mid_f * 10_000.0)
387 }
388
389 #[deprecated(since = "2.2.0", note = "Use `bid_volume_total()` instead")]
393 pub fn total_bid_volume(&self) -> Decimal {
394 self.bid_volume_total()
395 }
396
397 #[deprecated(since = "2.2.0", note = "Use `ask_volume_total()` instead")]
401 pub fn total_ask_volume(&self) -> Decimal {
402 self.ask_volume_total()
403 }
404
405 pub fn cumulative_bid_volume(&self, n: usize) -> Decimal {
410 self.bids.values().rev().take(n).copied().sum()
411 }
412
413 pub fn cumulative_ask_volume(&self, n: usize) -> Decimal {
418 self.asks.values().take(n).copied().sum()
419 }
420
421 pub fn top_n_bids(&self, n: usize) -> Vec<PriceLevel> {
425 self.bids
426 .iter()
427 .rev()
428 .take(n)
429 .map(|(p, q)| PriceLevel::new(*p, *q))
430 .collect()
431 }
432
433 pub fn top_n_asks(&self, n: usize) -> Vec<PriceLevel> {
437 self.asks
438 .iter()
439 .take(n)
440 .map(|(p, q)| PriceLevel::new(*p, *q))
441 .collect()
442 }
443
444 pub fn depth_ratio(&self, n: usize) -> Option<f64> {
449 use rust_decimal::prelude::ToPrimitive;
450 let ask_vol = self.cumulative_ask_volume(n);
451 if ask_vol.is_zero() {
452 return None;
453 }
454 (self.cumulative_bid_volume(n) / ask_vol).to_f64()
455 }
456
457 pub fn ask_wall(&self, min_qty: Decimal) -> Option<PriceLevel> {
462 self.asks
463 .iter()
464 .find(|(_, qty)| **qty >= min_qty)
465 .map(|(price, qty)| PriceLevel::new(*price, *qty))
466 }
467
468 pub fn bid_wall(&self, min_qty: Decimal) -> Option<PriceLevel> {
473 self.bids
474 .iter()
475 .rev()
476 .find(|(_, qty)| **qty >= min_qty)
477 .map(|(price, qty)| PriceLevel::new(*price, *qty))
478 }
479
480 pub fn bid_levels_above(&self, price: Decimal) -> usize {
485 self.bids.range((std::ops::Bound::Excluded(&price), std::ops::Bound::Unbounded)).count()
486 }
487
488 pub fn ask_levels_below(&self, price: Decimal) -> usize {
493 self.asks.range(..price).count()
494 }
495
496 pub fn bid_ask_volume_ratio(&self) -> Option<f64> {
502 use rust_decimal::prelude::ToPrimitive;
503 let bid = self.bid_volume_total();
504 let ask = self.ask_volume_total();
505 if bid.is_zero() || ask.is_zero() {
506 return None;
507 }
508 let bid_f = bid.to_f64()?;
509 let ask_f = ask.to_f64()?;
510 Some(bid_f / ask_f)
511 }
512
513 pub fn top_n_bid_volume(&self, n: usize) -> Decimal {
518 self.cumulative_bid_volume(n)
519 }
520
521 pub fn imbalance_ratio(&self) -> Option<f64> {
528 use rust_decimal::prelude::ToPrimitive;
529 let bid = self.bid_volume_total();
530 let ask = self.ask_volume_total();
531 let total = bid + ask;
532 if total.is_zero() {
533 return None;
534 }
535 let bid_f = bid.to_f64()?;
536 let ask_f = ask.to_f64()?;
537 let total_f = bid_f + ask_f;
538 Some((bid_f - ask_f) / total_f)
539 }
540
541 pub fn top_n_ask_volume(&self, n: usize) -> Decimal {
547 self.cumulative_ask_volume(n)
548 }
549
550 pub fn has_ask_at(&self, price: Decimal) -> bool {
552 self.asks.get(&price).map_or(false, |q| !q.is_zero())
553 }
554
555 pub fn bid_ask_depth(&self) -> (usize, usize) {
558 (self.bid_depth(), self.ask_depth())
559 }
560
561 pub fn total_book_volume(&self) -> Decimal {
565 self.total_volume()
566 }
567
568 pub fn price_range_bids(&self) -> Option<Decimal> {
572 if self.bid_depth() < 2 {
573 return None;
574 }
575 let best = self.best_bid_price()?;
576 let worst = *self.bids.keys().next()?;
577 Some(best - worst)
578 }
579
580 pub fn spread_pct(&self) -> Option<f64> {
585 use rust_decimal::prelude::ToPrimitive;
586 let mid = self.mid_price()?;
587 if mid.is_zero() {
588 return None;
589 }
590 let spread = self.spread()?;
591 (spread / mid * Decimal::from(100)).to_f64()
592 }
593
594 pub fn is_tight_spread(&self, threshold: Decimal) -> bool {
598 self.spread().map_or(false, |s| s <= threshold)
599 }
600
601 pub fn total_depth(&self) -> usize {
605 self.bid_depth() + self.ask_depth()
606 }
607
608 pub fn total_volume(&self) -> Decimal {
612 self.bid_volume_total() + self.ask_volume_total()
613 }
614
615 pub fn symbol(&self) -> &str {
617 &self.symbol
618 }
619
620 pub fn last_sequence(&self) -> Option<u64> {
622 self.last_sequence
623 }
624
625 pub fn quote_imbalance(&self) -> Option<f64> {
630 use rust_decimal::prelude::ToPrimitive;
631 let bid_qty = self.best_bid_qty()?;
632 let ask_qty = self.best_ask_qty()?;
633 let total = bid_qty + ask_qty;
634 if total.is_zero() {
635 return None;
636 }
637 (bid_qty / total).to_f64()
638 }
639
640 pub fn contains_bid(&self, price: Decimal) -> bool {
642 self.bids.contains_key(&price)
643 }
644
645 pub fn contains_ask(&self, price: Decimal) -> bool {
647 self.asks.contains_key(&price)
648 }
649
650 pub fn volume_at_bid(&self, price: Decimal) -> Option<Decimal> {
652 self.bids.get(&price).copied()
653 }
654
655 pub fn volume_at_ask(&self, price: Decimal) -> Option<Decimal> {
657 self.asks.get(&price).copied()
658 }
659
660 pub fn level_count(&self, side: BookSide) -> usize {
665 match side {
666 BookSide::Bid => self.bid_depth(),
667 BookSide::Ask => self.ask_depth(),
668 }
669 }
670
671 pub fn level_count_both_sides(&self) -> usize {
675 self.total_depth()
676 }
677
678 pub fn ask_price_at_rank(&self, n: usize) -> Option<Decimal> {
682 self.asks.keys().nth(n).copied()
683 }
684
685 pub fn bid_price_at_rank(&self, n: usize) -> Option<Decimal> {
689 self.bids.keys().nth_back(n).copied()
690 }
691
692 pub fn quote_density(&self, side: BookSide) -> Option<Decimal> {
697 let map = match side {
698 BookSide::Bid => &self.bids,
699 BookSide::Ask => &self.asks,
700 };
701 if map.len() < 2 { return None; }
702 let min_p = *map.keys().next()?;
703 let max_p = *map.keys().next_back()?;
704 let range = max_p - min_p;
705 if range.is_zero() { return None; }
706 Some(Decimal::from(map.len()) / range)
707 }
708
709 pub fn bid_ask_qty_ratio(&self) -> Option<f64> {
715 self.bid_ask_ratio()
716 }
717
718 pub fn top_bid_qty(&self) -> Option<Decimal> {
723 self.best_bid_qty()
724 }
725
726 pub fn top_ask_qty(&self) -> Option<Decimal> {
731 self.best_ask_qty()
732 }
733
734 pub fn cumulative_bid_qty(&self, n: usize) -> Decimal {
739 self.cumulative_bid_volume(n)
740 }
741
742 pub fn cumulative_ask_qty(&self, n: usize) -> Decimal {
747 self.cumulative_ask_volume(n)
748 }
749
750 pub fn ladder_balance(&self, n: usize) -> Option<f64> {
755 use rust_decimal::prelude::ToPrimitive;
756 let ask_qty = self.cumulative_ask_qty(n);
757 if ask_qty.is_zero() { return None; }
758 (self.cumulative_bid_qty(n) / ask_qty).to_f64()
759 }
760
761 pub fn ask_bid_level_ratio(&self) -> Option<f64> {
766 if self.bid_depth() == 0 { return None; }
767 Some(self.ask_depth() as f64 / self.bid_depth() as f64)
768 }
769
770 pub fn depth_at_price(&self, price: Decimal, side: BookSide) -> Option<Decimal> {
776 match side {
777 BookSide::Bid => self.bids.get(&price).copied(),
778 BookSide::Ask => self.asks.get(&price).copied(),
779 }
780 }
781
782 pub fn bid_ask_ratio(&self) -> Option<f64> {
787 use rust_decimal::prelude::ToPrimitive;
788 let ask = self.ask_volume_total();
789 if ask.is_zero() {
790 return None;
791 }
792 (self.bid_volume_total() / ask).to_f64()
793 }
794
795 pub fn all_bids(&self) -> Vec<PriceLevel> {
800 self.bids
801 .iter()
802 .rev()
803 .map(|(p, q)| PriceLevel::new(*p, *q))
804 .collect()
805 }
806
807 pub fn all_asks(&self) -> Vec<PriceLevel> {
812 self.asks
813 .iter()
814 .map(|(p, q)| PriceLevel::new(*p, *q))
815 .collect()
816 }
817
818 pub fn top_bids(&self, n: usize) -> Vec<PriceLevel> {
820 self.bids
821 .iter()
822 .rev()
823 .take(n)
824 .map(|(p, q)| PriceLevel::new(*p, *q))
825 .collect()
826 }
827
828 pub fn top_asks(&self, n: usize) -> Vec<PriceLevel> {
830 self.asks
831 .iter()
832 .take(n)
833 .map(|(p, q)| PriceLevel::new(*p, *q))
834 .collect()
835 }
836
837 pub fn imbalance(&self) -> Option<f64> {
846 use rust_decimal::prelude::ToPrimitive;
847 let bid_qty = self.best_bid()?.quantity;
848 let ask_qty = self.best_ask()?.quantity;
849 let total = bid_qty + ask_qty;
850 if total.is_zero() {
851 return None;
852 }
853 let imb = (bid_qty - ask_qty) / total;
854 imb.to_f64()
855 }
856
857 pub fn bid_ask_imbalance(&self, n: usize) -> Option<f64> {
863 use rust_decimal::prelude::ToPrimitive;
864 let bid_vol = self.cumulative_bid_volume(n);
865 let ask_vol = self.cumulative_ask_volume(n);
866 if bid_vol.is_zero() || ask_vol.is_zero() {
867 return None;
868 }
869 let total = bid_vol + ask_vol;
870 ((bid_vol - ask_vol) / total).to_f64()
871 }
872
873 pub fn vwap(&self, side: BookSide, n: usize) -> Option<Decimal> {
878 let levels = match side {
879 BookSide::Bid => self.top_bids(n),
880 BookSide::Ask => self.top_asks(n),
881 };
882 let total_vol: Decimal = levels.iter().map(|l| l.quantity).sum();
883 if total_vol.is_zero() {
884 return None;
885 }
886 let price_vol_sum: Decimal = levels.iter().map(|l| l.price * l.quantity).sum();
887 Some(price_vol_sum / total_vol)
888 }
889
890 pub fn price_at_volume(&self, side: BookSide, target_volume: Decimal) -> Option<Decimal> {
898 if target_volume.is_zero() {
899 return None;
900 }
901 let levels: Vec<(Decimal, Decimal)> = match side {
902 BookSide::Bid => self.bids.iter().rev().map(|(p, q)| (*p, *q)).collect(),
903 BookSide::Ask => self.asks.iter().map(|(p, q)| (*p, *q)).collect(),
904 };
905 if levels.is_empty() {
906 return None;
907 }
908 let mut remaining = target_volume;
909 let mut notional = Decimal::ZERO;
910 let mut filled = Decimal::ZERO;
911 for (price, qty) in &levels {
912 if remaining.is_zero() {
913 break;
914 }
915 let take = (*qty).min(remaining);
916 notional += price * take;
917 filled += take;
918 remaining -= take;
919 }
920 if filled.is_zero() {
921 return None;
922 }
923 Some(notional / filled)
924 }
925
926 pub fn depth_imbalance(&self, n: usize) -> Option<f64> {
935 use rust_decimal::prelude::ToPrimitive;
936 if n == 0 {
937 return None;
938 }
939 let bid_vol = self.cumulative_bid_volume(n);
940 let ask_vol = self.cumulative_ask_volume(n);
941 let total = bid_vol + ask_vol;
942 if total.is_zero() {
943 return None;
944 }
945 ((bid_vol - ask_vol) / total).to_f64()
946 }
947
948 pub fn levels(&self, side: BookSide, n: usize) -> Vec<PriceLevel> {
954 match side {
955 BookSide::Bid => self
956 .bids
957 .iter()
958 .rev()
959 .take(n)
960 .map(|(p, q)| PriceLevel::new(*p, *q))
961 .collect(),
962 BookSide::Ask => self
963 .asks
964 .iter()
965 .take(n)
966 .map(|(p, q)| PriceLevel::new(*p, *q))
967 .collect(),
968 }
969 }
970
971 pub fn bid_volume_at_price(&self, price: Decimal) -> Option<Decimal> {
973 self.bids.get(&price).copied()
974 }
975
976 pub fn ask_volume_at_price(&self, price: Decimal) -> Option<Decimal> {
978 self.asks.get(&price).copied()
979 }
980
981 pub fn snapshot(&self) -> (Vec<PriceLevel>, Vec<PriceLevel>) {
991 (self.all_bids(), self.all_asks())
992 }
993
994 pub fn best_bid_price(&self) -> Option<Decimal> {
996 self.best_bid().map(|l| l.price)
997 }
998
999 pub fn best_ask_price(&self) -> Option<Decimal> {
1001 self.best_ask().map(|l| l.price)
1002 }
1003
1004 pub fn is_crossed(&self) -> bool {
1009 self.best_bid_price().zip(self.best_ask_price()).map_or(false, |(b, a)| b >= a)
1010 }
1011
1012 pub fn has_bids(&self) -> bool {
1014 self.bid_depth() > 0
1015 }
1016
1017 pub fn has_asks(&self) -> bool {
1019 self.ask_depth() > 0
1020 }
1021
1022 pub fn ask_price_range(&self) -> Option<Decimal> {
1026 let best = self.best_ask_price()?;
1027 let worst = *self.asks.keys().next_back()?;
1028 Some(worst - best)
1029 }
1030
1031 pub fn bid_price_range(&self) -> Option<Decimal> {
1035 let best = self.best_bid_price()?;
1036 let worst = *self.bids.keys().next()?;
1037 Some(best - worst)
1038 }
1039
1040 pub fn mid_spread_ratio(&self) -> Option<f64> {
1044 use rust_decimal::prelude::ToPrimitive;
1045 let spread = self.spread()?;
1046 let mid = self.mid_price()?;
1047 if mid.is_zero() {
1048 return None;
1049 }
1050 (spread / mid).to_f64()
1051 }
1052
1053 pub fn volume_imbalance(&self) -> Option<f64> {
1058 use rust_decimal::prelude::ToPrimitive;
1059 let bid = self.bid_volume_total();
1060 let ask = self.ask_volume_total();
1061 let total = bid + ask;
1062 if total.is_zero() {
1063 return None;
1064 }
1065 ((bid - ask) / total).to_f64()
1066 }
1067
1068 fn check_crossed(&self) -> Result<(), StreamError> {
1069 if let (Some(bid), Some(ask)) = (self.best_bid(), self.best_ask()) {
1070 if bid.price >= ask.price {
1071 return Err(StreamError::BookCrossed {
1072 symbol: self.symbol.clone(),
1073 bid: bid.price,
1074 ask: ask.price,
1075 });
1076 }
1077 }
1078 Ok(())
1079 }
1080
1081 pub fn fee_estimate(&self, side: BookSide, qty: Decimal, fee_bps: Decimal) -> Option<Decimal> {
1086 if qty <= Decimal::ZERO { return None; }
1087 let best_price = match side {
1088 BookSide::Bid => self.best_bid_price()?,
1089 BookSide::Ask => self.best_ask_price()?,
1090 };
1091 let impact = self.price_impact(side, qty).unwrap_or(Decimal::ZERO);
1092 let fill_price = best_price + impact;
1093 Some(fill_price * qty * fee_bps / Decimal::from(10_000u32))
1094 }
1095
1096 pub fn spread_ticks(&self, tick_size: Decimal) -> Option<Decimal> {
1100 if tick_size.is_zero() { return None; }
1101 let spread = self.spread()?;
1102 Some(spread / tick_size)
1103 }
1104
1105 pub fn spread_bps(&self) -> Option<f64> {
1109 use rust_decimal::prelude::ToPrimitive;
1110 let mid = self.mid_price()?;
1111 if mid.is_zero() {
1112 return None;
1113 }
1114 let spread = self.spread()?;
1115 (spread / mid * Decimal::from(10_000u32)).to_f64()
1116 }
1117
1118 pub fn depth_at_pct(&self, side: BookSide, pct: f64) -> Option<Decimal> {
1125 use rust_decimal::prelude::FromPrimitive;
1126 if pct < 0.0 { return None; }
1127 let pct_dec = Decimal::from_f64(pct / 100.0)?;
1128 match side {
1129 BookSide::Bid => {
1130 let best = self.best_bid_price()?;
1131 let threshold = best * (Decimal::ONE - pct_dec);
1132 Some(self.bids.range(threshold..).map(|(_, q)| q).sum())
1133 }
1134 BookSide::Ask => {
1135 let best = self.best_ask_price()?;
1136 let threshold = best * (Decimal::ONE + pct_dec);
1137 Some(self.asks.range(..=threshold).map(|(_, q)| q).sum())
1138 }
1139 }
1140 }
1141
1142 pub fn microprice(&self) -> Option<Decimal> {
1149 let bid = self.best_bid()?;
1150 let ask = self.best_ask()?;
1151 let total_qty = bid.quantity + ask.quantity;
1152 if total_qty.is_zero() { return None; }
1153 Some((ask.quantity * bid.price + bid.quantity * ask.price) / total_qty)
1154 }
1155
1156 pub fn best_n_levels(&self, side: BookSide, n: usize) -> Vec<(Decimal, Decimal)> {
1162 match side {
1163 BookSide::Bid => self.bids.iter().rev().take(n)
1164 .map(|(&p, &q)| (p, q)).collect(),
1165 BookSide::Ask => self.asks.iter().take(n)
1166 .map(|(&p, &q)| (p, q)).collect(),
1167 }
1168 }
1169
1170 pub fn price_impact(&self, side: BookSide, qty: Decimal) -> Option<Decimal> {
1176 if qty <= Decimal::ZERO { return None; }
1177 let best_price = match side {
1178 BookSide::Bid => self.best_bid_price()?,
1179 BookSide::Ask => self.best_ask_price()?,
1180 };
1181 let mut remaining = qty;
1182 let mut cost = Decimal::ZERO;
1183 let levels: Box<dyn Iterator<Item = (&Decimal, &Decimal)>> = match side {
1184 BookSide::Bid => Box::new(self.bids.iter().rev()),
1185 BookSide::Ask => Box::new(self.asks.iter()),
1186 };
1187 for (&price, &level_qty) in levels {
1188 if remaining <= Decimal::ZERO { break; }
1189 let filled = remaining.min(level_qty);
1190 cost += price * filled;
1191 remaining -= filled;
1192 }
1193 if remaining > Decimal::ZERO { return None; } let avg_fill = cost / qty;
1195 Some((avg_fill - best_price).abs())
1196 }
1197
1198 pub fn total_value_at_level(&self, side: BookSide, price: Decimal) -> Option<Decimal> {
1202 match side {
1203 BookSide::Bid => self.bids.get(&price).map(|&q| price * q),
1204 BookSide::Ask => self.asks.get(&price).map(|&q| price * q),
1205 }
1206 }
1207
1208 pub fn price_impact_buy(&self, quantity: Decimal) -> Option<Decimal> {
1212 if quantity <= Decimal::ZERO {
1213 return None;
1214 }
1215 let mut remaining = quantity;
1216 let mut cost = Decimal::ZERO;
1217 for (&price, &qty) in &self.asks {
1218 if remaining.is_zero() { break; }
1219 let fill = remaining.min(qty);
1220 cost += fill * price;
1221 remaining -= fill;
1222 }
1223 if !remaining.is_zero() { return None; }
1224 Some(cost / quantity)
1225 }
1226
1227 pub fn price_impact_sell(&self, quantity: Decimal) -> Option<Decimal> {
1231 if quantity <= Decimal::ZERO {
1232 return None;
1233 }
1234 let mut remaining = quantity;
1235 let mut proceeds = Decimal::ZERO;
1236 for (&price, &qty) in self.bids.iter().rev() {
1237 if remaining.is_zero() { break; }
1238 let fill = remaining.min(qty);
1239 proceeds += fill * price;
1240 remaining -= fill;
1241 }
1242 if !remaining.is_zero() { return None; }
1243 Some(proceeds / quantity)
1244 }
1245
1246 #[deprecated(note = "use ask_depth() instead")]
1248 pub fn ask_level_count(&self) -> usize {
1249 self.ask_depth()
1250 }
1251
1252 #[deprecated(note = "use bid_depth() instead")]
1254 pub fn bid_level_count(&self) -> usize {
1255 self.bid_depth()
1256 }
1257
1258 pub fn ask_volume_within(&self, price_range: Decimal) -> Decimal {
1263 self.best_ask().map_or(Decimal::ZERO, |best| {
1264 self.asks.range(..=(best.price + price_range)).map(|(_, &q)| q).sum()
1265 })
1266 }
1267
1268 pub fn bid_volume_within(&self, price_range: Decimal) -> Decimal {
1273 self.best_bid().map_or(Decimal::ZERO, |best| {
1274 self.bids.range((best.price - price_range)..).map(|(_, &q)| q).sum()
1275 })
1276 }
1277
1278 pub fn ask_volume_above(&self, price: Decimal) -> Decimal {
1280 use std::ops::Bound::Excluded;
1281 self.asks
1282 .range((Excluded(price), std::ops::Bound::Unbounded))
1283 .map(|(_, &q)| q)
1284 .sum()
1285 }
1286
1287 pub fn bid_volume_below(&self, price: Decimal) -> Decimal {
1289 use std::ops::Bound::Unbounded;
1290 use std::ops::Bound::Excluded;
1291 self.bids
1292 .range((Unbounded, Excluded(price)))
1293 .map(|(_, &q)| q)
1294 .sum()
1295 }
1296}
1297
1298#[cfg(test)]
1299mod tests {
1300 use super::*;
1301 use rust_decimal_macros::dec;
1302
1303 fn book(symbol: &str) -> OrderBook {
1304 OrderBook::new(symbol)
1305 }
1306
1307 fn delta(symbol: &str, side: BookSide, price: Decimal, qty: Decimal) -> BookDelta {
1308 BookDelta::new(symbol, side, price, qty)
1309 }
1310
1311 #[test]
1312 fn test_order_book_apply_bid_level() {
1313 let mut b = book("BTC-USD");
1314 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1315 .unwrap();
1316 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
1317 }
1318
1319 #[test]
1320 fn test_order_book_apply_ask_level() {
1321 let mut b = book("BTC-USD");
1322 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2)))
1323 .unwrap();
1324 assert_eq!(b.best_ask().unwrap().price, dec!(50100));
1325 }
1326
1327 #[test]
1328 fn test_order_book_remove_level_with_zero_qty() {
1329 let mut b = book("BTC-USD");
1330 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1331 .unwrap();
1332 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(0)))
1333 .unwrap();
1334 assert!(b.best_bid().is_none());
1335 }
1336
1337 #[test]
1338 fn test_order_book_best_bid_is_highest() {
1339 let mut b = book("BTC-USD");
1340 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)))
1341 .unwrap();
1342 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2)))
1343 .unwrap();
1344 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3)))
1345 .unwrap();
1346 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
1347 }
1348
1349 #[test]
1350 fn test_order_book_best_ask_is_lowest() {
1351 let mut b = book("BTC-USD");
1352 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(1)))
1353 .unwrap();
1354 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2)))
1355 .unwrap();
1356 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3)))
1357 .unwrap();
1358 assert_eq!(b.best_ask().unwrap().price, dec!(50100));
1359 }
1360
1361 #[test]
1362 fn test_order_book_mid_price() {
1363 let mut b = book("BTC-USD");
1364 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1365 .unwrap();
1366 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
1367 .unwrap();
1368 assert_eq!(b.mid_price().unwrap(), dec!(50050));
1369 }
1370
1371 #[test]
1372 fn test_order_book_spread() {
1373 let mut b = book("BTC-USD");
1374 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1375 .unwrap();
1376 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
1377 .unwrap();
1378 assert_eq!(b.spread().unwrap(), dec!(100));
1379 }
1380
1381 #[test]
1382 fn test_order_book_crossed_returns_error() {
1383 let mut b = book("BTC-USD");
1384 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50000), dec!(1)))
1385 .unwrap();
1386 let result = b.apply(delta("BTC-USD", BookSide::Bid, dec!(50001), dec!(1)));
1387 assert!(matches!(result, Err(StreamError::BookCrossed { .. })));
1388 }
1389
1390 #[test]
1391 fn test_order_book_wrong_symbol_delta_rejected() {
1392 let mut b = book("BTC-USD");
1393 let result = b.apply(delta("ETH-USD", BookSide::Bid, dec!(3000), dec!(1)));
1394 assert!(matches!(
1395 result,
1396 Err(StreamError::BookReconstructionFailed { .. })
1397 ));
1398 }
1399
1400 #[test]
1401 fn test_order_book_reset_clears_and_reloads() {
1402 let mut b = book("BTC-USD");
1403 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49000), dec!(5)))
1404 .unwrap();
1405 b.reset(
1406 vec![PriceLevel::new(dec!(50000), dec!(1))],
1407 vec![PriceLevel::new(dec!(50100), dec!(1))],
1408 )
1409 .unwrap();
1410 assert_eq!(b.bid_depth(), 1);
1411 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
1412 }
1413
1414 #[test]
1415 fn test_order_book_reset_ignores_zero_qty_levels() {
1416 let mut b = book("BTC-USD");
1417 b.reset(
1418 vec![
1419 PriceLevel::new(dec!(50000), dec!(1)),
1420 PriceLevel::new(dec!(49900), dec!(0)),
1421 ],
1422 vec![PriceLevel::new(dec!(50100), dec!(1))],
1423 )
1424 .unwrap();
1425 assert_eq!(b.bid_depth(), 1);
1426 }
1427
1428 #[test]
1429 fn test_order_book_reset_clears_sequence() {
1430 let mut b = book("BTC-USD");
1431 b.apply(
1432 delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)).with_sequence(5),
1433 )
1434 .unwrap();
1435 assert_eq!(b.last_sequence(), Some(5));
1436 b.reset(
1437 vec![PriceLevel::new(dec!(50000), dec!(1))],
1438 vec![PriceLevel::new(dec!(50100), dec!(1))],
1439 )
1440 .unwrap();
1441 assert_eq!(b.last_sequence(), None);
1442 }
1443
1444 #[test]
1445 fn test_order_book_depth_counts() {
1446 let mut b = book("BTC-USD");
1447 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)))
1448 .unwrap();
1449 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1)))
1450 .unwrap();
1451 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
1452 .unwrap();
1453 assert_eq!(b.bid_depth(), 2);
1454 assert_eq!(b.ask_depth(), 1);
1455 }
1456
1457 #[test]
1458 fn test_order_book_top_bids_descending() {
1459 let mut b = book("BTC-USD");
1460 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3)))
1461 .unwrap();
1462 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1463 .unwrap();
1464 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(2)))
1465 .unwrap();
1466 let top = b.top_bids(2);
1467 assert_eq!(top[0].price, dec!(50000));
1468 assert_eq!(top[1].price, dec!(49900));
1469 }
1470
1471 #[test]
1472 fn test_order_book_top_asks_ascending() {
1473 let mut b = book("BTC-USD");
1474 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3)))
1475 .unwrap();
1476 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
1477 .unwrap();
1478 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2)))
1479 .unwrap();
1480 let top = b.top_asks(2);
1481 assert_eq!(top[0].price, dec!(50100));
1482 assert_eq!(top[1].price, dec!(50200));
1483 }
1484
1485 #[test]
1486 fn test_book_delta_with_sequence() {
1487 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)).with_sequence(42);
1488 assert_eq!(d.sequence, Some(42));
1489 }
1490
1491 #[test]
1492 fn test_order_book_sequence_tracking() {
1493 let mut b = book("BTC-USD");
1494 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)).with_sequence(7))
1495 .unwrap();
1496 assert_eq!(b.last_sequence(), Some(7));
1497 }
1498
1499 #[test]
1500 fn test_order_book_sequence_gap_detected() {
1501 let mut b = book("BTC-USD");
1502 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)).with_sequence(1))
1503 .unwrap();
1504 let result =
1506 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)).with_sequence(3));
1507 assert!(matches!(
1508 result,
1509 Err(StreamError::SequenceGap { expected: 2, got: 3, .. })
1510 ));
1511 }
1512
1513 #[test]
1514 fn test_order_book_sequential_deltas_accepted() {
1515 let mut b = book("BTC-USD");
1516 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)).with_sequence(1))
1517 .unwrap();
1518 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)).with_sequence(2))
1519 .unwrap();
1520 assert_eq!(b.last_sequence(), Some(2));
1521 }
1522
1523 #[test]
1524 fn test_order_book_mid_price_empty_returns_none() {
1525 let b = book("BTC-USD");
1526 assert!(b.mid_price().is_none());
1527 }
1528
1529 #[test]
1530 fn test_price_level_new() {
1531 let lvl = PriceLevel::new(dec!(100), dec!(5));
1532 assert_eq!(lvl.price, dec!(100));
1533 assert_eq!(lvl.quantity, dec!(5));
1534 }
1535
1536 #[test]
1537 fn test_contains_bid_present() {
1538 let mut b = book("BTC-USD");
1539 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1540 .unwrap();
1541 assert!(b.contains_bid(dec!(50000)));
1542 assert!(!b.contains_bid(dec!(49999)));
1543 }
1544
1545 #[test]
1546 fn test_contains_ask_present() {
1547 let mut b = book("BTC-USD");
1548 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2)))
1549 .unwrap();
1550 assert!(b.contains_ask(dec!(50100)));
1551 assert!(!b.contains_ask(dec!(50200)));
1552 }
1553
1554 #[test]
1555 fn test_contains_bid_removed_after_zero_qty() {
1556 let mut b = book("BTC-USD");
1557 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1558 .unwrap();
1559 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(0)))
1560 .unwrap();
1561 assert!(!b.contains_bid(dec!(50000)));
1562 }
1563
1564 #[test]
1565 fn test_book_delta_serde_roundtrip() {
1566 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))
1567 .with_sequence(42);
1568 let json = serde_json::to_string(&d).unwrap();
1569 let d2: BookDelta = serde_json::from_str(&json).unwrap();
1570 assert_eq!(d2.symbol, "BTC-USD");
1571 assert_eq!(d2.price, dec!(50000));
1572 assert_eq!(d2.sequence, Some(42));
1573 }
1574
1575 #[test]
1576 fn test_volume_at_bid_present() {
1577 let mut b = book("BTC-USD");
1578 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(3)))
1579 .unwrap();
1580 assert_eq!(b.volume_at_bid(dec!(50000)), Some(dec!(3)));
1581 assert_eq!(b.volume_at_bid(dec!(49999)), None);
1582 }
1583
1584 #[test]
1585 fn test_volume_at_ask_present() {
1586 let mut b = book("BTC-USD");
1587 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5)))
1588 .unwrap();
1589 assert_eq!(b.volume_at_ask(dec!(50100)), Some(dec!(5)));
1590 assert_eq!(b.volume_at_ask(dec!(50200)), None);
1591 }
1592
1593 #[test]
1594 fn test_book_delta_display_with_sequence() {
1595 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))
1596 .with_sequence(42);
1597 let s = d.to_string();
1598 assert!(s.contains("BTC-USD"));
1599 assert!(s.contains("Bid"));
1600 assert!(s.contains("seq=42"));
1601 }
1602
1603 #[test]
1604 fn test_book_delta_display_without_sequence() {
1605 let d = BookDelta::new("ETH-USD", BookSide::Ask, dec!(3000), dec!(2));
1606 let s = d.to_string();
1607 assert!(s.contains("ETH-USD"));
1608 assert!(s.contains("Ask"));
1609 assert!(!s.contains("seq="));
1610 }
1611
1612 #[test]
1613 fn test_book_delta_is_delete_zero_qty() {
1614 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(0));
1615 assert!(d.is_delete());
1616 }
1617
1618 #[test]
1619 fn test_book_delta_is_delete_nonzero_qty() {
1620 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1));
1621 assert!(!d.is_delete());
1622 }
1623
1624 #[test]
1625 fn test_snapshot_bids_descending_asks_ascending() {
1626 let mut b = book("BTC-USD");
1627 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1)))
1628 .unwrap();
1629 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2)))
1630 .unwrap();
1631 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(1)))
1632 .unwrap();
1633 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(3)))
1634 .unwrap();
1635 let (bids, asks) = b.snapshot();
1636 assert_eq!(bids[0].price, dec!(50000));
1637 assert_eq!(bids[1].price, dec!(49800));
1638 assert_eq!(asks[0].price, dec!(50100));
1639 assert_eq!(asks[1].price, dec!(50200));
1640 }
1641
1642 #[test]
1645 fn test_bid_ask_imbalance_balanced() {
1646 let mut b = book("BTC-USD");
1647 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
1648 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
1649 let imb = b.bid_ask_imbalance(1).unwrap();
1650 assert!((imb).abs() < 1e-9, "equal qty → ~0");
1651 }
1652
1653 #[test]
1654 fn test_bid_ask_imbalance_full_bid_pressure() {
1655 let mut b = book("BTC-USD");
1656 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(10))).unwrap();
1657 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(0))).unwrap();
1658 assert!(b.bid_ask_imbalance(1).is_none());
1660 }
1661
1662 #[test]
1663 fn test_bid_ask_imbalance_two_levels() {
1664 let mut b = book("BTC-USD");
1665 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(3))).unwrap();
1666 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1))).unwrap();
1667 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
1668 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2))).unwrap();
1669 let imb = b.bid_ask_imbalance(2).unwrap();
1671 assert!((imb).abs() < 1e-9);
1672 }
1673
1674 #[test]
1677 fn test_vwap_single_level_equals_price() {
1678 let mut b = book("BTC-USD");
1679 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5))).unwrap();
1680 assert_eq!(b.vwap(BookSide::Ask, 1), Some(dec!(50100)));
1681 }
1682
1683 #[test]
1684 fn test_vwap_two_equal_qty_levels() {
1685 let mut b = book("BTC-USD");
1686 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
1687 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1))).unwrap();
1688 assert_eq!(b.vwap(BookSide::Bid, 2), Some(dec!(49900)));
1690 }
1691
1692 #[test]
1693 fn test_vwap_empty_side_returns_none() {
1694 let b = book("BTC-USD");
1695 assert!(b.vwap(BookSide::Ask, 5).is_none());
1696 }
1697
1698 #[test]
1701 fn test_depth_at_price_bid_present() {
1702 let mut b = book("BTC-USD");
1703 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(3))).unwrap();
1704 assert_eq!(b.depth_at_price(dec!(50000), BookSide::Bid), Some(dec!(3)));
1705 }
1706
1707 #[test]
1708 fn test_depth_at_price_ask_present() {
1709 let mut b = book("BTC-USD");
1710 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
1711 assert_eq!(b.depth_at_price(dec!(50100), BookSide::Ask), Some(dec!(2)));
1712 }
1713
1714 #[test]
1715 fn test_depth_at_price_absent_returns_none() {
1716 let b = book("BTC-USD");
1717 assert!(b.depth_at_price(dec!(99999), BookSide::Bid).is_none());
1718 assert!(b.depth_at_price(dec!(99999), BookSide::Ask).is_none());
1719 }
1720
1721 #[test]
1722 fn test_bid_ask_ratio_equal_sides() {
1723 let mut b = book("BTC-USD");
1724 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(5))).unwrap();
1725 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5))).unwrap();
1726 let ratio = b.bid_ask_ratio().unwrap();
1727 assert!((ratio - 1.0).abs() < 1e-9);
1728 }
1729
1730 #[test]
1731 fn test_bid_ask_ratio_more_bids() {
1732 let mut b = book("BTC-USD");
1733 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(10))).unwrap();
1734 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5))).unwrap();
1735 let ratio = b.bid_ask_ratio().unwrap();
1736 assert!((ratio - 2.0).abs() < 1e-9);
1737 }
1738
1739 #[test]
1740 fn test_bid_ask_ratio_no_asks_returns_none() {
1741 let mut b = book("BTC-USD");
1742 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(5))).unwrap();
1743 assert!(b.bid_ask_ratio().is_none());
1744 }
1745
1746 #[test]
1749 fn test_all_bids_sorted_descending() {
1750 let mut b = book("BTC-USD");
1751 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1))).unwrap();
1752 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2))).unwrap();
1753 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(3))).unwrap();
1754 let bids = b.all_bids();
1755 assert_eq!(bids.len(), 3);
1756 assert_eq!(bids[0].price, dec!(50000));
1757 assert_eq!(bids[1].price, dec!(49900));
1758 assert_eq!(bids[2].price, dec!(49800));
1759 }
1760
1761 #[test]
1762 fn test_all_asks_sorted_ascending() {
1763 let mut b = book("BTC-USD");
1764 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
1765 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(2))).unwrap();
1766 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(3))).unwrap();
1767 let asks = b.all_asks();
1768 assert_eq!(asks.len(), 3);
1769 assert_eq!(asks[0].price, dec!(50100));
1770 assert_eq!(asks[1].price, dec!(50200));
1771 assert_eq!(asks[2].price, dec!(50300));
1772 }
1773
1774 #[test]
1775 fn test_all_bids_empty_returns_empty() {
1776 let b = book("BTC-USD");
1777 assert!(b.all_bids().is_empty());
1778 }
1779
1780 #[test]
1783 fn test_spread_pct_basic() {
1784 let mut b = book("X");
1786 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1787 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
1788 let pct = b.spread_pct().unwrap();
1789 assert!((pct - 100.0 / 100.5).abs() < 1e-9, "got {pct}");
1790 }
1791
1792 #[test]
1793 fn test_spread_pct_empty_book_returns_none() {
1794 let b = book("X");
1795 assert!(b.spread_pct().is_none());
1796 }
1797
1798 #[test]
1799 fn test_total_depth_counts_both_sides() {
1800 let mut b = book("X");
1801 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
1802 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
1803 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
1804 assert_eq!(b.total_depth(), 3);
1805 }
1806
1807 #[test]
1808 fn test_total_depth_empty_is_zero() {
1809 let b = book("X");
1810 assert_eq!(b.total_depth(), 0);
1811 }
1812
1813 #[test]
1814 fn test_total_volume_sums_both_sides() {
1815 let mut b = book("X");
1816 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(3))).unwrap();
1817 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
1818 assert_eq!(b.total_volume(), dec!(8));
1819 }
1820
1821 #[test]
1822 fn test_total_volume_empty_is_zero() {
1823 let b = book("X");
1824 assert_eq!(b.total_volume(), dec!(0));
1825 }
1826
1827 #[test]
1830 fn test_level_count_empty() {
1831 let b = book("BTC-USD");
1832 assert_eq!(b.level_count(BookSide::Bid), 0);
1833 assert_eq!(b.level_count(BookSide::Ask), 0);
1834 }
1835
1836 #[test]
1837 fn test_level_count_matches_depth_methods() {
1838 let mut b = book("BTC-USD");
1839 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1840 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
1841 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
1842 assert_eq!(b.level_count(BookSide::Bid), b.bid_depth());
1843 assert_eq!(b.level_count(BookSide::Ask), b.ask_depth());
1844 }
1845
1846 #[test]
1849 fn test_price_level_notional() {
1850 let level = PriceLevel::new(dec!(50000), dec!(2));
1851 assert_eq!(level.notional(), dec!(100000));
1852 }
1853
1854 #[test]
1855 fn test_price_level_notional_zero_qty() {
1856 let level = PriceLevel::new(dec!(100), dec!(0));
1857 assert_eq!(level.notional(), dec!(0));
1858 }
1859
1860 #[test]
1863 fn test_weighted_mid_price_equal_qtys_is_arithmetic_mid() {
1864 let mut b = book("X");
1866 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1867 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(1))).unwrap();
1868 assert_eq!(b.weighted_mid_price().unwrap(), dec!(101));
1869 }
1870
1871 #[test]
1872 fn test_weighted_mid_price_skews_toward_larger_qty() {
1873 let mut b = book("X");
1875 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1876 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
1877 assert_eq!(b.weighted_mid_price().unwrap(), dec!(100.5));
1878 }
1879
1880 #[test]
1881 fn test_weighted_mid_price_empty_returns_none() {
1882 let b = book("X");
1883 assert!(b.weighted_mid_price().is_none());
1884 }
1885
1886 #[test]
1889 fn test_is_empty_new_book() {
1890 let b = book("BTC-USD");
1891 assert!(b.is_empty());
1892 }
1893
1894 #[test]
1895 fn test_is_empty_false_with_bid() {
1896 let mut b = book("BTC-USD");
1897 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1898 assert!(!b.is_empty());
1899 }
1900
1901 #[test]
1902 fn test_is_empty_false_with_ask() {
1903 let mut b = book("BTC-USD");
1904 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
1905 assert!(!b.is_empty());
1906 }
1907
1908 #[test]
1909 fn test_is_empty_true_after_removing_all_levels() {
1910 let mut b = book("BTC-USD");
1911 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1912 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(0))).unwrap(); assert!(b.is_empty());
1914 }
1915
1916 #[test]
1919 fn test_clear_empty_book_is_noop() {
1920 let mut b = book("BTC-USD");
1921 b.clear();
1922 assert!(b.is_empty());
1923 }
1924
1925 #[test]
1926 fn test_clear_removes_all_levels() {
1927 let mut b = book("BTC-USD");
1928 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1929 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
1930 b.clear();
1931 assert!(b.is_empty());
1932 }
1933
1934 #[test]
1935 fn test_clear_allows_fresh_apply_after() {
1936 let mut b = book("BTC-USD");
1937 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1938 b.clear();
1939 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(5))).unwrap();
1941 assert_eq!(b.bid_depth(), 1);
1942 }
1943
1944 #[test]
1947 fn test_total_notional_bid_side() {
1948 let mut b = book("BTC-USD");
1949 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2))).unwrap();
1950 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(3))).unwrap();
1951 assert_eq!(b.total_notional(BookSide::Bid), dec!(249700));
1953 }
1954
1955 #[test]
1956 fn test_total_notional_ask_side() {
1957 let mut b = book("BTC-USD");
1958 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
1959 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2))).unwrap();
1960 assert_eq!(b.total_notional(BookSide::Ask), dec!(150500));
1962 }
1963
1964 #[test]
1965 fn test_total_notional_empty_side_is_zero() {
1966 let b = book("BTC-USD");
1967 assert_eq!(b.total_notional(BookSide::Bid), dec!(0));
1968 assert_eq!(b.total_notional(BookSide::Ask), dec!(0));
1969 }
1970
1971 #[test]
1972 fn test_cumulative_bid_volume_top_two() {
1973 let mut b = book("BTC-USD");
1974 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(5))).unwrap();
1975 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(3))).unwrap();
1976 b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(2))).unwrap();
1977 assert_eq!(b.cumulative_bid_volume(2), dec!(8));
1979 }
1980
1981 #[test]
1982 fn test_cumulative_ask_volume_top_two() {
1983 let mut b = book("BTC-USD");
1984 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(4))).unwrap();
1985 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(6))).unwrap();
1986 b.apply(delta("BTC-USD", BookSide::Ask, dec!(103), dec!(1))).unwrap();
1987 assert_eq!(b.cumulative_ask_volume(2), dec!(10));
1989 }
1990
1991 #[test]
1992 fn test_cumulative_volume_empty_returns_zero() {
1993 let b = book("BTC-USD");
1994 assert_eq!(b.cumulative_bid_volume(5), dec!(0));
1995 assert_eq!(b.cumulative_ask_volume(5), dec!(0));
1996 }
1997
1998 #[test]
1999 fn test_top_n_bids_best_first() {
2000 let mut b = book("BTC-USD");
2001 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2002 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(2))).unwrap();
2003 b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(3))).unwrap();
2004 let top2 = b.top_n_bids(2);
2005 assert_eq!(top2.len(), 2);
2006 assert_eq!(top2[0].price, dec!(100)); assert_eq!(top2[1].price, dec!(99));
2008 }
2009
2010 #[test]
2011 fn test_top_n_asks_best_first() {
2012 let mut b = book("BTC-USD");
2013 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2014 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(2))).unwrap();
2015 b.apply(delta("BTC-USD", BookSide::Ask, dec!(103), dec!(3))).unwrap();
2016 let top2 = b.top_n_asks(2);
2017 assert_eq!(top2.len(), 2);
2018 assert_eq!(top2[0].price, dec!(101)); assert_eq!(top2[1].price, dec!(102));
2020 }
2021
2022 #[test]
2023 fn test_depth_ratio_balanced_book() {
2024 let mut b = book("BTC-USD");
2025 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(5))).unwrap();
2026 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(5))).unwrap();
2027 let ratio = b.depth_ratio(1).unwrap();
2028 assert!((ratio - 1.0).abs() < 1e-9);
2029 }
2030
2031 #[test]
2032 fn test_depth_ratio_empty_asks_returns_none() {
2033 let mut b = book("BTC-USD");
2034 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(5))).unwrap();
2035 assert!(b.depth_ratio(1).is_none());
2036 }
2037
2038 #[test]
2041 fn test_is_one_sided_bids_only() {
2042 let mut b = book("BTC-USD");
2043 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2044 assert!(b.is_one_sided());
2045 }
2046
2047 #[test]
2048 fn test_is_one_sided_asks_only() {
2049 let mut b = book("BTC-USD");
2050 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2051 assert!(b.is_one_sided());
2052 }
2053
2054 #[test]
2055 fn test_is_one_sided_false_with_both_sides() {
2056 let mut b = book("BTC-USD");
2057 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2058 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2059 assert!(!b.is_one_sided());
2060 }
2061
2062 #[test]
2063 fn test_is_one_sided_false_for_empty_book() {
2064 let b = book("BTC-USD");
2065 assert!(!b.is_one_sided());
2066 }
2067
2068 #[test]
2071 fn test_bid_ask_spread_bps_known_value() {
2072 let mut b = book("BTC-USD");
2073 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2074 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2075 let bps = b.bid_ask_spread_bps().unwrap();
2077 assert!((bps - 1.0 / 100.5 * 10_000.0).abs() < 0.01);
2078 }
2079
2080 #[test]
2081 fn test_bid_ask_spread_bps_none_when_one_sided() {
2082 let mut b = book("BTC-USD");
2083 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2084 assert!(b.bid_ask_spread_bps().is_none());
2085 }
2086
2087 #[test]
2088 fn test_bid_ask_spread_bps_none_for_empty_book() {
2089 let b = book("BTC-USD");
2090 assert!(b.bid_ask_spread_bps().is_none());
2091 }
2092
2093 #[test]
2096 fn test_ask_wall_returns_cheapest_ask_above_threshold() {
2097 let mut b = book("BTC-USD");
2098 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
2099 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(10))).unwrap();
2100 let wall = b.ask_wall(dec!(5)).unwrap();
2102 assert_eq!(wall.price, dec!(50200));
2103 assert_eq!(wall.quantity, dec!(10));
2104 }
2105
2106 #[test]
2107 fn test_ask_wall_none_when_no_level_meets_threshold() {
2108 let mut b = book("BTC-USD");
2109 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
2110 assert!(b.ask_wall(dec!(5)).is_none());
2111 }
2112
2113 #[test]
2114 fn test_bid_wall_returns_highest_bid_above_threshold() {
2115 let mut b = book("BTC-USD");
2116 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(10))).unwrap();
2117 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(2))).unwrap();
2118 let wall = b.bid_wall(dec!(5)).unwrap();
2120 assert_eq!(wall.price, dec!(49900));
2121 assert_eq!(wall.quantity, dec!(10));
2122 }
2123
2124 #[test]
2125 fn test_bid_wall_none_when_no_level_meets_threshold() {
2126 let mut b = book("BTC-USD");
2127 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1))).unwrap();
2128 assert!(b.bid_wall(dec!(5)).is_none());
2129 }
2130
2131 #[test]
2134 fn test_level_count_imbalance_balanced_sides() {
2135 let mut b = book("BTC-USD");
2136 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2137 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2138 assert_eq!(b.level_count_imbalance(), Some(0.0));
2140 }
2141
2142 #[test]
2143 fn test_level_count_imbalance_bids_only() {
2144 let mut b = book("BTC-USD");
2145 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2146 b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(1))).unwrap();
2147 assert_eq!(b.level_count_imbalance(), Some(1.0));
2149 }
2150
2151 #[test]
2152 fn test_level_count_imbalance_none_for_empty_book() {
2153 let b = book("BTC-USD");
2154 assert!(b.level_count_imbalance().is_none());
2155 }
2156
2157 #[test]
2160 fn test_total_bid_volume_sums_all_levels() {
2161 let mut b = book("BTC-USD");
2162 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(3))).unwrap();
2163 b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(2))).unwrap();
2164 assert_eq!(b.total_bid_volume(), dec!(5));
2165 }
2166
2167 #[test]
2168 fn test_total_ask_volume_sums_all_levels() {
2169 let mut b = book("BTC-USD");
2170 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(4))).unwrap();
2171 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(1))).unwrap();
2172 assert_eq!(b.total_ask_volume(), dec!(5));
2173 }
2174
2175 #[test]
2176 fn test_total_bid_volume_zero_for_empty_side() {
2177 let b = book("BTC-USD");
2178 assert_eq!(b.total_bid_volume(), dec!(0));
2179 }
2180
2181 #[test]
2184 fn test_bid_levels_above_counts_strictly_above() {
2185 let mut b = book("BTC-USD");
2186 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2187 b.apply(delta("BTC-USD", BookSide::Bid, dec!(101), dec!(1))).unwrap();
2188 b.apply(delta("BTC-USD", BookSide::Bid, dec!(102), dec!(1))).unwrap();
2189 assert_eq!(b.bid_levels_above(dec!(100)), 2);
2191 }
2192
2193 #[test]
2194 fn test_bid_levels_above_zero_when_none_above() {
2195 let mut b = book("BTC-USD");
2196 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2197 assert_eq!(b.bid_levels_above(dec!(100)), 0);
2198 }
2199
2200 #[test]
2201 fn test_ask_levels_below_counts_strictly_below() {
2202 let mut b = book("BTC-USD");
2203 b.apply(delta("BTC-USD", BookSide::Ask, dec!(100), dec!(1))).unwrap();
2204 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2205 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(1))).unwrap();
2206 assert_eq!(b.ask_levels_below(dec!(102)), 2);
2208 }
2209
2210 #[test]
2211 fn test_ask_levels_below_zero_for_empty_book() {
2212 let b = book("BTC-USD");
2213 assert_eq!(b.ask_levels_below(dec!(100)), 0);
2214 }
2215
2216 #[test]
2219 fn test_bid_ask_volume_ratio_returns_correct_ratio() {
2220 let mut b = book("BTC-USD");
2221 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(3))).unwrap();
2222 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2223 let ratio = b.bid_ask_volume_ratio().unwrap();
2225 assert!((ratio - 3.0).abs() < 1e-10);
2226 }
2227
2228 #[test]
2229 fn test_bid_ask_volume_ratio_none_when_ask_empty() {
2230 let mut b = book("BTC-USD");
2231 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2232 assert!(b.bid_ask_volume_ratio().is_none());
2233 }
2234
2235 #[test]
2236 fn test_bid_ask_volume_ratio_none_when_bid_empty() {
2237 let mut b = book("BTC-USD");
2238 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2239 assert!(b.bid_ask_volume_ratio().is_none());
2240 }
2241
2242 #[test]
2243 fn test_top_n_bid_volume_sums_top_levels() {
2244 let mut b = book("BTC-USD");
2245 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap(); b.apply(delta("BTC-USD", BookSide::Bid, dec!(101), dec!(2))).unwrap();
2247 b.apply(delta("BTC-USD", BookSide::Bid, dec!(102), dec!(3))).unwrap(); assert_eq!(b.top_n_bid_volume(2), dec!(5));
2250 }
2251
2252 #[test]
2253 fn test_top_n_bid_volume_all_when_n_exceeds_levels() {
2254 let mut b = book("BTC-USD");
2255 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2256 b.apply(delta("BTC-USD", BookSide::Bid, dec!(101), dec!(2))).unwrap();
2257 assert_eq!(b.top_n_bid_volume(5), dec!(3));
2259 }
2260
2261 #[test]
2262 fn test_top_n_bid_volume_zero_for_empty_book() {
2263 let b = book("BTC-USD");
2264 assert_eq!(b.top_n_bid_volume(3), dec!(0));
2265 }
2266
2267 #[test]
2270 fn test_imbalance_ratio_positive_when_more_bids() {
2271 let mut b = book("BTC-USD");
2272 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(3))).unwrap();
2273 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2274 let ratio = b.imbalance_ratio().unwrap();
2276 assert!((ratio - 0.5).abs() < 1e-10);
2277 }
2278
2279 #[test]
2280 fn test_imbalance_ratio_negative_when_more_asks() {
2281 let mut b = book("BTC-USD");
2282 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2283 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(3))).unwrap();
2284 let ratio = b.imbalance_ratio().unwrap();
2286 assert!((ratio - (-0.5)).abs() < 1e-10);
2287 }
2288
2289 #[test]
2290 fn test_imbalance_ratio_none_when_both_empty() {
2291 let b = book("BTC-USD");
2292 assert!(b.imbalance_ratio().is_none());
2293 }
2294
2295 #[test]
2296 fn test_top_n_ask_volume_sums_lowest_asks() {
2297 let mut b = book("BTC-USD");
2298 b.apply(delta("BTC-USD", BookSide::Ask, dec!(100), dec!(2))).unwrap(); b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(3))).unwrap();
2300 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(5))).unwrap(); assert_eq!(b.top_n_ask_volume(2), dec!(5));
2303 }
2304
2305 #[test]
2306 fn test_top_n_ask_volume_zero_for_empty_book() {
2307 let b = book("BTC-USD");
2308 assert_eq!(b.top_n_ask_volume(3), dec!(0));
2309 }
2310
2311 #[test]
2314 fn test_has_ask_at_true_when_ask_exists() {
2315 let mut b = book("BTC-USD");
2316 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
2317 assert!(b.has_ask_at(dec!(101)));
2318 }
2319
2320 #[test]
2321 fn test_has_ask_at_false_when_no_ask_at_price() {
2322 let mut b = book("BTC-USD");
2323 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(1))).unwrap();
2324 assert!(!b.has_ask_at(dec!(101)));
2325 }
2326
2327 #[test]
2328 fn test_bid_ask_depth_correct_counts() {
2329 let mut b = book("BTC-USD");
2330 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2331 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2332 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2333 assert_eq!(b.bid_ask_depth(), (2, 1));
2334 }
2335
2336 #[test]
2337 fn test_bid_ask_depth_zero_for_empty_book() {
2338 let b = book("BTC-USD");
2339 assert_eq!(b.bid_ask_depth(), (0, 0));
2340 }
2341
2342 #[test]
2344 fn test_best_bid_qty_returns_top_bid_quantity() {
2345 let mut b = book("BTC-USD");
2346 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(3))).unwrap();
2347 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(5))).unwrap();
2348 assert_eq!(b.best_bid_qty(), Some(dec!(3)));
2350 }
2351
2352 #[test]
2353 fn test_best_ask_qty_returns_top_ask_quantity() {
2354 let mut b = book("BTC-USD");
2355 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(7))).unwrap();
2356 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(2))).unwrap();
2357 assert_eq!(b.best_ask_qty(), Some(dec!(7)));
2359 }
2360
2361 #[test]
2362 fn test_best_bid_qty_none_when_no_bids() {
2363 let b = book("BTC-USD");
2364 assert!(b.best_bid_qty().is_none());
2365 }
2366
2367 #[test]
2368 fn test_best_ask_qty_none_when_no_asks() {
2369 let b = book("BTC-USD");
2370 assert!(b.best_ask_qty().is_none());
2371 }
2372
2373 #[test]
2375 fn test_total_book_volume_sum_of_bids_and_asks() {
2376 let mut b = book("BTC-USD");
2377 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(3))).unwrap();
2378 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
2379 assert_eq!(b.total_book_volume(), dec!(5));
2380 }
2381
2382 #[test]
2383 fn test_total_book_volume_zero_on_empty_book() {
2384 let b = book("BTC-USD");
2385 assert_eq!(b.total_book_volume(), dec!(0));
2386 }
2387
2388 #[test]
2390 fn test_price_range_bids_correct_range() {
2391 let mut b = book("BTC-USD");
2392 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2393 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2394 b.apply(delta("BTC-USD", BookSide::Bid, dec!(97), dec!(1))).unwrap();
2395 assert_eq!(b.price_range_bids(), Some(dec!(3)));
2397 }
2398
2399 #[test]
2400 fn test_price_range_bids_none_with_single_bid() {
2401 let mut b = book("BTC-USD");
2402 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2403 assert!(b.price_range_bids().is_none());
2404 }
2405
2406 #[test]
2409 fn test_is_tight_spread_true_when_spread_at_threshold() {
2410 let mut b = book("BTC-USD");
2411 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2412 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2413 assert!(b.is_tight_spread(dec!(1)));
2415 }
2416
2417 #[test]
2418 fn test_is_tight_spread_false_when_spread_above_threshold() {
2419 let mut b = book("BTC-USD");
2420 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2421 b.apply(delta("BTC-USD", BookSide::Ask, dec!(103), dec!(1))).unwrap();
2422 assert!(!b.is_tight_spread(dec!(1)));
2424 }
2425
2426 #[test]
2427 fn test_is_tight_spread_false_when_empty() {
2428 let b = book("BTC-USD");
2429 assert!(!b.is_tight_spread(dec!(10)));
2430 }
2431
2432 #[test]
2435 fn test_best_bid_price_returns_price() {
2436 let mut b = book("X");
2437 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(5))).unwrap();
2438 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(3))).unwrap();
2439 assert_eq!(b.best_bid_price(), Some(dec!(99)));
2440 }
2441
2442 #[test]
2443 fn test_best_ask_price_returns_price() {
2444 let mut b = book("X");
2445 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
2446 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
2447 assert_eq!(b.best_ask_price(), Some(dec!(101)));
2448 }
2449
2450 #[test]
2451 fn test_best_bid_price_none_when_empty() {
2452 assert_eq!(book("X").best_bid_price(), None);
2453 }
2454
2455 #[test]
2456 fn test_best_ask_price_none_when_empty() {
2457 assert_eq!(book("X").best_ask_price(), None);
2458 }
2459
2460 #[test]
2463 fn test_is_crossed_false_when_normal() {
2464 let mut b = book("X");
2465 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2466 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2467 assert!(!b.is_crossed());
2468 }
2469
2470 #[test]
2471 fn test_is_crossed_false_when_empty() {
2472 assert!(!book("X").is_crossed());
2473 }
2474
2475 #[test]
2478 fn test_has_bids_true_when_bid_present() {
2479 let mut b = book("X");
2480 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2481 assert!(b.has_bids());
2482 }
2483
2484 #[test]
2485 fn test_has_bids_false_when_empty() {
2486 assert!(!book("X").has_bids());
2487 }
2488
2489 #[test]
2490 fn test_has_asks_true_when_ask_present() {
2491 let mut b = book("X");
2492 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2493 assert!(b.has_asks());
2494 }
2495
2496 #[test]
2497 fn test_has_asks_false_when_empty() {
2498 assert!(!book("X").has_asks());
2499 }
2500
2501 #[test]
2504 fn test_ask_price_range_none_when_empty() {
2505 assert_eq!(book("X").ask_price_range(), None);
2506 }
2507
2508 #[test]
2509 fn test_ask_price_range_zero_when_single_level() {
2510 let mut b = book("X");
2511 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(1))).unwrap();
2512 assert_eq!(b.ask_price_range(), Some(dec!(0)));
2513 }
2514
2515 #[test]
2516 fn test_ask_price_range_correct_with_multiple_levels() {
2517 let mut b = book("X");
2518 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(1))).unwrap();
2519 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(1))).unwrap();
2520 b.apply(delta("X", BookSide::Ask, dec!(105), dec!(1))).unwrap();
2521 assert_eq!(b.ask_price_range(), Some(dec!(5)));
2522 }
2523
2524 #[test]
2525 fn test_bid_price_range_none_when_empty() {
2526 assert_eq!(book("X").bid_price_range(), None);
2527 }
2528
2529 #[test]
2530 fn test_bid_price_range_correct_with_multiple_levels() {
2531 let mut b = book("X");
2532 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
2533 b.apply(delta("X", BookSide::Bid, dec!(96), dec!(1))).unwrap();
2534 b.apply(delta("X", BookSide::Bid, dec!(94), dec!(1))).unwrap();
2535 assert_eq!(b.bid_price_range(), Some(dec!(4)));
2537 }
2538
2539 #[test]
2542 fn test_mid_spread_ratio_none_when_empty() {
2543 assert_eq!(book("X").mid_spread_ratio(), None);
2544 }
2545
2546 #[test]
2547 fn test_mid_spread_ratio_correct() {
2548 let mut b = book("X");
2549 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2550 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2551 let ratio = b.mid_spread_ratio().unwrap();
2553 assert!((ratio - 0.02).abs() < 1e-9);
2554 }
2555
2556 #[test]
2559 fn test_volume_imbalance_none_when_empty() {
2560 assert_eq!(book("X").volume_imbalance(), None);
2561 }
2562
2563 #[test]
2564 fn test_volume_imbalance_positive_when_more_bids() {
2565 let mut b = book("X");
2566 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(3))).unwrap();
2567 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2568 let imb = b.volume_imbalance().unwrap();
2570 assert!((imb - 0.5).abs() < 1e-9);
2571 }
2572
2573 #[test]
2574 fn test_volume_imbalance_negative_when_more_asks() {
2575 let mut b = book("X");
2576 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2577 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(3))).unwrap();
2578 let imb = b.volume_imbalance().unwrap();
2580 assert!((imb - (-0.5)).abs() < 1e-9);
2581 }
2582
2583 #[test]
2584 fn test_volume_imbalance_zero_when_equal() {
2585 let mut b = book("X");
2586 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(2))).unwrap();
2587 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(2))).unwrap();
2588 let imb = b.volume_imbalance().unwrap();
2589 assert!(imb.abs() < 1e-9);
2590 }
2591
2592 #[test]
2595 fn test_ask_volume_within_sums_levels_in_range() {
2596 let mut b = book("X");
2597 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(2))).unwrap();
2598 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(3))).unwrap();
2599 b.apply(delta("X", BookSide::Ask, dec!(105), dec!(10))).unwrap();
2600 let vol = b.ask_volume_within(dec!(2));
2602 assert_eq!(vol, dec!(5)); }
2604
2605 #[test]
2606 fn test_ask_volume_within_zero_when_empty() {
2607 assert_eq!(book("X").ask_volume_within(dec!(10)), dec!(0));
2608 }
2609
2610 #[test]
2611 fn test_bid_volume_within_sums_levels_in_range() {
2612 let mut b = book("X");
2613 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(5))).unwrap();
2614 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(3))).unwrap();
2615 b.apply(delta("X", BookSide::Bid, dec!(95), dec!(10))).unwrap();
2616 let vol = b.bid_volume_within(dec!(2));
2618 assert_eq!(vol, dec!(8)); }
2620
2621 #[test]
2622 fn test_bid_volume_within_zero_when_empty() {
2623 assert_eq!(book("X").bid_volume_within(dec!(10)), dec!(0));
2624 }
2625
2626 #[test]
2629 fn test_ask_level_count_zero_when_empty() {
2630 assert_eq!(book("X").ask_level_count(), 0);
2631 }
2632
2633 #[test]
2634 fn test_ask_level_count_correct() {
2635 let mut b = book("X");
2636 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(1))).unwrap();
2637 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(2))).unwrap();
2638 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
2639 assert_eq!(b.ask_level_count(), 3);
2640 }
2641
2642 #[test]
2643 fn test_bid_level_count_correct() {
2644 let mut b = book("X");
2645 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2646 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(2))).unwrap();
2647 assert_eq!(b.bid_level_count(), 2);
2648 }
2649
2650 #[test]
2653 fn test_price_impact_buy_correct_single_level() {
2654 let mut b = book("X");
2655 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(10))).unwrap();
2656 assert_eq!(b.price_impact_buy(dec!(5)), Some(dec!(100)));
2658 }
2659
2660 #[test]
2661 fn test_price_impact_buy_spans_levels() {
2662 let mut b = book("X");
2663 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(5))).unwrap();
2664 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
2665 assert_eq!(b.price_impact_buy(dec!(10)), Some(dec!(100.5)));
2667 }
2668
2669 #[test]
2670 fn test_price_impact_buy_none_when_insufficient_liquidity() {
2671 let mut b = book("X");
2672 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(3))).unwrap();
2673 assert!(b.price_impact_buy(dec!(5)).is_none());
2674 }
2675
2676 #[test]
2677 fn test_price_impact_sell_correct_single_level() {
2678 let mut b = book("X");
2679 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(10))).unwrap();
2680 assert_eq!(b.price_impact_sell(dec!(5)), Some(dec!(99)));
2681 }
2682
2683 #[test]
2686 fn test_total_value_at_level_bid_returns_price_times_qty() {
2687 let mut b = book("X");
2688 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(5))).unwrap();
2689 assert_eq!(b.total_value_at_level(BookSide::Bid, dec!(100)), Some(dec!(500)));
2690 }
2691
2692 #[test]
2693 fn test_total_value_at_level_ask_returns_price_times_qty() {
2694 let mut b = book("X");
2695 b.apply(delta("X", BookSide::Ask, dec!(105), dec!(3))).unwrap();
2696 assert_eq!(b.total_value_at_level(BookSide::Ask, dec!(105)), Some(dec!(315)));
2697 }
2698
2699 #[test]
2700 fn test_total_value_at_level_none_when_price_missing() {
2701 let b = book("X");
2702 assert!(b.total_value_at_level(BookSide::Bid, dec!(100)).is_none());
2703 }
2704
2705 #[test]
2708 fn test_ask_volume_above_sums_asks_above_price() {
2709 let mut b = book("X");
2710 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
2711 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
2712 b.apply(delta("X", BookSide::Ask, dec!(103), dec!(2))).unwrap();
2713 assert_eq!(b.ask_volume_above(dec!(101)), dec!(5));
2715 }
2716
2717 #[test]
2718 fn test_ask_volume_above_zero_when_no_asks_above() {
2719 let mut b = book("X");
2720 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(10))).unwrap();
2721 assert_eq!(b.ask_volume_above(dec!(100)), dec!(0));
2722 }
2723
2724 #[test]
2725 fn test_bid_volume_below_sums_bids_below_price() {
2726 let mut b = book("X");
2727 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(4))).unwrap();
2728 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(6))).unwrap();
2729 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(2))).unwrap();
2730 assert_eq!(b.bid_volume_below(dec!(100)), dec!(10));
2732 }
2733
2734 #[test]
2735 fn test_bid_volume_below_zero_when_no_bids_below() {
2736 let mut b = book("X");
2737 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(5))).unwrap();
2738 assert_eq!(b.bid_volume_below(dec!(100)), dec!(0));
2739 }
2740
2741 #[test]
2744 fn test_total_notional_both_sides_sums_both() {
2745 let mut b = book("X");
2746 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(2))).unwrap(); b.apply(delta("X", BookSide::Ask, dec!(105), dec!(3))).unwrap(); assert_eq!(b.total_notional_both_sides(), dec!(515));
2749 }
2750
2751 #[test]
2752 fn test_total_notional_both_sides_zero_when_empty() {
2753 let b = book("X");
2754 assert_eq!(b.total_notional_both_sides(), dec!(0));
2755 }
2756
2757 #[test]
2760 fn test_level_count_both_sides_zero_when_empty() {
2761 let b = book("X");
2762 assert_eq!(b.level_count_both_sides(), 0);
2763 }
2764
2765 #[test]
2766 fn test_level_count_both_sides_counts_all_levels() {
2767 let mut b = book("X");
2768 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2769 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
2770 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2771 assert_eq!(b.level_count_both_sides(), 3);
2772 }
2773
2774 #[test]
2777 fn test_ask_price_at_rank_best_ask_at_zero() {
2778 let mut b = book("X");
2779 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2780 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(1))).unwrap();
2781 assert_eq!(b.ask_price_at_rank(0), Some(dec!(101)));
2782 }
2783
2784 #[test]
2785 fn test_bid_price_at_rank_best_bid_at_zero() {
2786 let mut b = book("X");
2787 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2788 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
2789 assert_eq!(b.bid_price_at_rank(0), Some(dec!(99)));
2790 }
2791
2792 #[test]
2793 fn test_ask_price_at_rank_none_out_of_bounds() {
2794 let b = book("X");
2795 assert!(b.ask_price_at_rank(0).is_none());
2796 }
2797
2798 #[test]
2801 fn test_price_level_exists_true_when_present() {
2802 let mut b = book("X");
2803 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2804 assert!(b.price_level_exists(BookSide::Bid, dec!(100)));
2805 }
2806
2807 #[test]
2808 fn test_price_level_exists_false_when_absent() {
2809 let b = book("X");
2810 assert!(!b.price_level_exists(BookSide::Bid, dec!(100)));
2811 }
2812
2813 #[test]
2814 fn test_price_level_exists_ask_side() {
2815 let mut b = book("X");
2816 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
2817 assert!(b.price_level_exists(BookSide::Ask, dec!(101)));
2818 assert!(!b.price_level_exists(BookSide::Ask, dec!(102)));
2819 }
2820}