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.asks.iter().next()?.0; 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 let has_bids = !self.bids.is_empty();
360 let has_asks = !self.asks.is_empty();
361 has_bids != has_asks
362 }
363
364 pub fn level_count_imbalance(&self) -> Option<f64> {
372 let total = self.bids.len() + self.asks.len();
373 if total == 0 {
374 return None;
375 }
376 let diff = self.bids.len() as f64 - self.asks.len() as f64;
377 Some(diff / total as f64)
378 }
379
380 pub fn bid_ask_spread_bps(&self) -> Option<f64> {
384 let bid = self.best_bid()?.price;
385 let ask = self.best_ask()?.price;
386 let mid = (bid + ask) / Decimal::from(2);
387 if mid.is_zero() {
388 return None;
389 }
390 let spread = ask - bid;
391 let spread_f: f64 = spread.to_string().parse().ok()?;
392 let mid_f: f64 = mid.to_string().parse().ok()?;
393 Some(spread_f / mid_f * 10_000.0)
394 }
395
396 pub fn total_bid_volume(&self) -> Decimal {
400 self.bids.values().copied().sum()
401 }
402
403 pub fn total_ask_volume(&self) -> Decimal {
405 self.asks.values().copied().sum()
406 }
407
408 pub fn cumulative_bid_volume(&self, n: usize) -> Decimal {
413 self.bids.values().rev().take(n).copied().sum()
414 }
415
416 pub fn cumulative_ask_volume(&self, n: usize) -> Decimal {
421 self.asks.values().take(n).copied().sum()
422 }
423
424 pub fn top_n_bids(&self, n: usize) -> Vec<PriceLevel> {
428 self.bids
429 .iter()
430 .rev()
431 .take(n)
432 .map(|(p, q)| PriceLevel::new(*p, *q))
433 .collect()
434 }
435
436 pub fn top_n_asks(&self, n: usize) -> Vec<PriceLevel> {
440 self.asks
441 .iter()
442 .take(n)
443 .map(|(p, q)| PriceLevel::new(*p, *q))
444 .collect()
445 }
446
447 pub fn depth_ratio(&self, n: usize) -> Option<f64> {
452 use rust_decimal::prelude::ToPrimitive;
453 let ask_vol = self.cumulative_ask_volume(n);
454 if ask_vol.is_zero() {
455 return None;
456 }
457 (self.cumulative_bid_volume(n) / ask_vol).to_f64()
458 }
459
460 pub fn ask_wall(&self, min_qty: Decimal) -> Option<PriceLevel> {
465 self.asks
466 .iter()
467 .find(|(_, qty)| **qty >= min_qty)
468 .map(|(price, qty)| PriceLevel::new(*price, *qty))
469 }
470
471 pub fn bid_wall(&self, min_qty: Decimal) -> Option<PriceLevel> {
476 self.bids
477 .iter()
478 .rev()
479 .find(|(_, qty)| **qty >= min_qty)
480 .map(|(price, qty)| PriceLevel::new(*price, *qty))
481 }
482
483 pub fn bid_levels_above(&self, price: Decimal) -> usize {
488 self.bids.range((std::ops::Bound::Excluded(&price), std::ops::Bound::Unbounded)).count()
489 }
490
491 pub fn ask_levels_below(&self, price: Decimal) -> usize {
496 self.asks.range(..price).count()
497 }
498
499 pub fn bid_ask_volume_ratio(&self) -> Option<f64> {
505 use rust_decimal::prelude::ToPrimitive;
506 let bid = self.total_bid_volume();
507 let ask = self.total_ask_volume();
508 if bid.is_zero() || ask.is_zero() {
509 return None;
510 }
511 let bid_f = bid.to_f64()?;
512 let ask_f = ask.to_f64()?;
513 Some(bid_f / ask_f)
514 }
515
516 pub fn top_n_bid_volume(&self, n: usize) -> Decimal {
521 self.bids.iter().rev().take(n).map(|(_, qty)| *qty).sum()
522 }
523
524 pub fn imbalance_ratio(&self) -> Option<f64> {
531 use rust_decimal::prelude::ToPrimitive;
532 let bid = self.total_bid_volume();
533 let ask = self.total_ask_volume();
534 let total = bid + ask;
535 if total.is_zero() {
536 return None;
537 }
538 let bid_f = bid.to_f64()?;
539 let ask_f = ask.to_f64()?;
540 let total_f = bid_f + ask_f;
541 Some((bid_f - ask_f) / total_f)
542 }
543
544 pub fn top_n_ask_volume(&self, n: usize) -> Decimal {
550 self.asks.iter().take(n).map(|(_, qty)| *qty).sum()
551 }
552
553 pub fn has_ask_at(&self, price: Decimal) -> bool {
555 self.asks.get(&price).map(|q| !q.is_zero()).unwrap_or(false)
556 }
557
558 pub fn bid_ask_depth(&self) -> (usize, usize) {
561 (self.bids.len(), self.asks.len())
562 }
563
564 pub fn total_book_volume(&self) -> Decimal {
566 self.total_bid_volume() + self.total_ask_volume()
567 }
568
569 pub fn price_range_bids(&self) -> Option<Decimal> {
573 if self.bids.len() < 2 {
574 return None;
575 }
576 let best = *self.bids.keys().next_back()?;
577 let worst = *self.bids.keys().next()?;
578 Some(best - worst)
579 }
580
581 pub fn spread_pct(&self) -> Option<f64> {
586 use rust_decimal::prelude::ToPrimitive;
587 let mid = self.mid_price()?;
588 if mid.is_zero() {
589 return None;
590 }
591 let spread = self.spread()?;
592 (spread / mid * Decimal::from(100)).to_f64()
593 }
594
595 pub fn is_tight_spread(&self, threshold: Decimal) -> bool {
599 match self.spread() {
600 Some(s) => s <= threshold,
601 None => false,
602 }
603 }
604
605 pub fn total_depth(&self) -> usize {
609 self.bids.len() + self.asks.len()
610 }
611
612 pub fn total_volume(&self) -> Decimal {
616 self.bid_volume_total() + self.ask_volume_total()
617 }
618
619 pub fn symbol(&self) -> &str {
621 &self.symbol
622 }
623
624 pub fn last_sequence(&self) -> Option<u64> {
626 self.last_sequence
627 }
628
629 pub fn quote_imbalance(&self) -> Option<f64> {
634 use rust_decimal::prelude::ToPrimitive;
635 let bid_qty = *self.bids.iter().next_back()?.1;
636 let ask_qty = *self.asks.iter().next()?.1;
637 let total = bid_qty + ask_qty;
638 if total.is_zero() {
639 return None;
640 }
641 (bid_qty / total).to_f64()
642 }
643
644 pub fn contains_bid(&self, price: Decimal) -> bool {
646 self.bids.contains_key(&price)
647 }
648
649 pub fn contains_ask(&self, price: Decimal) -> bool {
651 self.asks.contains_key(&price)
652 }
653
654 pub fn volume_at_bid(&self, price: Decimal) -> Option<Decimal> {
656 self.bids.get(&price).copied()
657 }
658
659 pub fn volume_at_ask(&self, price: Decimal) -> Option<Decimal> {
661 self.asks.get(&price).copied()
662 }
663
664 pub fn level_count(&self, side: BookSide) -> usize {
669 match side {
670 BookSide::Bid => self.bids.len(),
671 BookSide::Ask => self.asks.len(),
672 }
673 }
674
675 pub fn level_count_both_sides(&self) -> usize {
677 self.bids.len() + self.asks.len()
678 }
679
680 pub fn ask_price_at_rank(&self, n: usize) -> Option<Decimal> {
684 self.asks.keys().nth(n).copied()
685 }
686
687 pub fn bid_price_at_rank(&self, n: usize) -> Option<Decimal> {
691 self.bids.keys().nth_back(n).copied()
692 }
693
694 pub fn quote_density(&self, side: BookSide) -> Option<Decimal> {
699 let map = match side {
700 BookSide::Bid => &self.bids,
701 BookSide::Ask => &self.asks,
702 };
703 if map.len() < 2 { return None; }
704 let min_p = *map.keys().next()?;
705 let max_p = *map.keys().next_back()?;
706 let range = max_p - min_p;
707 if range.is_zero() { return None; }
708 Some(Decimal::from(map.len()) / range)
709 }
710
711 pub fn bid_ask_qty_ratio(&self) -> Option<f64> {
716 use rust_decimal::prelude::ToPrimitive;
717 let ask_vol = self.ask_volume_total();
718 if ask_vol.is_zero() { return None; }
719 (self.bid_volume_total() / ask_vol).to_f64()
720 }
721
722 pub fn top_bid_qty(&self) -> Option<Decimal> {
726 self.bids.iter().next_back().map(|(_, &q)| q)
727 }
728
729 pub fn top_ask_qty(&self) -> Option<Decimal> {
733 self.asks.iter().next().map(|(_, &q)| q)
734 }
735
736 pub fn cumulative_bid_qty(&self, n: usize) -> Decimal {
740 self.bids.iter().rev().take(n).map(|(_, &q)| q).sum()
741 }
742
743 pub fn cumulative_ask_qty(&self, n: usize) -> Decimal {
747 self.asks.iter().take(n).map(|(_, &q)| q).sum()
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.bids.is_empty() { return None; }
767 Some(self.asks.len() as f64 / self.bids.len() 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: Decimal = self.top_bids(n).iter().map(|l| l.quantity).sum();
865 let ask_vol: Decimal = self.top_asks(n).iter().map(|l| l.quantity).sum();
866 if bid_vol.is_zero() || ask_vol.is_zero() {
867 return None;
868 }
869 let total = bid_vol + ask_vol;
870 if total.is_zero() {
871 return None;
872 }
873 ((bid_vol - ask_vol) / total).to_f64()
874 }
875
876 pub fn vwap(&self, side: BookSide, n: usize) -> Option<Decimal> {
881 let levels = match side {
882 BookSide::Bid => self.top_bids(n),
883 BookSide::Ask => self.top_asks(n),
884 };
885 let total_vol: Decimal = levels.iter().map(|l| l.quantity).sum();
886 if total_vol.is_zero() {
887 return None;
888 }
889 let price_vol_sum: Decimal = levels.iter().map(|l| l.price * l.quantity).sum();
890 Some(price_vol_sum / total_vol)
891 }
892
893 pub fn price_at_volume(&self, side: BookSide, target_volume: Decimal) -> Option<Decimal> {
901 if target_volume.is_zero() {
902 return None;
903 }
904 let levels: Vec<(Decimal, Decimal)> = match side {
905 BookSide::Bid => self.bids.iter().rev().map(|(p, q)| (*p, *q)).collect(),
906 BookSide::Ask => self.asks.iter().map(|(p, q)| (*p, *q)).collect(),
907 };
908 if levels.is_empty() {
909 return None;
910 }
911 let mut remaining = target_volume;
912 let mut notional = Decimal::ZERO;
913 let mut filled = Decimal::ZERO;
914 for (price, qty) in &levels {
915 if remaining.is_zero() {
916 break;
917 }
918 let take = (*qty).min(remaining);
919 notional += price * take;
920 filled += take;
921 remaining -= take;
922 }
923 if filled.is_zero() {
924 return None;
925 }
926 Some(notional / filled)
927 }
928
929 pub fn depth_imbalance(&self, n: usize) -> Option<f64> {
938 use rust_decimal::prelude::ToPrimitive;
939 if n == 0 {
940 return None;
941 }
942 let bid_vol: Decimal = self.bids.values().rev().take(n).copied().sum();
943 let ask_vol: Decimal = self.asks.values().take(n).copied().sum();
944 let total = bid_vol + ask_vol;
945 if total.is_zero() {
946 return None;
947 }
948 ((bid_vol - ask_vol) / total).to_f64()
949 }
950
951 pub fn levels(&self, side: BookSide, n: usize) -> Vec<PriceLevel> {
957 match side {
958 BookSide::Bid => self
959 .bids
960 .iter()
961 .rev()
962 .take(n)
963 .map(|(p, q)| PriceLevel::new(*p, *q))
964 .collect(),
965 BookSide::Ask => self
966 .asks
967 .iter()
968 .take(n)
969 .map(|(p, q)| PriceLevel::new(*p, *q))
970 .collect(),
971 }
972 }
973
974 pub fn bid_volume_at_price(&self, price: Decimal) -> Option<Decimal> {
976 self.bids.get(&price).copied()
977 }
978
979 pub fn ask_volume_at_price(&self, price: Decimal) -> Option<Decimal> {
981 self.asks.get(&price).copied()
982 }
983
984 pub fn snapshot(&self) -> (Vec<PriceLevel>, Vec<PriceLevel>) {
994 let bids = self
995 .bids
996 .iter()
997 .rev()
998 .map(|(p, q)| PriceLevel::new(*p, *q))
999 .collect();
1000 let asks = self
1001 .asks
1002 .iter()
1003 .map(|(p, q)| PriceLevel::new(*p, *q))
1004 .collect();
1005 (bids, asks)
1006 }
1007
1008 pub fn best_bid_price(&self) -> Option<Decimal> {
1010 self.best_bid().map(|l| l.price)
1011 }
1012
1013 pub fn best_ask_price(&self) -> Option<Decimal> {
1015 self.best_ask().map(|l| l.price)
1016 }
1017
1018 pub fn is_crossed(&self) -> bool {
1023 match (self.best_bid(), self.best_ask()) {
1024 (Some(bid), Some(ask)) => bid.price >= ask.price,
1025 _ => false,
1026 }
1027 }
1028
1029 pub fn has_bids(&self) -> bool {
1031 !self.bids.is_empty()
1032 }
1033
1034 pub fn has_asks(&self) -> bool {
1036 !self.asks.is_empty()
1037 }
1038
1039 pub fn ask_price_range(&self) -> Option<Decimal> {
1043 if self.asks.is_empty() {
1044 return None;
1045 }
1046 let best = *self.asks.keys().next()?;
1047 let worst = *self.asks.keys().next_back()?;
1048 Some(worst - best)
1049 }
1050
1051 pub fn bid_price_range(&self) -> Option<Decimal> {
1055 if self.bids.is_empty() {
1056 return None;
1057 }
1058 let best = *self.bids.keys().next_back()?;
1059 let worst = *self.bids.keys().next()?;
1060 Some(best - worst)
1061 }
1062
1063 pub fn mid_spread_ratio(&self) -> Option<f64> {
1067 use rust_decimal::prelude::ToPrimitive;
1068 let spread = self.spread()?;
1069 let mid = self.mid_price()?;
1070 if mid.is_zero() {
1071 return None;
1072 }
1073 (spread / mid).to_f64()
1074 }
1075
1076 pub fn volume_imbalance(&self) -> Option<f64> {
1081 use rust_decimal::prelude::ToPrimitive;
1082 let bid = self.total_bid_volume();
1083 let ask = self.total_ask_volume();
1084 let total = bid + ask;
1085 if total.is_zero() {
1086 return None;
1087 }
1088 ((bid - ask) / total).to_f64()
1089 }
1090
1091 fn check_crossed(&self) -> Result<(), StreamError> {
1092 if let (Some(bid), Some(ask)) = (self.best_bid(), self.best_ask()) {
1093 if bid.price >= ask.price {
1094 return Err(StreamError::BookCrossed {
1095 symbol: self.symbol.clone(),
1096 bid: bid.price,
1097 ask: ask.price,
1098 });
1099 }
1100 }
1101 Ok(())
1102 }
1103
1104 pub fn fee_estimate(&self, side: BookSide, qty: Decimal, fee_bps: Decimal) -> Option<Decimal> {
1109 if qty <= Decimal::ZERO { return None; }
1110 let best_price = match side {
1111 BookSide::Bid => self.bids.keys().next_back().copied()?,
1112 BookSide::Ask => self.asks.keys().next().copied()?,
1113 };
1114 let impact = self.price_impact(side, qty).unwrap_or(Decimal::ZERO);
1115 let fill_price = best_price + impact;
1116 Some(fill_price * qty * fee_bps / Decimal::from(10_000u32))
1117 }
1118
1119 pub fn spread_ticks(&self, tick_size: Decimal) -> Option<Decimal> {
1123 if tick_size.is_zero() { return None; }
1124 let spread = self.spread()?;
1125 Some(spread / tick_size)
1126 }
1127
1128 pub fn spread_bps(&self) -> Option<f64> {
1132 use rust_decimal::prelude::ToPrimitive;
1133 let mid = self.mid_price()?;
1134 if mid.is_zero() {
1135 return None;
1136 }
1137 let spread = self.spread()?;
1138 (spread / mid * Decimal::from(10_000u32)).to_f64()
1139 }
1140
1141 pub fn depth_at_pct(&self, side: BookSide, pct: f64) -> Option<Decimal> {
1148 use rust_decimal::prelude::FromPrimitive;
1149 if pct < 0.0 { return None; }
1150 let pct_dec = Decimal::from_f64(pct / 100.0)?;
1151 match side {
1152 BookSide::Bid => {
1153 let best = *self.bids.keys().next_back()?;
1154 let threshold = best * (Decimal::ONE - pct_dec);
1155 Some(self.bids.range(threshold..).map(|(_, q)| q).sum())
1156 }
1157 BookSide::Ask => {
1158 let best = *self.asks.keys().next()?;
1159 let threshold = best * (Decimal::ONE + pct_dec);
1160 Some(self.asks.range(..=threshold).map(|(_, q)| q).sum())
1161 }
1162 }
1163 }
1164
1165 pub fn microprice(&self) -> Option<Decimal> {
1172 let best_bid = *self.bids.keys().next_back()?;
1173 let best_ask = *self.asks.keys().next()?;
1174 let bid_qty = *self.bids.get(&best_bid)?;
1175 let ask_qty = *self.asks.get(&best_ask)?;
1176 let total_qty = bid_qty + ask_qty;
1177 if total_qty.is_zero() { return None; }
1178 Some((ask_qty * best_bid + bid_qty * best_ask) / total_qty)
1179 }
1180
1181 pub fn best_n_levels(&self, side: BookSide, n: usize) -> Vec<(Decimal, Decimal)> {
1187 match side {
1188 BookSide::Bid => self.bids.iter().rev().take(n)
1189 .map(|(&p, &q)| (p, q)).collect(),
1190 BookSide::Ask => self.asks.iter().take(n)
1191 .map(|(&p, &q)| (p, q)).collect(),
1192 }
1193 }
1194
1195 pub fn price_impact(&self, side: BookSide, qty: Decimal) -> Option<Decimal> {
1201 if qty <= Decimal::ZERO { return None; }
1202 let best_price = match side {
1203 BookSide::Bid => self.bids.keys().next_back().copied()?,
1204 BookSide::Ask => self.asks.keys().next().copied()?,
1205 };
1206 let mut remaining = qty;
1207 let mut cost = Decimal::ZERO;
1208 let levels: Box<dyn Iterator<Item = (&Decimal, &Decimal)>> = match side {
1209 BookSide::Bid => Box::new(self.bids.iter().rev()),
1210 BookSide::Ask => Box::new(self.asks.iter()),
1211 };
1212 for (&price, &level_qty) in levels {
1213 if remaining <= Decimal::ZERO { break; }
1214 let filled = remaining.min(level_qty);
1215 cost += price * filled;
1216 remaining -= filled;
1217 }
1218 if remaining > Decimal::ZERO { return None; } let avg_fill = cost / qty;
1220 Some((avg_fill - best_price).abs())
1221 }
1222
1223 pub fn total_value_at_level(&self, side: BookSide, price: Decimal) -> Option<Decimal> {
1227 match side {
1228 BookSide::Bid => self.bids.get(&price).map(|&q| price * q),
1229 BookSide::Ask => self.asks.get(&price).map(|&q| price * q),
1230 }
1231 }
1232
1233 pub fn price_impact_buy(&self, quantity: Decimal) -> Option<Decimal> {
1237 if quantity <= Decimal::ZERO {
1238 return None;
1239 }
1240 let mut remaining = quantity;
1241 let mut cost = Decimal::ZERO;
1242 for (&price, &qty) in &self.asks {
1243 if remaining.is_zero() { break; }
1244 let fill = remaining.min(qty);
1245 cost += fill * price;
1246 remaining -= fill;
1247 }
1248 if !remaining.is_zero() { return None; }
1249 Some(cost / quantity)
1250 }
1251
1252 pub fn price_impact_sell(&self, quantity: Decimal) -> Option<Decimal> {
1256 if quantity <= Decimal::ZERO {
1257 return None;
1258 }
1259 let mut remaining = quantity;
1260 let mut proceeds = Decimal::ZERO;
1261 for (&price, &qty) in self.bids.iter().rev() {
1262 if remaining.is_zero() { break; }
1263 let fill = remaining.min(qty);
1264 proceeds += fill * price;
1265 remaining -= fill;
1266 }
1267 if !remaining.is_zero() { return None; }
1268 Some(proceeds / quantity)
1269 }
1270
1271 pub fn ask_level_count(&self) -> usize {
1273 self.asks.len()
1274 }
1275
1276 pub fn bid_level_count(&self) -> usize {
1278 self.bids.len()
1279 }
1280
1281 pub fn ask_volume_within(&self, price_range: Decimal) -> Decimal {
1286 match self.best_ask() {
1287 None => Decimal::ZERO,
1288 Some(best) => {
1289 let ceiling = best.price + price_range;
1290 self.asks.range(..=ceiling).map(|(_, &q)| q).sum()
1291 }
1292 }
1293 }
1294
1295 pub fn bid_volume_within(&self, price_range: Decimal) -> Decimal {
1300 match self.best_bid() {
1301 None => Decimal::ZERO,
1302 Some(best) => {
1303 let floor = best.price - price_range;
1304 self.bids.range(floor..).map(|(_, &q)| q).sum()
1305 }
1306 }
1307 }
1308
1309 pub fn ask_volume_above(&self, price: Decimal) -> Decimal {
1311 use std::ops::Bound::Excluded;
1312 self.asks
1313 .range((Excluded(price), std::ops::Bound::Unbounded))
1314 .map(|(_, &q)| q)
1315 .sum()
1316 }
1317
1318 pub fn bid_volume_below(&self, price: Decimal) -> Decimal {
1320 use std::ops::Bound::Unbounded;
1321 use std::ops::Bound::Excluded;
1322 self.bids
1323 .range((Unbounded, Excluded(price)))
1324 .map(|(_, &q)| q)
1325 .sum()
1326 }
1327}
1328
1329#[cfg(test)]
1330mod tests {
1331 use super::*;
1332 use rust_decimal_macros::dec;
1333
1334 fn book(symbol: &str) -> OrderBook {
1335 OrderBook::new(symbol)
1336 }
1337
1338 fn delta(symbol: &str, side: BookSide, price: Decimal, qty: Decimal) -> BookDelta {
1339 BookDelta::new(symbol, side, price, qty)
1340 }
1341
1342 #[test]
1343 fn test_order_book_apply_bid_level() {
1344 let mut b = book("BTC-USD");
1345 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1346 .unwrap();
1347 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
1348 }
1349
1350 #[test]
1351 fn test_order_book_apply_ask_level() {
1352 let mut b = book("BTC-USD");
1353 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2)))
1354 .unwrap();
1355 assert_eq!(b.best_ask().unwrap().price, dec!(50100));
1356 }
1357
1358 #[test]
1359 fn test_order_book_remove_level_with_zero_qty() {
1360 let mut b = book("BTC-USD");
1361 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1362 .unwrap();
1363 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(0)))
1364 .unwrap();
1365 assert!(b.best_bid().is_none());
1366 }
1367
1368 #[test]
1369 fn test_order_book_best_bid_is_highest() {
1370 let mut b = book("BTC-USD");
1371 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)))
1372 .unwrap();
1373 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2)))
1374 .unwrap();
1375 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3)))
1376 .unwrap();
1377 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
1378 }
1379
1380 #[test]
1381 fn test_order_book_best_ask_is_lowest() {
1382 let mut b = book("BTC-USD");
1383 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(1)))
1384 .unwrap();
1385 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2)))
1386 .unwrap();
1387 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3)))
1388 .unwrap();
1389 assert_eq!(b.best_ask().unwrap().price, dec!(50100));
1390 }
1391
1392 #[test]
1393 fn test_order_book_mid_price() {
1394 let mut b = book("BTC-USD");
1395 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1396 .unwrap();
1397 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
1398 .unwrap();
1399 assert_eq!(b.mid_price().unwrap(), dec!(50050));
1400 }
1401
1402 #[test]
1403 fn test_order_book_spread() {
1404 let mut b = book("BTC-USD");
1405 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1406 .unwrap();
1407 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
1408 .unwrap();
1409 assert_eq!(b.spread().unwrap(), dec!(100));
1410 }
1411
1412 #[test]
1413 fn test_order_book_crossed_returns_error() {
1414 let mut b = book("BTC-USD");
1415 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50000), dec!(1)))
1416 .unwrap();
1417 let result = b.apply(delta("BTC-USD", BookSide::Bid, dec!(50001), dec!(1)));
1418 assert!(matches!(result, Err(StreamError::BookCrossed { .. })));
1419 }
1420
1421 #[test]
1422 fn test_order_book_wrong_symbol_delta_rejected() {
1423 let mut b = book("BTC-USD");
1424 let result = b.apply(delta("ETH-USD", BookSide::Bid, dec!(3000), dec!(1)));
1425 assert!(matches!(
1426 result,
1427 Err(StreamError::BookReconstructionFailed { .. })
1428 ));
1429 }
1430
1431 #[test]
1432 fn test_order_book_reset_clears_and_reloads() {
1433 let mut b = book("BTC-USD");
1434 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49000), dec!(5)))
1435 .unwrap();
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.bid_depth(), 1);
1442 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
1443 }
1444
1445 #[test]
1446 fn test_order_book_reset_ignores_zero_qty_levels() {
1447 let mut b = book("BTC-USD");
1448 b.reset(
1449 vec![
1450 PriceLevel::new(dec!(50000), dec!(1)),
1451 PriceLevel::new(dec!(49900), dec!(0)),
1452 ],
1453 vec![PriceLevel::new(dec!(50100), dec!(1))],
1454 )
1455 .unwrap();
1456 assert_eq!(b.bid_depth(), 1);
1457 }
1458
1459 #[test]
1460 fn test_order_book_reset_clears_sequence() {
1461 let mut b = book("BTC-USD");
1462 b.apply(
1463 delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)).with_sequence(5),
1464 )
1465 .unwrap();
1466 assert_eq!(b.last_sequence(), Some(5));
1467 b.reset(
1468 vec![PriceLevel::new(dec!(50000), dec!(1))],
1469 vec![PriceLevel::new(dec!(50100), dec!(1))],
1470 )
1471 .unwrap();
1472 assert_eq!(b.last_sequence(), None);
1473 }
1474
1475 #[test]
1476 fn test_order_book_depth_counts() {
1477 let mut b = book("BTC-USD");
1478 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)))
1479 .unwrap();
1480 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1)))
1481 .unwrap();
1482 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
1483 .unwrap();
1484 assert_eq!(b.bid_depth(), 2);
1485 assert_eq!(b.ask_depth(), 1);
1486 }
1487
1488 #[test]
1489 fn test_order_book_top_bids_descending() {
1490 let mut b = book("BTC-USD");
1491 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3)))
1492 .unwrap();
1493 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1494 .unwrap();
1495 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(2)))
1496 .unwrap();
1497 let top = b.top_bids(2);
1498 assert_eq!(top[0].price, dec!(50000));
1499 assert_eq!(top[1].price, dec!(49900));
1500 }
1501
1502 #[test]
1503 fn test_order_book_top_asks_ascending() {
1504 let mut b = book("BTC-USD");
1505 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3)))
1506 .unwrap();
1507 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
1508 .unwrap();
1509 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2)))
1510 .unwrap();
1511 let top = b.top_asks(2);
1512 assert_eq!(top[0].price, dec!(50100));
1513 assert_eq!(top[1].price, dec!(50200));
1514 }
1515
1516 #[test]
1517 fn test_book_delta_with_sequence() {
1518 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)).with_sequence(42);
1519 assert_eq!(d.sequence, Some(42));
1520 }
1521
1522 #[test]
1523 fn test_order_book_sequence_tracking() {
1524 let mut b = book("BTC-USD");
1525 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)).with_sequence(7))
1526 .unwrap();
1527 assert_eq!(b.last_sequence(), Some(7));
1528 }
1529
1530 #[test]
1531 fn test_order_book_sequence_gap_detected() {
1532 let mut b = book("BTC-USD");
1533 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)).with_sequence(1))
1534 .unwrap();
1535 let result =
1537 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)).with_sequence(3));
1538 assert!(matches!(
1539 result,
1540 Err(StreamError::SequenceGap { expected: 2, got: 3, .. })
1541 ));
1542 }
1543
1544 #[test]
1545 fn test_order_book_sequential_deltas_accepted() {
1546 let mut b = book("BTC-USD");
1547 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)).with_sequence(1))
1548 .unwrap();
1549 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)).with_sequence(2))
1550 .unwrap();
1551 assert_eq!(b.last_sequence(), Some(2));
1552 }
1553
1554 #[test]
1555 fn test_order_book_mid_price_empty_returns_none() {
1556 let b = book("BTC-USD");
1557 assert!(b.mid_price().is_none());
1558 }
1559
1560 #[test]
1561 fn test_price_level_new() {
1562 let lvl = PriceLevel::new(dec!(100), dec!(5));
1563 assert_eq!(lvl.price, dec!(100));
1564 assert_eq!(lvl.quantity, dec!(5));
1565 }
1566
1567 #[test]
1568 fn test_contains_bid_present() {
1569 let mut b = book("BTC-USD");
1570 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1571 .unwrap();
1572 assert!(b.contains_bid(dec!(50000)));
1573 assert!(!b.contains_bid(dec!(49999)));
1574 }
1575
1576 #[test]
1577 fn test_contains_ask_present() {
1578 let mut b = book("BTC-USD");
1579 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2)))
1580 .unwrap();
1581 assert!(b.contains_ask(dec!(50100)));
1582 assert!(!b.contains_ask(dec!(50200)));
1583 }
1584
1585 #[test]
1586 fn test_contains_bid_removed_after_zero_qty() {
1587 let mut b = book("BTC-USD");
1588 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
1589 .unwrap();
1590 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(0)))
1591 .unwrap();
1592 assert!(!b.contains_bid(dec!(50000)));
1593 }
1594
1595 #[test]
1596 fn test_book_delta_serde_roundtrip() {
1597 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))
1598 .with_sequence(42);
1599 let json = serde_json::to_string(&d).unwrap();
1600 let d2: BookDelta = serde_json::from_str(&json).unwrap();
1601 assert_eq!(d2.symbol, "BTC-USD");
1602 assert_eq!(d2.price, dec!(50000));
1603 assert_eq!(d2.sequence, Some(42));
1604 }
1605
1606 #[test]
1607 fn test_volume_at_bid_present() {
1608 let mut b = book("BTC-USD");
1609 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(3)))
1610 .unwrap();
1611 assert_eq!(b.volume_at_bid(dec!(50000)), Some(dec!(3)));
1612 assert_eq!(b.volume_at_bid(dec!(49999)), None);
1613 }
1614
1615 #[test]
1616 fn test_volume_at_ask_present() {
1617 let mut b = book("BTC-USD");
1618 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5)))
1619 .unwrap();
1620 assert_eq!(b.volume_at_ask(dec!(50100)), Some(dec!(5)));
1621 assert_eq!(b.volume_at_ask(dec!(50200)), None);
1622 }
1623
1624 #[test]
1625 fn test_book_delta_display_with_sequence() {
1626 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))
1627 .with_sequence(42);
1628 let s = d.to_string();
1629 assert!(s.contains("BTC-USD"));
1630 assert!(s.contains("Bid"));
1631 assert!(s.contains("seq=42"));
1632 }
1633
1634 #[test]
1635 fn test_book_delta_display_without_sequence() {
1636 let d = BookDelta::new("ETH-USD", BookSide::Ask, dec!(3000), dec!(2));
1637 let s = d.to_string();
1638 assert!(s.contains("ETH-USD"));
1639 assert!(s.contains("Ask"));
1640 assert!(!s.contains("seq="));
1641 }
1642
1643 #[test]
1644 fn test_book_delta_is_delete_zero_qty() {
1645 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(0));
1646 assert!(d.is_delete());
1647 }
1648
1649 #[test]
1650 fn test_book_delta_is_delete_nonzero_qty() {
1651 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1));
1652 assert!(!d.is_delete());
1653 }
1654
1655 #[test]
1656 fn test_snapshot_bids_descending_asks_ascending() {
1657 let mut b = book("BTC-USD");
1658 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1)))
1659 .unwrap();
1660 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2)))
1661 .unwrap();
1662 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(1)))
1663 .unwrap();
1664 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(3)))
1665 .unwrap();
1666 let (bids, asks) = b.snapshot();
1667 assert_eq!(bids[0].price, dec!(50000));
1668 assert_eq!(bids[1].price, dec!(49800));
1669 assert_eq!(asks[0].price, dec!(50100));
1670 assert_eq!(asks[1].price, dec!(50200));
1671 }
1672
1673 #[test]
1676 fn test_bid_ask_imbalance_balanced() {
1677 let mut b = book("BTC-USD");
1678 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
1679 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
1680 let imb = b.bid_ask_imbalance(1).unwrap();
1681 assert!((imb).abs() < 1e-9, "equal qty → ~0");
1682 }
1683
1684 #[test]
1685 fn test_bid_ask_imbalance_full_bid_pressure() {
1686 let mut b = book("BTC-USD");
1687 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(10))).unwrap();
1688 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(0))).unwrap();
1689 assert!(b.bid_ask_imbalance(1).is_none());
1691 }
1692
1693 #[test]
1694 fn test_bid_ask_imbalance_two_levels() {
1695 let mut b = book("BTC-USD");
1696 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(3))).unwrap();
1697 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1))).unwrap();
1698 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
1699 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2))).unwrap();
1700 let imb = b.bid_ask_imbalance(2).unwrap();
1702 assert!((imb).abs() < 1e-9);
1703 }
1704
1705 #[test]
1708 fn test_vwap_single_level_equals_price() {
1709 let mut b = book("BTC-USD");
1710 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5))).unwrap();
1711 assert_eq!(b.vwap(BookSide::Ask, 1), Some(dec!(50100)));
1712 }
1713
1714 #[test]
1715 fn test_vwap_two_equal_qty_levels() {
1716 let mut b = book("BTC-USD");
1717 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
1718 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1))).unwrap();
1719 assert_eq!(b.vwap(BookSide::Bid, 2), Some(dec!(49900)));
1721 }
1722
1723 #[test]
1724 fn test_vwap_empty_side_returns_none() {
1725 let b = book("BTC-USD");
1726 assert!(b.vwap(BookSide::Ask, 5).is_none());
1727 }
1728
1729 #[test]
1732 fn test_depth_at_price_bid_present() {
1733 let mut b = book("BTC-USD");
1734 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(3))).unwrap();
1735 assert_eq!(b.depth_at_price(dec!(50000), BookSide::Bid), Some(dec!(3)));
1736 }
1737
1738 #[test]
1739 fn test_depth_at_price_ask_present() {
1740 let mut b = book("BTC-USD");
1741 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
1742 assert_eq!(b.depth_at_price(dec!(50100), BookSide::Ask), Some(dec!(2)));
1743 }
1744
1745 #[test]
1746 fn test_depth_at_price_absent_returns_none() {
1747 let b = book("BTC-USD");
1748 assert!(b.depth_at_price(dec!(99999), BookSide::Bid).is_none());
1749 assert!(b.depth_at_price(dec!(99999), BookSide::Ask).is_none());
1750 }
1751
1752 #[test]
1753 fn test_bid_ask_ratio_equal_sides() {
1754 let mut b = book("BTC-USD");
1755 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(5))).unwrap();
1756 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5))).unwrap();
1757 let ratio = b.bid_ask_ratio().unwrap();
1758 assert!((ratio - 1.0).abs() < 1e-9);
1759 }
1760
1761 #[test]
1762 fn test_bid_ask_ratio_more_bids() {
1763 let mut b = book("BTC-USD");
1764 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(10))).unwrap();
1765 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5))).unwrap();
1766 let ratio = b.bid_ask_ratio().unwrap();
1767 assert!((ratio - 2.0).abs() < 1e-9);
1768 }
1769
1770 #[test]
1771 fn test_bid_ask_ratio_no_asks_returns_none() {
1772 let mut b = book("BTC-USD");
1773 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(5))).unwrap();
1774 assert!(b.bid_ask_ratio().is_none());
1775 }
1776
1777 #[test]
1780 fn test_all_bids_sorted_descending() {
1781 let mut b = book("BTC-USD");
1782 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1))).unwrap();
1783 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2))).unwrap();
1784 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(3))).unwrap();
1785 let bids = b.all_bids();
1786 assert_eq!(bids.len(), 3);
1787 assert_eq!(bids[0].price, dec!(50000));
1788 assert_eq!(bids[1].price, dec!(49900));
1789 assert_eq!(bids[2].price, dec!(49800));
1790 }
1791
1792 #[test]
1793 fn test_all_asks_sorted_ascending() {
1794 let mut b = book("BTC-USD");
1795 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
1796 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(2))).unwrap();
1797 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(3))).unwrap();
1798 let asks = b.all_asks();
1799 assert_eq!(asks.len(), 3);
1800 assert_eq!(asks[0].price, dec!(50100));
1801 assert_eq!(asks[1].price, dec!(50200));
1802 assert_eq!(asks[2].price, dec!(50300));
1803 }
1804
1805 #[test]
1806 fn test_all_bids_empty_returns_empty() {
1807 let b = book("BTC-USD");
1808 assert!(b.all_bids().is_empty());
1809 }
1810
1811 #[test]
1814 fn test_spread_pct_basic() {
1815 let mut b = book("X");
1817 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1818 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
1819 let pct = b.spread_pct().unwrap();
1820 assert!((pct - 100.0 / 100.5).abs() < 1e-9, "got {pct}");
1821 }
1822
1823 #[test]
1824 fn test_spread_pct_empty_book_returns_none() {
1825 let b = book("X");
1826 assert!(b.spread_pct().is_none());
1827 }
1828
1829 #[test]
1830 fn test_total_depth_counts_both_sides() {
1831 let mut b = book("X");
1832 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
1833 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
1834 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
1835 assert_eq!(b.total_depth(), 3);
1836 }
1837
1838 #[test]
1839 fn test_total_depth_empty_is_zero() {
1840 let b = book("X");
1841 assert_eq!(b.total_depth(), 0);
1842 }
1843
1844 #[test]
1845 fn test_total_volume_sums_both_sides() {
1846 let mut b = book("X");
1847 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(3))).unwrap();
1848 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
1849 assert_eq!(b.total_volume(), dec!(8));
1850 }
1851
1852 #[test]
1853 fn test_total_volume_empty_is_zero() {
1854 let b = book("X");
1855 assert_eq!(b.total_volume(), dec!(0));
1856 }
1857
1858 #[test]
1861 fn test_level_count_empty() {
1862 let b = book("BTC-USD");
1863 assert_eq!(b.level_count(BookSide::Bid), 0);
1864 assert_eq!(b.level_count(BookSide::Ask), 0);
1865 }
1866
1867 #[test]
1868 fn test_level_count_matches_depth_methods() {
1869 let mut b = book("BTC-USD");
1870 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1871 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
1872 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
1873 assert_eq!(b.level_count(BookSide::Bid), b.bid_depth());
1874 assert_eq!(b.level_count(BookSide::Ask), b.ask_depth());
1875 }
1876
1877 #[test]
1880 fn test_price_level_notional() {
1881 let level = PriceLevel::new(dec!(50000), dec!(2));
1882 assert_eq!(level.notional(), dec!(100000));
1883 }
1884
1885 #[test]
1886 fn test_price_level_notional_zero_qty() {
1887 let level = PriceLevel::new(dec!(100), dec!(0));
1888 assert_eq!(level.notional(), dec!(0));
1889 }
1890
1891 #[test]
1894 fn test_weighted_mid_price_equal_qtys_is_arithmetic_mid() {
1895 let mut b = book("X");
1897 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1898 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(1))).unwrap();
1899 assert_eq!(b.weighted_mid_price().unwrap(), dec!(101));
1900 }
1901
1902 #[test]
1903 fn test_weighted_mid_price_skews_toward_larger_qty() {
1904 let mut b = book("X");
1906 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1907 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
1908 assert_eq!(b.weighted_mid_price().unwrap(), dec!(100.5));
1909 }
1910
1911 #[test]
1912 fn test_weighted_mid_price_empty_returns_none() {
1913 let b = book("X");
1914 assert!(b.weighted_mid_price().is_none());
1915 }
1916
1917 #[test]
1920 fn test_is_empty_new_book() {
1921 let b = book("BTC-USD");
1922 assert!(b.is_empty());
1923 }
1924
1925 #[test]
1926 fn test_is_empty_false_with_bid() {
1927 let mut b = book("BTC-USD");
1928 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1929 assert!(!b.is_empty());
1930 }
1931
1932 #[test]
1933 fn test_is_empty_false_with_ask() {
1934 let mut b = book("BTC-USD");
1935 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
1936 assert!(!b.is_empty());
1937 }
1938
1939 #[test]
1940 fn test_is_empty_true_after_removing_all_levels() {
1941 let mut b = book("BTC-USD");
1942 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1943 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(0))).unwrap(); assert!(b.is_empty());
1945 }
1946
1947 #[test]
1950 fn test_clear_empty_book_is_noop() {
1951 let mut b = book("BTC-USD");
1952 b.clear();
1953 assert!(b.is_empty());
1954 }
1955
1956 #[test]
1957 fn test_clear_removes_all_levels() {
1958 let mut b = book("BTC-USD");
1959 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1960 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
1961 b.clear();
1962 assert!(b.is_empty());
1963 }
1964
1965 #[test]
1966 fn test_clear_allows_fresh_apply_after() {
1967 let mut b = book("BTC-USD");
1968 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
1969 b.clear();
1970 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(5))).unwrap();
1972 assert_eq!(b.bid_depth(), 1);
1973 }
1974
1975 #[test]
1978 fn test_total_notional_bid_side() {
1979 let mut b = book("BTC-USD");
1980 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2))).unwrap();
1981 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(3))).unwrap();
1982 assert_eq!(b.total_notional(BookSide::Bid), dec!(249700));
1984 }
1985
1986 #[test]
1987 fn test_total_notional_ask_side() {
1988 let mut b = book("BTC-USD");
1989 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
1990 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2))).unwrap();
1991 assert_eq!(b.total_notional(BookSide::Ask), dec!(150500));
1993 }
1994
1995 #[test]
1996 fn test_total_notional_empty_side_is_zero() {
1997 let b = book("BTC-USD");
1998 assert_eq!(b.total_notional(BookSide::Bid), dec!(0));
1999 assert_eq!(b.total_notional(BookSide::Ask), dec!(0));
2000 }
2001
2002 #[test]
2003 fn test_cumulative_bid_volume_top_two() {
2004 let mut b = book("BTC-USD");
2005 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(5))).unwrap();
2006 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(3))).unwrap();
2007 b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(2))).unwrap();
2008 assert_eq!(b.cumulative_bid_volume(2), dec!(8));
2010 }
2011
2012 #[test]
2013 fn test_cumulative_ask_volume_top_two() {
2014 let mut b = book("BTC-USD");
2015 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(4))).unwrap();
2016 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(6))).unwrap();
2017 b.apply(delta("BTC-USD", BookSide::Ask, dec!(103), dec!(1))).unwrap();
2018 assert_eq!(b.cumulative_ask_volume(2), dec!(10));
2020 }
2021
2022 #[test]
2023 fn test_cumulative_volume_empty_returns_zero() {
2024 let b = book("BTC-USD");
2025 assert_eq!(b.cumulative_bid_volume(5), dec!(0));
2026 assert_eq!(b.cumulative_ask_volume(5), dec!(0));
2027 }
2028
2029 #[test]
2030 fn test_top_n_bids_best_first() {
2031 let mut b = book("BTC-USD");
2032 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2033 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(2))).unwrap();
2034 b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(3))).unwrap();
2035 let top2 = b.top_n_bids(2);
2036 assert_eq!(top2.len(), 2);
2037 assert_eq!(top2[0].price, dec!(100)); assert_eq!(top2[1].price, dec!(99));
2039 }
2040
2041 #[test]
2042 fn test_top_n_asks_best_first() {
2043 let mut b = book("BTC-USD");
2044 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2045 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(2))).unwrap();
2046 b.apply(delta("BTC-USD", BookSide::Ask, dec!(103), dec!(3))).unwrap();
2047 let top2 = b.top_n_asks(2);
2048 assert_eq!(top2.len(), 2);
2049 assert_eq!(top2[0].price, dec!(101)); assert_eq!(top2[1].price, dec!(102));
2051 }
2052
2053 #[test]
2054 fn test_depth_ratio_balanced_book() {
2055 let mut b = book("BTC-USD");
2056 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(5))).unwrap();
2057 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(5))).unwrap();
2058 let ratio = b.depth_ratio(1).unwrap();
2059 assert!((ratio - 1.0).abs() < 1e-9);
2060 }
2061
2062 #[test]
2063 fn test_depth_ratio_empty_asks_returns_none() {
2064 let mut b = book("BTC-USD");
2065 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(5))).unwrap();
2066 assert!(b.depth_ratio(1).is_none());
2067 }
2068
2069 #[test]
2072 fn test_is_one_sided_bids_only() {
2073 let mut b = book("BTC-USD");
2074 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2075 assert!(b.is_one_sided());
2076 }
2077
2078 #[test]
2079 fn test_is_one_sided_asks_only() {
2080 let mut b = book("BTC-USD");
2081 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2082 assert!(b.is_one_sided());
2083 }
2084
2085 #[test]
2086 fn test_is_one_sided_false_with_both_sides() {
2087 let mut b = book("BTC-USD");
2088 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2089 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2090 assert!(!b.is_one_sided());
2091 }
2092
2093 #[test]
2094 fn test_is_one_sided_false_for_empty_book() {
2095 let b = book("BTC-USD");
2096 assert!(!b.is_one_sided());
2097 }
2098
2099 #[test]
2102 fn test_bid_ask_spread_bps_known_value() {
2103 let mut b = book("BTC-USD");
2104 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2105 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2106 let bps = b.bid_ask_spread_bps().unwrap();
2108 assert!((bps - 1.0 / 100.5 * 10_000.0).abs() < 0.01);
2109 }
2110
2111 #[test]
2112 fn test_bid_ask_spread_bps_none_when_one_sided() {
2113 let mut b = book("BTC-USD");
2114 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2115 assert!(b.bid_ask_spread_bps().is_none());
2116 }
2117
2118 #[test]
2119 fn test_bid_ask_spread_bps_none_for_empty_book() {
2120 let b = book("BTC-USD");
2121 assert!(b.bid_ask_spread_bps().is_none());
2122 }
2123
2124 #[test]
2127 fn test_ask_wall_returns_cheapest_ask_above_threshold() {
2128 let mut b = book("BTC-USD");
2129 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
2130 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(10))).unwrap();
2131 let wall = b.ask_wall(dec!(5)).unwrap();
2133 assert_eq!(wall.price, dec!(50200));
2134 assert_eq!(wall.quantity, dec!(10));
2135 }
2136
2137 #[test]
2138 fn test_ask_wall_none_when_no_level_meets_threshold() {
2139 let mut b = book("BTC-USD");
2140 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
2141 assert!(b.ask_wall(dec!(5)).is_none());
2142 }
2143
2144 #[test]
2145 fn test_bid_wall_returns_highest_bid_above_threshold() {
2146 let mut b = book("BTC-USD");
2147 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(10))).unwrap();
2148 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(2))).unwrap();
2149 let wall = b.bid_wall(dec!(5)).unwrap();
2151 assert_eq!(wall.price, dec!(49900));
2152 assert_eq!(wall.quantity, dec!(10));
2153 }
2154
2155 #[test]
2156 fn test_bid_wall_none_when_no_level_meets_threshold() {
2157 let mut b = book("BTC-USD");
2158 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1))).unwrap();
2159 assert!(b.bid_wall(dec!(5)).is_none());
2160 }
2161
2162 #[test]
2165 fn test_level_count_imbalance_balanced_sides() {
2166 let mut b = book("BTC-USD");
2167 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2168 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2169 assert_eq!(b.level_count_imbalance(), Some(0.0));
2171 }
2172
2173 #[test]
2174 fn test_level_count_imbalance_bids_only() {
2175 let mut b = book("BTC-USD");
2176 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2177 b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(1))).unwrap();
2178 assert_eq!(b.level_count_imbalance(), Some(1.0));
2180 }
2181
2182 #[test]
2183 fn test_level_count_imbalance_none_for_empty_book() {
2184 let b = book("BTC-USD");
2185 assert!(b.level_count_imbalance().is_none());
2186 }
2187
2188 #[test]
2191 fn test_total_bid_volume_sums_all_levels() {
2192 let mut b = book("BTC-USD");
2193 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(3))).unwrap();
2194 b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(2))).unwrap();
2195 assert_eq!(b.total_bid_volume(), dec!(5));
2196 }
2197
2198 #[test]
2199 fn test_total_ask_volume_sums_all_levels() {
2200 let mut b = book("BTC-USD");
2201 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(4))).unwrap();
2202 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(1))).unwrap();
2203 assert_eq!(b.total_ask_volume(), dec!(5));
2204 }
2205
2206 #[test]
2207 fn test_total_bid_volume_zero_for_empty_side() {
2208 let b = book("BTC-USD");
2209 assert_eq!(b.total_bid_volume(), dec!(0));
2210 }
2211
2212 #[test]
2215 fn test_bid_levels_above_counts_strictly_above() {
2216 let mut b = book("BTC-USD");
2217 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2218 b.apply(delta("BTC-USD", BookSide::Bid, dec!(101), dec!(1))).unwrap();
2219 b.apply(delta("BTC-USD", BookSide::Bid, dec!(102), dec!(1))).unwrap();
2220 assert_eq!(b.bid_levels_above(dec!(100)), 2);
2222 }
2223
2224 #[test]
2225 fn test_bid_levels_above_zero_when_none_above() {
2226 let mut b = book("BTC-USD");
2227 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2228 assert_eq!(b.bid_levels_above(dec!(100)), 0);
2229 }
2230
2231 #[test]
2232 fn test_ask_levels_below_counts_strictly_below() {
2233 let mut b = book("BTC-USD");
2234 b.apply(delta("BTC-USD", BookSide::Ask, dec!(100), dec!(1))).unwrap();
2235 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2236 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(1))).unwrap();
2237 assert_eq!(b.ask_levels_below(dec!(102)), 2);
2239 }
2240
2241 #[test]
2242 fn test_ask_levels_below_zero_for_empty_book() {
2243 let b = book("BTC-USD");
2244 assert_eq!(b.ask_levels_below(dec!(100)), 0);
2245 }
2246
2247 #[test]
2250 fn test_bid_ask_volume_ratio_returns_correct_ratio() {
2251 let mut b = book("BTC-USD");
2252 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(3))).unwrap();
2253 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2254 let ratio = b.bid_ask_volume_ratio().unwrap();
2256 assert!((ratio - 3.0).abs() < 1e-10);
2257 }
2258
2259 #[test]
2260 fn test_bid_ask_volume_ratio_none_when_ask_empty() {
2261 let mut b = book("BTC-USD");
2262 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2263 assert!(b.bid_ask_volume_ratio().is_none());
2264 }
2265
2266 #[test]
2267 fn test_bid_ask_volume_ratio_none_when_bid_empty() {
2268 let mut b = book("BTC-USD");
2269 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2270 assert!(b.bid_ask_volume_ratio().is_none());
2271 }
2272
2273 #[test]
2274 fn test_top_n_bid_volume_sums_top_levels() {
2275 let mut b = book("BTC-USD");
2276 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap(); b.apply(delta("BTC-USD", BookSide::Bid, dec!(101), dec!(2))).unwrap();
2278 b.apply(delta("BTC-USD", BookSide::Bid, dec!(102), dec!(3))).unwrap(); assert_eq!(b.top_n_bid_volume(2), dec!(5));
2281 }
2282
2283 #[test]
2284 fn test_top_n_bid_volume_all_when_n_exceeds_levels() {
2285 let mut b = book("BTC-USD");
2286 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2287 b.apply(delta("BTC-USD", BookSide::Bid, dec!(101), dec!(2))).unwrap();
2288 assert_eq!(b.top_n_bid_volume(5), dec!(3));
2290 }
2291
2292 #[test]
2293 fn test_top_n_bid_volume_zero_for_empty_book() {
2294 let b = book("BTC-USD");
2295 assert_eq!(b.top_n_bid_volume(3), dec!(0));
2296 }
2297
2298 #[test]
2301 fn test_imbalance_ratio_positive_when_more_bids() {
2302 let mut b = book("BTC-USD");
2303 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(3))).unwrap();
2304 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2305 let ratio = b.imbalance_ratio().unwrap();
2307 assert!((ratio - 0.5).abs() < 1e-10);
2308 }
2309
2310 #[test]
2311 fn test_imbalance_ratio_negative_when_more_asks() {
2312 let mut b = book("BTC-USD");
2313 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2314 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(3))).unwrap();
2315 let ratio = b.imbalance_ratio().unwrap();
2317 assert!((ratio - (-0.5)).abs() < 1e-10);
2318 }
2319
2320 #[test]
2321 fn test_imbalance_ratio_none_when_both_empty() {
2322 let b = book("BTC-USD");
2323 assert!(b.imbalance_ratio().is_none());
2324 }
2325
2326 #[test]
2327 fn test_top_n_ask_volume_sums_lowest_asks() {
2328 let mut b = book("BTC-USD");
2329 b.apply(delta("BTC-USD", BookSide::Ask, dec!(100), dec!(2))).unwrap(); b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(3))).unwrap();
2331 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(5))).unwrap(); assert_eq!(b.top_n_ask_volume(2), dec!(5));
2334 }
2335
2336 #[test]
2337 fn test_top_n_ask_volume_zero_for_empty_book() {
2338 let b = book("BTC-USD");
2339 assert_eq!(b.top_n_ask_volume(3), dec!(0));
2340 }
2341
2342 #[test]
2345 fn test_has_ask_at_true_when_ask_exists() {
2346 let mut b = book("BTC-USD");
2347 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
2348 assert!(b.has_ask_at(dec!(101)));
2349 }
2350
2351 #[test]
2352 fn test_has_ask_at_false_when_no_ask_at_price() {
2353 let mut b = book("BTC-USD");
2354 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(1))).unwrap();
2355 assert!(!b.has_ask_at(dec!(101)));
2356 }
2357
2358 #[test]
2359 fn test_bid_ask_depth_correct_counts() {
2360 let mut b = book("BTC-USD");
2361 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2362 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2363 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2364 assert_eq!(b.bid_ask_depth(), (2, 1));
2365 }
2366
2367 #[test]
2368 fn test_bid_ask_depth_zero_for_empty_book() {
2369 let b = book("BTC-USD");
2370 assert_eq!(b.bid_ask_depth(), (0, 0));
2371 }
2372
2373 #[test]
2375 fn test_best_bid_qty_returns_top_bid_quantity() {
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::Bid, dec!(99), dec!(5))).unwrap();
2379 assert_eq!(b.best_bid_qty(), Some(dec!(3)));
2381 }
2382
2383 #[test]
2384 fn test_best_ask_qty_returns_top_ask_quantity() {
2385 let mut b = book("BTC-USD");
2386 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(7))).unwrap();
2387 b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(2))).unwrap();
2388 assert_eq!(b.best_ask_qty(), Some(dec!(7)));
2390 }
2391
2392 #[test]
2393 fn test_best_bid_qty_none_when_no_bids() {
2394 let b = book("BTC-USD");
2395 assert!(b.best_bid_qty().is_none());
2396 }
2397
2398 #[test]
2399 fn test_best_ask_qty_none_when_no_asks() {
2400 let b = book("BTC-USD");
2401 assert!(b.best_ask_qty().is_none());
2402 }
2403
2404 #[test]
2406 fn test_total_book_volume_sum_of_bids_and_asks() {
2407 let mut b = book("BTC-USD");
2408 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(3))).unwrap();
2409 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
2410 assert_eq!(b.total_book_volume(), dec!(5));
2411 }
2412
2413 #[test]
2414 fn test_total_book_volume_zero_on_empty_book() {
2415 let b = book("BTC-USD");
2416 assert_eq!(b.total_book_volume(), dec!(0));
2417 }
2418
2419 #[test]
2421 fn test_price_range_bids_correct_range() {
2422 let mut b = book("BTC-USD");
2423 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2424 b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2425 b.apply(delta("BTC-USD", BookSide::Bid, dec!(97), dec!(1))).unwrap();
2426 assert_eq!(b.price_range_bids(), Some(dec!(3)));
2428 }
2429
2430 #[test]
2431 fn test_price_range_bids_none_with_single_bid() {
2432 let mut b = book("BTC-USD");
2433 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2434 assert!(b.price_range_bids().is_none());
2435 }
2436
2437 #[test]
2440 fn test_is_tight_spread_true_when_spread_at_threshold() {
2441 let mut b = book("BTC-USD");
2442 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2443 b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2444 assert!(b.is_tight_spread(dec!(1)));
2446 }
2447
2448 #[test]
2449 fn test_is_tight_spread_false_when_spread_above_threshold() {
2450 let mut b = book("BTC-USD");
2451 b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2452 b.apply(delta("BTC-USD", BookSide::Ask, dec!(103), dec!(1))).unwrap();
2453 assert!(!b.is_tight_spread(dec!(1)));
2455 }
2456
2457 #[test]
2458 fn test_is_tight_spread_false_when_empty() {
2459 let b = book("BTC-USD");
2460 assert!(!b.is_tight_spread(dec!(10)));
2461 }
2462
2463 #[test]
2466 fn test_best_bid_price_returns_price() {
2467 let mut b = book("X");
2468 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(5))).unwrap();
2469 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(3))).unwrap();
2470 assert_eq!(b.best_bid_price(), Some(dec!(99)));
2471 }
2472
2473 #[test]
2474 fn test_best_ask_price_returns_price() {
2475 let mut b = book("X");
2476 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
2477 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
2478 assert_eq!(b.best_ask_price(), Some(dec!(101)));
2479 }
2480
2481 #[test]
2482 fn test_best_bid_price_none_when_empty() {
2483 assert_eq!(book("X").best_bid_price(), None);
2484 }
2485
2486 #[test]
2487 fn test_best_ask_price_none_when_empty() {
2488 assert_eq!(book("X").best_ask_price(), None);
2489 }
2490
2491 #[test]
2494 fn test_is_crossed_false_when_normal() {
2495 let mut b = book("X");
2496 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2497 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2498 assert!(!b.is_crossed());
2499 }
2500
2501 #[test]
2502 fn test_is_crossed_false_when_empty() {
2503 assert!(!book("X").is_crossed());
2504 }
2505
2506 #[test]
2509 fn test_has_bids_true_when_bid_present() {
2510 let mut b = book("X");
2511 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2512 assert!(b.has_bids());
2513 }
2514
2515 #[test]
2516 fn test_has_bids_false_when_empty() {
2517 assert!(!book("X").has_bids());
2518 }
2519
2520 #[test]
2521 fn test_has_asks_true_when_ask_present() {
2522 let mut b = book("X");
2523 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2524 assert!(b.has_asks());
2525 }
2526
2527 #[test]
2528 fn test_has_asks_false_when_empty() {
2529 assert!(!book("X").has_asks());
2530 }
2531
2532 #[test]
2535 fn test_ask_price_range_none_when_empty() {
2536 assert_eq!(book("X").ask_price_range(), None);
2537 }
2538
2539 #[test]
2540 fn test_ask_price_range_zero_when_single_level() {
2541 let mut b = book("X");
2542 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(1))).unwrap();
2543 assert_eq!(b.ask_price_range(), Some(dec!(0)));
2544 }
2545
2546 #[test]
2547 fn test_ask_price_range_correct_with_multiple_levels() {
2548 let mut b = book("X");
2549 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(1))).unwrap();
2550 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(1))).unwrap();
2551 b.apply(delta("X", BookSide::Ask, dec!(105), dec!(1))).unwrap();
2552 assert_eq!(b.ask_price_range(), Some(dec!(5)));
2553 }
2554
2555 #[test]
2556 fn test_bid_price_range_none_when_empty() {
2557 assert_eq!(book("X").bid_price_range(), None);
2558 }
2559
2560 #[test]
2561 fn test_bid_price_range_correct_with_multiple_levels() {
2562 let mut b = book("X");
2563 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
2564 b.apply(delta("X", BookSide::Bid, dec!(96), dec!(1))).unwrap();
2565 b.apply(delta("X", BookSide::Bid, dec!(94), dec!(1))).unwrap();
2566 assert_eq!(b.bid_price_range(), Some(dec!(4)));
2568 }
2569
2570 #[test]
2573 fn test_mid_spread_ratio_none_when_empty() {
2574 assert_eq!(book("X").mid_spread_ratio(), None);
2575 }
2576
2577 #[test]
2578 fn test_mid_spread_ratio_correct() {
2579 let mut b = book("X");
2580 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2581 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2582 let ratio = b.mid_spread_ratio().unwrap();
2584 assert!((ratio - 0.02).abs() < 1e-9);
2585 }
2586
2587 #[test]
2590 fn test_volume_imbalance_none_when_empty() {
2591 assert_eq!(book("X").volume_imbalance(), None);
2592 }
2593
2594 #[test]
2595 fn test_volume_imbalance_positive_when_more_bids() {
2596 let mut b = book("X");
2597 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(3))).unwrap();
2598 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2599 let imb = b.volume_imbalance().unwrap();
2601 assert!((imb - 0.5).abs() < 1e-9);
2602 }
2603
2604 #[test]
2605 fn test_volume_imbalance_negative_when_more_asks() {
2606 let mut b = book("X");
2607 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2608 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(3))).unwrap();
2609 let imb = b.volume_imbalance().unwrap();
2611 assert!((imb - (-0.5)).abs() < 1e-9);
2612 }
2613
2614 #[test]
2615 fn test_volume_imbalance_zero_when_equal() {
2616 let mut b = book("X");
2617 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(2))).unwrap();
2618 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(2))).unwrap();
2619 let imb = b.volume_imbalance().unwrap();
2620 assert!(imb.abs() < 1e-9);
2621 }
2622
2623 #[test]
2626 fn test_ask_volume_within_sums_levels_in_range() {
2627 let mut b = book("X");
2628 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(2))).unwrap();
2629 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(3))).unwrap();
2630 b.apply(delta("X", BookSide::Ask, dec!(105), dec!(10))).unwrap();
2631 let vol = b.ask_volume_within(dec!(2));
2633 assert_eq!(vol, dec!(5)); }
2635
2636 #[test]
2637 fn test_ask_volume_within_zero_when_empty() {
2638 assert_eq!(book("X").ask_volume_within(dec!(10)), dec!(0));
2639 }
2640
2641 #[test]
2642 fn test_bid_volume_within_sums_levels_in_range() {
2643 let mut b = book("X");
2644 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(5))).unwrap();
2645 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(3))).unwrap();
2646 b.apply(delta("X", BookSide::Bid, dec!(95), dec!(10))).unwrap();
2647 let vol = b.bid_volume_within(dec!(2));
2649 assert_eq!(vol, dec!(8)); }
2651
2652 #[test]
2653 fn test_bid_volume_within_zero_when_empty() {
2654 assert_eq!(book("X").bid_volume_within(dec!(10)), dec!(0));
2655 }
2656
2657 #[test]
2660 fn test_ask_level_count_zero_when_empty() {
2661 assert_eq!(book("X").ask_level_count(), 0);
2662 }
2663
2664 #[test]
2665 fn test_ask_level_count_correct() {
2666 let mut b = book("X");
2667 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(1))).unwrap();
2668 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(2))).unwrap();
2669 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
2670 assert_eq!(b.ask_level_count(), 3);
2671 }
2672
2673 #[test]
2674 fn test_bid_level_count_correct() {
2675 let mut b = book("X");
2676 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2677 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(2))).unwrap();
2678 assert_eq!(b.bid_level_count(), 2);
2679 }
2680
2681 #[test]
2684 fn test_price_impact_buy_correct_single_level() {
2685 let mut b = book("X");
2686 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(10))).unwrap();
2687 assert_eq!(b.price_impact_buy(dec!(5)), Some(dec!(100)));
2689 }
2690
2691 #[test]
2692 fn test_price_impact_buy_spans_levels() {
2693 let mut b = book("X");
2694 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(5))).unwrap();
2695 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
2696 assert_eq!(b.price_impact_buy(dec!(10)), Some(dec!(100.5)));
2698 }
2699
2700 #[test]
2701 fn test_price_impact_buy_none_when_insufficient_liquidity() {
2702 let mut b = book("X");
2703 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(3))).unwrap();
2704 assert!(b.price_impact_buy(dec!(5)).is_none());
2705 }
2706
2707 #[test]
2708 fn test_price_impact_sell_correct_single_level() {
2709 let mut b = book("X");
2710 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(10))).unwrap();
2711 assert_eq!(b.price_impact_sell(dec!(5)), Some(dec!(99)));
2712 }
2713
2714 #[test]
2717 fn test_total_value_at_level_bid_returns_price_times_qty() {
2718 let mut b = book("X");
2719 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(5))).unwrap();
2720 assert_eq!(b.total_value_at_level(BookSide::Bid, dec!(100)), Some(dec!(500)));
2721 }
2722
2723 #[test]
2724 fn test_total_value_at_level_ask_returns_price_times_qty() {
2725 let mut b = book("X");
2726 b.apply(delta("X", BookSide::Ask, dec!(105), dec!(3))).unwrap();
2727 assert_eq!(b.total_value_at_level(BookSide::Ask, dec!(105)), Some(dec!(315)));
2728 }
2729
2730 #[test]
2731 fn test_total_value_at_level_none_when_price_missing() {
2732 let b = book("X");
2733 assert!(b.total_value_at_level(BookSide::Bid, dec!(100)).is_none());
2734 }
2735
2736 #[test]
2739 fn test_ask_volume_above_sums_asks_above_price() {
2740 let mut b = book("X");
2741 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
2742 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
2743 b.apply(delta("X", BookSide::Ask, dec!(103), dec!(2))).unwrap();
2744 assert_eq!(b.ask_volume_above(dec!(101)), dec!(5));
2746 }
2747
2748 #[test]
2749 fn test_ask_volume_above_zero_when_no_asks_above() {
2750 let mut b = book("X");
2751 b.apply(delta("X", BookSide::Ask, dec!(100), dec!(10))).unwrap();
2752 assert_eq!(b.ask_volume_above(dec!(100)), dec!(0));
2753 }
2754
2755 #[test]
2756 fn test_bid_volume_below_sums_bids_below_price() {
2757 let mut b = book("X");
2758 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(4))).unwrap();
2759 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(6))).unwrap();
2760 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(2))).unwrap();
2761 assert_eq!(b.bid_volume_below(dec!(100)), dec!(10));
2763 }
2764
2765 #[test]
2766 fn test_bid_volume_below_zero_when_no_bids_below() {
2767 let mut b = book("X");
2768 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(5))).unwrap();
2769 assert_eq!(b.bid_volume_below(dec!(100)), dec!(0));
2770 }
2771
2772 #[test]
2775 fn test_total_notional_both_sides_sums_both() {
2776 let mut b = book("X");
2777 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));
2780 }
2781
2782 #[test]
2783 fn test_total_notional_both_sides_zero_when_empty() {
2784 let b = book("X");
2785 assert_eq!(b.total_notional_both_sides(), dec!(0));
2786 }
2787
2788 #[test]
2791 fn test_level_count_both_sides_zero_when_empty() {
2792 let b = book("X");
2793 assert_eq!(b.level_count_both_sides(), 0);
2794 }
2795
2796 #[test]
2797 fn test_level_count_both_sides_counts_all_levels() {
2798 let mut b = book("X");
2799 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2800 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
2801 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2802 assert_eq!(b.level_count_both_sides(), 3);
2803 }
2804
2805 #[test]
2808 fn test_ask_price_at_rank_best_ask_at_zero() {
2809 let mut b = book("X");
2810 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
2811 b.apply(delta("X", BookSide::Ask, dec!(102), dec!(1))).unwrap();
2812 assert_eq!(b.ask_price_at_rank(0), Some(dec!(101)));
2813 }
2814
2815 #[test]
2816 fn test_bid_price_at_rank_best_bid_at_zero() {
2817 let mut b = book("X");
2818 b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
2819 b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
2820 assert_eq!(b.bid_price_at_rank(0), Some(dec!(99)));
2821 }
2822
2823 #[test]
2824 fn test_ask_price_at_rank_none_out_of_bounds() {
2825 let b = book("X");
2826 assert!(b.ask_price_at_rank(0).is_none());
2827 }
2828
2829 #[test]
2832 fn test_price_level_exists_true_when_present() {
2833 let mut b = book("X");
2834 b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
2835 assert!(b.price_level_exists(BookSide::Bid, dec!(100)));
2836 }
2837
2838 #[test]
2839 fn test_price_level_exists_false_when_absent() {
2840 let b = book("X");
2841 assert!(!b.price_level_exists(BookSide::Bid, dec!(100)));
2842 }
2843
2844 #[test]
2845 fn test_price_level_exists_ask_side() {
2846 let mut b = book("X");
2847 b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
2848 assert!(b.price_level_exists(BookSide::Ask, dec!(101)));
2849 assert!(!b.price_level_exists(BookSide::Ask, dec!(102)));
2850 }
2851}