1use crate::errors::{PolyfillError, Result};
4use crate::types::*;
5use crate::utils::math;
6use chrono::Utc;
7use parking_lot::RwLock;
8use rust_decimal::Decimal;
9use std::collections::HashMap;
10use std::sync::Arc; use tracing::{debug, trace, warn}; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14struct StoredLevel {
15 qty: Qty,
16 generation: u64,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub(crate) struct ParsedBookLevel {
21 pub side: Side,
22 pub price_ticks: Price,
23 pub size_units: Qty,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27enum BookSideKind {
28 Bid,
29 Ask,
30}
31
32#[derive(Debug, Clone)]
33struct BookSide {
34 levels: Vec<(Price, StoredLevel)>,
35 kind: BookSideKind,
36}
37
38impl BookSide {
39 fn new(kind: BookSideKind, max_depth: usize) -> Self {
40 Self {
41 levels: Vec::with_capacity(max_depth.saturating_add(8)),
42 kind,
43 }
44 }
45
46 #[inline]
47 fn len(&self) -> usize {
48 self.levels.len()
49 }
50
51 #[inline]
52 fn search(&self, price: Price) -> std::result::Result<usize, usize> {
53 match self.kind {
54 BookSideKind::Bid => self
56 .levels
57 .binary_search_by(|(level_price, _)| price.cmp(level_price)),
58 BookSideKind::Ask => self
60 .levels
61 .binary_search_by(|(level_price, _)| level_price.cmp(&price)),
62 }
63 }
64
65 #[inline]
66 fn best(&self) -> Option<(Price, &StoredLevel)> {
67 self.levels.first().map(|(price, level)| (*price, level))
68 }
69
70 #[inline]
71 fn get(&self, price: Price) -> Option<&StoredLevel> {
72 self.search(price)
73 .ok()
74 .and_then(|idx| self.levels.get(idx).map(|(_, level)| level))
75 }
76
77 #[inline]
78 fn upsert(&mut self, price: Price, level: StoredLevel) {
79 match self.search(price) {
80 Ok(idx) if level.qty == 0 => {
81 self.levels.remove(idx);
82 },
83 Ok(idx) => {
84 self.levels[idx].1 = level;
85 },
86 Err(idx) if level.qty != 0 => {
87 self.levels.insert(idx, (price, level));
88 },
89 Err(_) => {},
90 }
91 }
92
93 #[inline]
94 fn retain_generation(&mut self, generation: u64) {
95 self.levels
96 .retain(|(_, level)| level.generation == generation);
97 }
98
99 #[inline]
100 fn trim_depth(&mut self, max_depth: usize) {
101 if self.levels.len() > max_depth {
102 self.levels.truncate(max_depth);
103 }
104 }
105
106 #[inline]
107 fn iter_top(&self, depth: usize) -> impl Iterator<Item = (Price, &StoredLevel)> {
108 self.levels
109 .iter()
110 .take(depth)
111 .map(|(price, level)| (*price, level))
112 }
113
114 #[inline]
115 fn iter_all(&self) -> impl Iterator<Item = (Price, &StoredLevel)> {
116 self.levels.iter().map(|(price, level)| (*price, level))
117 }
118
119 fn sum_range(&self, min_price: Price, max_price: Price) -> Qty {
120 let mut total = 0;
121 for (price, level) in &self.levels {
122 match self.kind {
123 BookSideKind::Bid => {
124 if *price > max_price {
125 continue;
126 }
127 if *price < min_price {
128 break;
129 }
130 },
131 BookSideKind::Ask => {
132 if *price < min_price {
133 continue;
134 }
135 if *price > max_price {
136 break;
137 }
138 },
139 }
140
141 total += level.qty;
142 }
143 total
144 }
145}
146
147const DEFAULT_BOOK_SHARDS: usize = 64;
148
149#[derive(Debug, Clone)]
161pub struct OrderBook {
162 pub token_id: String,
164
165 pub token_id_hash: u64,
167
168 pub sequence: u64,
173
174 pub last_delta_sequence: u64,
179
180 pub last_snapshot_timestamp_ms: u64,
184
185 pub timestamp: chrono::DateTime<Utc>,
187
188 bids: BookSide,
200
201 asks: BookSide,
207
208 snapshot_generation: u64,
210
211 last_snapshot_hash_fingerprint: Option<u64>,
216
217 tick_size_ticks: Option<Price>,
221
222 max_depth: usize,
233}
234
235impl OrderBook {
236 pub fn new(token_id: String, max_depth: usize) -> Self {
239 let token_id_hash = {
241 use std::collections::hash_map::DefaultHasher;
242 use std::hash::{Hash, Hasher};
243 let mut hasher = DefaultHasher::new();
244 token_id.hash(&mut hasher);
245 hasher.finish()
246 };
247
248 Self {
249 token_id,
250 token_id_hash,
251 sequence: 0, last_delta_sequence: 0,
253 last_snapshot_timestamp_ms: 0,
254 timestamp: Utc::now(),
255 bids: BookSide::new(BookSideKind::Bid, max_depth), asks: BookSide::new(BookSideKind::Ask, max_depth), snapshot_generation: 0,
258 last_snapshot_hash_fingerprint: None,
259 tick_size_ticks: None, max_depth,
261 }
262 }
263
264 pub fn set_tick_size(&mut self, tick_size: Decimal) -> Result<()> {
268 let tick_size_ticks = decimal_to_price_exact(tick_size)
269 .map_err(|_| PolyfillError::validation("Invalid tick size"))?;
270 self.tick_size_ticks = Some(tick_size_ticks);
271 Ok(())
272 }
273
274 pub fn set_tick_size_ticks(&mut self, tick_size_ticks: Price) {
277 self.tick_size_ticks = Some(tick_size_ticks);
278 }
279
280 pub fn best_bid(&self) -> Option<BookLevel> {
285 self.bids.best().map(|(price_ticks, level)| {
290 BookLevel {
293 price: price_to_decimal(price_ticks),
294 size: qty_to_decimal(level.qty),
295 }
296 })
297 }
298
299 pub fn best_ask(&self) -> Option<BookLevel> {
304 self.asks.best().map(|(price_ticks, level)| {
309 BookLevel {
312 price: price_to_decimal(price_ticks),
313 size: qty_to_decimal(level.qty),
314 }
315 })
316 }
317
318 pub fn best_bid_fast(&self) -> Option<FastBookLevel> {
321 self.bids
322 .best()
323 .map(|(price, level)| FastBookLevel::new(price, level.qty))
324 }
325
326 pub fn best_ask_fast(&self) -> Option<FastBookLevel> {
329 self.asks
330 .best()
331 .map(|(price, level)| FastBookLevel::new(price, level.qty))
332 }
333
334 pub fn spread(&self) -> Option<Decimal> {
339 let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
347 let spread_ticks = math::spread_fast(best_bid_ticks, best_ask_ticks)?;
348 Some(price_to_decimal(spread_ticks))
349 }
350
351 pub fn mid_price(&self) -> Option<Decimal> {
356 let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
364 let mid_ticks = math::mid_price_fast(best_bid_ticks, best_ask_ticks)?;
365 Some(price_to_decimal(mid_ticks))
366 }
367
368 pub fn spread_pct(&self) -> Option<Decimal> {
373 let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
374 let spread_bps = math::spread_pct_fast(best_bid_ticks, best_ask_ticks)?;
375 Some(Decimal::from(spread_bps) / Decimal::from(100))
377 }
378
379 fn best_prices_fast(&self) -> Option<(Price, Price)> {
382 let best_bid_ticks = self.bids.best()?.0;
383 let best_ask_ticks = self.asks.best()?.0;
384 Some((best_bid_ticks, best_ask_ticks))
385 }
386
387 pub fn spread_fast(&self) -> Option<Price> {
390 let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
391 math::spread_fast(best_bid_ticks, best_ask_ticks)
392 }
393
394 pub fn mid_price_fast(&self) -> Option<Price> {
397 let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
398 math::mid_price_fast(best_bid_ticks, best_ask_ticks)
399 }
400
401 pub fn bids(&self, depth: Option<usize>) -> Vec<BookLevel> {
407 let depth = depth.unwrap_or(self.max_depth);
408 self.bids
409 .iter_top(depth)
410 .map(|(price_ticks, level)| BookLevel {
411 price: price_to_decimal(price_ticks),
412 size: qty_to_decimal(level.qty),
413 })
414 .collect()
415 }
416
417 pub fn asks(&self, depth: Option<usize>) -> Vec<BookLevel> {
423 let depth = depth.unwrap_or(self.max_depth);
424 self.asks
425 .iter_top(depth)
426 .map(|(price_ticks, level)| BookLevel {
427 price: price_to_decimal(price_ticks),
428 size: qty_to_decimal(level.qty),
429 })
430 .collect()
431 }
432
433 pub fn bids_fast(&self, depth: Option<usize>) -> Vec<FastBookLevel> {
436 let depth = depth.unwrap_or(self.max_depth);
437 self.bids
438 .iter_top(depth)
439 .map(|(price, level)| FastBookLevel::new(price, level.qty))
440 .collect()
441 }
442
443 pub fn asks_fast(&self, depth: Option<usize>) -> Vec<FastBookLevel> {
446 let depth = depth.unwrap_or(self.max_depth);
447 self.asks
448 .iter_top(depth)
449 .map(|(price, level)| FastBookLevel::new(price, level.qty))
450 .collect()
451 }
452
453 pub fn snapshot(&self) -> crate::types::OrderBook {
457 crate::types::OrderBook {
458 token_id: self.token_id.clone(),
459 timestamp: self.timestamp,
460 bids: self.bids(None), asks: self.asks(None), sequence: self.last_delta_sequence,
463 last_delta_sequence: self.last_delta_sequence,
464 last_snapshot_timestamp_ms: self.last_snapshot_timestamp_ms,
465 }
466 }
467
468 pub fn apply_delta(&mut self, delta: OrderDelta) -> Result<()> {
474 let tick_size_decimal = self.tick_size_ticks.map(price_to_decimal);
476 let fast_delta = FastOrderDelta::from_order_delta(&delta, tick_size_decimal)
477 .map_err(|e| PolyfillError::validation(format!("Invalid delta: {}", e)))?;
478
479 self.apply_delta_fast(fast_delta)
481 }
482
483 pub fn apply_delta_fast(&mut self, delta: FastOrderDelta) -> Result<()> {
493 if delta.sequence <= self.last_delta_sequence {
496 trace!(
497 "Ignoring stale delta: {} <= {}",
498 delta.sequence,
499 self.last_delta_sequence
500 );
501 return Ok(());
502 }
503
504 if delta.token_id_hash != self.token_id_hash {
506 return Err(PolyfillError::validation("Token ID mismatch"));
507 }
508
509 if let Some(tick_size_ticks) = self.tick_size_ticks {
512 if tick_size_ticks > 0 && !delta.price.is_multiple_of(tick_size_ticks) {
520 warn!(
522 "Rejecting misaligned price: {} not divisible by tick size {}",
523 delta.price, tick_size_ticks
524 );
525 return Err(PolyfillError::validation("Price not aligned to tick size"));
526 }
527 }
528
529 self.last_delta_sequence = delta.sequence;
531 self.sequence = delta.sequence;
532 self.timestamp = delta.timestamp;
533
534 match delta.side {
536 Side::BUY => self.apply_bid_delta_fast(delta.price, delta.size),
537 Side::SELL => self.apply_ask_delta_fast(delta.price, delta.size),
538 }
539
540 self.trim_depth();
542
543 debug!(
544 "Applied fast delta: {} {} @ {} ticks (seq: {})",
545 delta.side.as_str(),
546 delta.size,
547 delta.price,
548 delta.sequence
549 );
550
551 Ok(())
552 }
553
554 pub(crate) fn should_apply_ws_book_update(
556 &self,
557 asset_id: &str,
558 timestamp: u64,
559 hash: Option<&str>,
560 ) -> Result<bool> {
561 if asset_id != self.token_id {
562 return Err(PolyfillError::validation("Token ID mismatch"));
563 }
564
565 Ok(self.should_apply_snapshot(timestamp, hash))
566 }
567
568 pub(crate) fn apply_ws_book_snapshot_fast(
574 &mut self,
575 asset_id: &str,
576 timestamp: u64,
577 hash: Option<&str>,
578 levels: &[ParsedBookLevel],
579 ) -> Result<bool> {
580 if !self.should_apply_ws_book_update(asset_id, timestamp, hash)? {
581 return Ok(false);
582 }
583
584 self.apply_validated_snapshot(timestamp, hash, levels)?;
585
586 Ok(true)
587 }
588
589 pub fn apply_book_update(&mut self, update: &BookUpdate) -> Result<()> {
600 if update.asset_id != self.token_id {
601 return Err(PolyfillError::validation("Token ID mismatch"));
602 }
603
604 if !self.should_apply_snapshot(update.timestamp, update.hash.as_deref()) {
610 return Ok(());
611 }
612
613 for level in &update.bids {
616 let parsed = self.parse_snapshot_summary(Side::BUY, level)?;
617 self.validate_snapshot_level(parsed)?;
618 }
619
620 for level in &update.asks {
621 let parsed = self.parse_snapshot_summary(Side::SELL, level)?;
622 self.validate_snapshot_level(parsed)?;
623 }
624
625 self.last_snapshot_timestamp_ms = update.timestamp;
626 self.last_snapshot_hash_fingerprint = update.hash.as_deref().map(snapshot_hash_fingerprint);
627 self.timestamp = chrono::DateTime::<Utc>::from_timestamp_millis(update.timestamp as i64)
628 .unwrap_or_else(Utc::now);
629 self.begin_snapshot();
630
631 for level in &update.bids {
635 let parsed = self
636 .parse_snapshot_summary(Side::BUY, level)
637 .expect("book update bid level was validated before mutation");
638 self.apply_snapshot_level(parsed.side, parsed.price_ticks, parsed.size_units);
639 }
640
641 for level in &update.asks {
642 let parsed = self
643 .parse_snapshot_summary(Side::SELL, level)
644 .expect("book update ask level was validated before mutation");
645 self.apply_snapshot_level(parsed.side, parsed.price_ticks, parsed.size_units);
646 }
647
648 self.finish_snapshot();
649 self.trim_depth();
650 Ok(())
651 }
652
653 #[allow(dead_code)]
659 fn apply_bid_delta(&mut self, price: Decimal, size: Decimal) {
660 let price_ticks = decimal_to_price_lossy(price).unwrap_or(0);
662 let size_units = decimal_to_qty(size).unwrap_or(0);
663 self.apply_bid_delta_fast(price_ticks, size_units);
664 }
665
666 #[allow(dead_code)]
671 fn apply_ask_delta(&mut self, price: Decimal, size: Decimal) {
672 let price_ticks = decimal_to_price_lossy(price).unwrap_or(0);
674 let size_units = decimal_to_qty(size).unwrap_or(0);
675 self.apply_ask_delta_fast(price_ticks, size_units);
676 }
677
678 fn apply_bid_delta_fast(&mut self, price_ticks: Price, size_units: Qty) {
683 self.bids.upsert(
692 price_ticks,
693 StoredLevel {
694 qty: size_units,
695 generation: self.snapshot_generation,
696 },
697 );
698 }
699
700 fn apply_ask_delta_fast(&mut self, price_ticks: Price, size_units: Qty) {
705 self.asks.upsert(
714 price_ticks,
715 StoredLevel {
716 qty: size_units,
717 generation: self.snapshot_generation,
718 },
719 );
720 }
721
722 #[inline]
723 fn begin_snapshot(&mut self) {
724 self.snapshot_generation = self.snapshot_generation.wrapping_add(1);
725 }
726
727 #[inline]
728 fn should_apply_snapshot(&self, timestamp: u64, hash: Option<&str>) -> bool {
729 if timestamp > self.last_snapshot_timestamp_ms {
732 return true;
733 }
734 if timestamp < self.last_snapshot_timestamp_ms {
735 return false;
736 }
737
738 hash.is_some_and(|hash| {
739 self.last_snapshot_hash_fingerprint != Some(snapshot_hash_fingerprint(hash))
740 })
741 }
742
743 #[inline]
744 fn parse_snapshot_summary(&self, side: Side, level: &OrderSummary) -> Result<ParsedBookLevel> {
745 let price_ticks = decimal_to_price_exact(level.price)
746 .map_err(|_| PolyfillError::validation("Invalid price"))?;
747 let size_units =
748 decimal_to_qty(level.size).map_err(|_| PolyfillError::validation("Invalid size"))?;
749
750 Ok(ParsedBookLevel {
751 side,
752 price_ticks,
753 size_units,
754 })
755 }
756
757 #[inline]
758 fn validate_snapshot_level(&self, level: ParsedBookLevel) -> Result<()> {
759 if let Some(tick_size_ticks) = self.tick_size_ticks {
760 if tick_size_ticks > 0 && !level.price_ticks.is_multiple_of(tick_size_ticks) {
761 return Err(PolyfillError::validation("Price not aligned to tick size"));
762 }
763 }
764
765 Ok(())
766 }
767
768 fn apply_validated_snapshot(
769 &mut self,
770 timestamp: u64,
771 hash: Option<&str>,
772 levels: &[ParsedBookLevel],
773 ) -> Result<()> {
774 for &level in levels {
775 self.validate_snapshot_level(level)?;
776 }
777
778 self.last_snapshot_timestamp_ms = timestamp;
779 self.last_snapshot_hash_fingerprint = hash.map(snapshot_hash_fingerprint);
780 self.timestamp = chrono::DateTime::<Utc>::from_timestamp_millis(timestamp as i64)
781 .unwrap_or_else(Utc::now);
782 self.begin_snapshot();
783
784 for &level in levels {
785 self.apply_snapshot_level(level.side, level.price_ticks, level.size_units);
786 }
787
788 self.finish_snapshot();
789 self.trim_depth();
790
791 Ok(())
792 }
793
794 #[inline]
795 fn apply_snapshot_level(&mut self, side: Side, price_ticks: Price, size_units: Qty) {
796 let generation = self.snapshot_generation;
797 let book_side = match side {
798 Side::BUY => &mut self.bids,
799 Side::SELL => &mut self.asks,
800 };
801
802 book_side.upsert(
803 price_ticks,
804 StoredLevel {
805 qty: size_units,
806 generation,
807 },
808 );
809 }
810
811 #[inline]
812 fn finish_snapshot(&mut self) {
813 let generation = self.snapshot_generation;
814 self.bids.retain_generation(generation);
815 self.asks.retain_generation(generation);
816 }
817
818 fn trim_depth(&mut self) {
830 self.bids.trim_depth(self.max_depth);
833
834 self.asks.trim_depth(self.max_depth);
837 }
838
839 pub fn calculate_market_impact(&self, side: Side, size: Decimal) -> Option<MarketImpact> {
847 let size_units = decimal_to_qty(size).ok()?;
848 let (filled_units, total_notional, best_price_ticks) = match side {
849 Side::BUY => fill_market_impact(self.asks.iter_all(), size_units)?,
850 Side::SELL => fill_market_impact(self.bids.iter_all(), size_units)?,
851 };
852
853 let total_cost = Decimal::from_i128_with_scale(total_notional, 8);
854 let filled_size = qty_to_decimal(filled_units);
855 let avg_price = total_cost / filled_size;
856 let best_price = price_to_decimal(best_price_ticks);
857 let impact = if side == Side::BUY {
858 (avg_price - best_price) / best_price
859 } else {
860 (best_price - avg_price) / best_price
861 };
862
863 Some(MarketImpact {
864 average_price: avg_price,
865 impact_pct: impact,
866 total_cost,
867 size_filled: filled_size,
868 })
869 }
870
871 pub fn is_stale(&self, max_age: std::time::Duration) -> bool {
874 let age = Utc::now() - self.timestamp;
875 age > chrono::Duration::from_std(max_age).unwrap_or_default()
876 }
877
878 pub fn liquidity_at_price(&self, price: Decimal, side: Side) -> Decimal {
881 let price_ticks = match decimal_to_price_exact(price) {
883 Ok(ticks) => ticks,
884 Err(_) => return Decimal::ZERO, };
886
887 match side {
888 Side::BUY => {
889 let size_units = self
891 .asks
892 .get(price_ticks)
893 .map(|level| level.qty)
894 .unwrap_or_default();
895 qty_to_decimal(size_units)
896 },
897 Side::SELL => {
898 let size_units = self
900 .bids
901 .get(price_ticks)
902 .map(|level| level.qty)
903 .unwrap_or_default();
904 qty_to_decimal(size_units)
905 },
906 }
907 }
908
909 pub fn liquidity_in_range(
912 &self,
913 min_price: Decimal,
914 max_price: Decimal,
915 side: Side,
916 ) -> Decimal {
917 let min_price_ticks = match decimal_to_price_exact(min_price) {
919 Ok(ticks) => ticks,
920 Err(_) => return Decimal::ZERO, };
922 let max_price_ticks = match decimal_to_price_exact(max_price) {
923 Ok(ticks) => ticks,
924 Err(_) => return Decimal::ZERO, };
926
927 let total_size_units: Qty = match side {
928 Side::BUY => self.asks.sum_range(min_price_ticks, max_price_ticks),
929 Side::SELL => self.bids.sum_range(min_price_ticks, max_price_ticks),
930 };
931
932 qty_to_decimal(total_size_units)
933 }
934
935 pub fn is_valid(&self) -> bool {
938 self.best_prices_fast()
939 .map(|(best_bid_ticks, best_ask_ticks)| best_bid_ticks < best_ask_ticks)
940 .unwrap_or(true)
941 }
942}
943
944fn fill_market_impact<'a>(
945 levels: impl Iterator<Item = (Price, &'a StoredLevel)>,
946 size_units: Qty,
947) -> Option<(Qty, i128, Price)> {
948 if size_units <= 0 {
949 return None;
950 }
951
952 let mut remaining_units = size_units;
953 let mut filled_units = 0;
954 let mut total_notional = 0i128;
955 let mut best_price_ticks = None;
956
957 for (price_ticks, level) in levels {
958 if best_price_ticks.is_none() {
959 best_price_ticks = Some(price_ticks);
960 }
961
962 if level.qty <= 0 {
963 continue;
964 }
965
966 let fill_units = remaining_units.min(level.qty);
967 let fill_notional = (price_ticks as i128).checked_mul(fill_units as i128)?;
968 total_notional = total_notional.checked_add(fill_notional)?;
969 filled_units += fill_units;
970 remaining_units -= fill_units;
971
972 if remaining_units == 0 {
973 break;
974 }
975 }
976
977 if remaining_units > 0 {
978 return None;
979 }
980
981 Some((filled_units, total_notional, best_price_ticks?))
982}
983
984#[derive(Debug, Clone)]
987pub struct MarketImpact {
988 pub average_price: Decimal, pub impact_pct: Decimal, pub total_cost: Decimal, pub size_filled: Decimal, }
993
994#[derive(Debug)]
1005pub struct OrderBookManager {
1006 shards: Arc<[BookShard]>, max_depth: usize,
1008}
1009
1010#[derive(Debug, Default)]
1011struct BookShard {
1012 books: RwLock<HashMap<String, OrderBook>>,
1013}
1014
1015#[inline]
1016fn snapshot_hash_fingerprint(hash: &str) -> u64 {
1017 let mut fingerprint = 0xcbf2_9ce4_8422_2325u64;
1018 for &byte in hash.as_bytes() {
1019 fingerprint ^= byte as u64;
1020 fingerprint = fingerprint.wrapping_mul(0x0000_0100_0000_01b3);
1021 }
1022 fingerprint
1023}
1024
1025#[inline]
1026fn shard_index(token_id: &str, shard_count: usize) -> usize {
1027 debug_assert!(shard_count > 0);
1028 let mut hash = 0xcbf2_9ce4_8422_2325u64;
1029 for &byte in token_id.as_bytes() {
1030 hash ^= byte as u64;
1031 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
1032 }
1033 (hash as usize) % shard_count
1034}
1035
1036impl OrderBookManager {
1037 pub fn new(max_depth: usize) -> Self {
1040 Self::with_shard_count(max_depth, DEFAULT_BOOK_SHARDS)
1041 }
1042
1043 pub fn with_shard_count(max_depth: usize, shard_count: usize) -> Self {
1048 let shard_count = shard_count.max(1);
1049 let shards = (0..shard_count)
1050 .map(|_| BookShard::default())
1051 .collect::<Vec<_>>()
1052 .into();
1053
1054 Self { shards, max_depth }
1055 }
1056
1057 #[inline]
1058 fn shard_for(&self, token_id: &str) -> &BookShard {
1059 &self.shards[shard_index(token_id, self.shards.len())]
1060 }
1061
1062 pub fn get_or_create_book(&self, token_id: &str) -> Result<OrderBook> {
1065 let shard = self.shard_for(token_id);
1066 let mut books = shard.books.write();
1067
1068 if let Some(book) = books.get(token_id) {
1069 Ok(book.clone()) } else {
1071 let book = OrderBook::new(token_id.to_string(), self.max_depth);
1073 books.insert(token_id.to_string(), book.clone());
1074 Ok(book)
1075 }
1076 }
1077
1078 pub fn with_book_mut<R>(
1083 &self,
1084 token_id: &str,
1085 f: impl FnOnce(&mut OrderBook) -> Result<R>,
1086 ) -> Result<R> {
1087 let shard = self.shard_for(token_id);
1088 let mut books = shard.books.write();
1089
1090 let book = books.get_mut(token_id).ok_or_else(|| {
1091 PolyfillError::market_data(
1092 format!("No book found for token: {}", token_id),
1093 crate::errors::MarketDataErrorKind::TokenNotFound,
1094 )
1095 })?;
1096
1097 f(book)
1098 }
1099
1100 pub fn apply_delta(&self, delta: OrderDelta) -> Result<()> {
1103 let shard = self.shard_for(delta.token_id.as_str());
1104 let mut books = shard.books.write();
1105
1106 let book = books.get_mut(&delta.token_id).ok_or_else(|| {
1108 PolyfillError::market_data(
1109 format!("No book found for token: {}", delta.token_id),
1110 crate::errors::MarketDataErrorKind::TokenNotFound,
1111 )
1112 })?;
1113
1114 book.apply_delta(delta)
1116 }
1117
1118 pub fn apply_book_update(&self, update: &BookUpdate) -> Result<()> {
1123 let shard = self.shard_for(update.asset_id.as_str());
1124 let mut books = shard.books.write();
1125
1126 if !books.contains_key(update.asset_id.as_str()) {
1127 let token_id = update.asset_id.clone();
1128 books.insert(token_id.clone(), OrderBook::new(token_id, self.max_depth));
1129 }
1130
1131 books
1132 .get_mut(update.asset_id.as_str())
1133 .ok_or_else(|| PolyfillError::internal_simple("Failed to insert order book"))?
1134 .apply_book_update(update)
1135 }
1136
1137 pub fn get_book(&self, token_id: &str) -> Result<crate::types::OrderBook> {
1140 let shard = self.shard_for(token_id);
1141 let books = shard.books.read();
1142
1143 books
1144 .get(token_id)
1145 .map(|book| book.snapshot()) .ok_or_else(|| {
1147 PolyfillError::market_data(
1148 format!("No book found for token: {}", token_id),
1149 crate::errors::MarketDataErrorKind::TokenNotFound,
1150 )
1151 })
1152 }
1153
1154 pub fn get_all_books(&self) -> Result<Vec<crate::types::OrderBook>> {
1157 let mut snapshots = Vec::new();
1158 for shard in self.shards.iter() {
1159 let books = shard.books.read();
1160 snapshots.extend(books.values().map(|book| book.snapshot()));
1161 }
1162
1163 Ok(snapshots)
1164 }
1165
1166 pub fn cleanup_stale_books(&self, max_age: std::time::Duration) -> Result<usize> {
1170 let mut removed = 0usize;
1171
1172 for shard in self.shards.iter() {
1173 let mut books = shard.books.write();
1174 let initial_count = books.len();
1175 books.retain(|_, book| !book.is_stale(max_age)); removed += initial_count - books.len();
1177 }
1178
1179 if removed > 0 {
1180 debug!("Removed {} stale order books", removed);
1181 }
1182
1183 Ok(removed)
1184 }
1185}
1186
1187#[derive(Debug, Clone)]
1190pub struct BookAnalytics {
1191 pub token_id: String,
1192 pub timestamp: chrono::DateTime<Utc>,
1193 pub bid_count: usize, pub ask_count: usize, pub total_bid_size: Decimal, pub total_ask_size: Decimal, pub spread: Option<Decimal>, pub spread_pct: Option<Decimal>, pub mid_price: Option<Decimal>, pub volatility: Option<Decimal>, }
1202
1203impl OrderBook {
1204 pub fn analytics(&self) -> BookAnalytics {
1207 let bid_count = self.bids.len();
1208 let ask_count = self.asks.len();
1209 let total_bid_size_units: i64 = self.bids.iter_all().map(|(_, level)| level.qty).sum();
1211 let total_ask_size_units: i64 = self.asks.iter_all().map(|(_, level)| level.qty).sum();
1212 let total_bid_size = qty_to_decimal(total_bid_size_units);
1213 let total_ask_size = qty_to_decimal(total_ask_size_units);
1214
1215 BookAnalytics {
1216 token_id: self.token_id.clone(),
1217 timestamp: self.timestamp,
1218 bid_count,
1219 ask_count,
1220 total_bid_size,
1221 total_ask_size,
1222 spread: self.spread(),
1223 spread_pct: self.spread_pct(),
1224 mid_price: self.mid_price(),
1225 volatility: self.calculate_volatility(),
1226 }
1227 }
1228
1229 fn calculate_volatility(&self) -> Option<Decimal> {
1232 None
1236 }
1237}
1238
1239#[cfg(test)]
1240mod tests {
1241 use super::*;
1242 use rust_decimal_macros::dec;
1243 use std::str::FromStr;
1244 use std::time::Duration; #[test]
1247 fn test_order_book_creation() {
1248 let book = OrderBook::new("test_token".to_string(), 10);
1250 assert_eq!(book.token_id, "test_token");
1251 assert_eq!(book.bids.len(), 0); assert_eq!(book.asks.len(), 0); }
1254
1255 #[test]
1256 fn test_set_tick_size_requires_exact_fixed_point_value() {
1257 let mut book = OrderBook::new("test_token".to_string(), 10);
1258
1259 book.set_tick_size(dec!(0.0001)).unwrap();
1260
1261 let err = book.set_tick_size(dec!(0.00005)).unwrap_err();
1262 assert!(err.to_string().contains("Invalid tick size"));
1263
1264 let err = book.set_tick_size(Decimal::ZERO).unwrap_err();
1265 assert!(err.to_string().contains("Invalid tick size"));
1266 }
1267
1268 #[test]
1269 fn test_order_book_manager_routes_tokens_to_shards() {
1270 let shard_count = 4;
1271 let first_token = "test_token_0";
1272 let first_shard = shard_index(first_token, shard_count);
1273 let second_token = (1..100)
1274 .map(|idx| format!("test_token_{idx}"))
1275 .find(|token| shard_index(token, shard_count) != first_shard)
1276 .expect("test tokens should cover multiple shards");
1277
1278 let manager = OrderBookManager::with_shard_count(10, shard_count);
1279 manager.get_or_create_book(first_token).unwrap();
1280 manager.get_or_create_book(&second_token).unwrap();
1281
1282 manager
1283 .apply_delta(OrderDelta {
1284 token_id: first_token.to_string(),
1285 timestamp: Utc::now(),
1286 side: Side::BUY,
1287 price: dec!(0.50),
1288 size: dec!(100),
1289 sequence: 1,
1290 })
1291 .unwrap();
1292 manager
1293 .apply_delta(OrderDelta {
1294 token_id: second_token.clone(),
1295 timestamp: Utc::now(),
1296 side: Side::SELL,
1297 price: dec!(0.60),
1298 size: dec!(200),
1299 sequence: 1,
1300 })
1301 .unwrap();
1302
1303 let first_book = manager.get_book(first_token).unwrap();
1304 let second_book = manager.get_book(&second_token).unwrap();
1305
1306 assert_eq!(first_book.bids.len(), 1);
1307 assert_eq!(first_book.asks.len(), 0);
1308 assert_eq!(second_book.bids.len(), 0);
1309 assert_eq!(second_book.asks.len(), 1);
1310 assert_eq!(manager.get_all_books().unwrap().len(), 2);
1311 }
1312
1313 #[test]
1314 fn test_apply_delta() {
1315 let mut book = OrderBook::new("test_token".to_string(), 10);
1317
1318 let delta = OrderDelta {
1320 token_id: "test_token".to_string(),
1321 timestamp: Utc::now(),
1322 side: Side::BUY,
1323 price: dec!(0.5),
1324 size: dec!(100),
1325 sequence: 1,
1326 };
1327
1328 book.apply_delta(delta).unwrap();
1329 assert_eq!(book.sequence, 1); assert_eq!(book.last_delta_sequence, 1);
1331 assert_eq!(book.last_snapshot_timestamp_ms, 0);
1332 assert_eq!(book.best_bid().unwrap().price, dec!(0.5)); assert_eq!(book.best_bid().unwrap().size, dec!(100)); }
1335
1336 #[test]
1337 fn test_sorted_side_ordering_and_removal() {
1338 let mut book = OrderBook::new("test_token".to_string(), 10);
1339
1340 for (sequence, price) in [(1, dec!(0.73)), (2, dec!(0.75)), (3, dec!(0.74))] {
1341 book.apply_delta(OrderDelta {
1342 token_id: "test_token".to_string(),
1343 timestamp: Utc::now(),
1344 side: Side::BUY,
1345 price,
1346 size: dec!(100),
1347 sequence,
1348 })
1349 .unwrap();
1350 }
1351
1352 for (sequence, price) in [(4, dec!(0.78)), (5, dec!(0.76)), (6, dec!(0.77))] {
1353 book.apply_delta(OrderDelta {
1354 token_id: "test_token".to_string(),
1355 timestamp: Utc::now(),
1356 side: Side::SELL,
1357 price,
1358 size: dec!(100),
1359 sequence,
1360 })
1361 .unwrap();
1362 }
1363
1364 let bids: Vec<_> = book
1365 .bids(Some(3))
1366 .into_iter()
1367 .map(|level| level.price)
1368 .collect();
1369 let asks: Vec<_> = book
1370 .asks(Some(3))
1371 .into_iter()
1372 .map(|level| level.price)
1373 .collect();
1374 assert_eq!(bids, vec![dec!(0.75), dec!(0.74), dec!(0.73)]);
1375 assert_eq!(asks, vec![dec!(0.76), dec!(0.77), dec!(0.78)]);
1376
1377 book.apply_delta(OrderDelta {
1378 token_id: "test_token".to_string(),
1379 timestamp: Utc::now(),
1380 side: Side::BUY,
1381 price: dec!(0.75),
1382 size: Decimal::ZERO,
1383 sequence: 7,
1384 })
1385 .unwrap();
1386 book.apply_delta(OrderDelta {
1387 token_id: "test_token".to_string(),
1388 timestamp: Utc::now(),
1389 side: Side::SELL,
1390 price: dec!(0.76),
1391 size: Decimal::ZERO,
1392 sequence: 8,
1393 })
1394 .unwrap();
1395
1396 assert_eq!(book.best_bid().unwrap().price, dec!(0.74));
1397 assert_eq!(book.best_ask().unwrap().price, dec!(0.77));
1398 }
1399
1400 #[test]
1401 fn test_book_update_replaces_snapshot_and_uses_millis_timestamp() {
1402 let mut book = OrderBook::new("test_token".to_string(), 10);
1403 let timestamp = 1_757_908_892_351;
1404
1405 book.apply_book_update(&BookUpdate {
1406 asset_id: "test_token".to_string(),
1407 market: "0xabc".to_string(),
1408 timestamp,
1409 bids: vec![
1410 OrderSummary {
1411 price: dec!(0.48),
1412 size: dec!(10),
1413 },
1414 OrderSummary {
1415 price: dec!(0.49),
1416 size: dec!(20),
1417 },
1418 ],
1419 asks: vec![
1420 OrderSummary {
1421 price: dec!(0.52),
1422 size: dec!(30),
1423 },
1424 OrderSummary {
1425 price: dec!(0.53),
1426 size: dec!(40),
1427 },
1428 ],
1429 hash: None,
1430 })
1431 .unwrap();
1432
1433 book.apply_book_update(&BookUpdate {
1434 asset_id: "test_token".to_string(),
1435 market: "0xabc".to_string(),
1436 timestamp: timestamp + 1,
1437 bids: vec![OrderSummary {
1438 price: dec!(0.49),
1439 size: dec!(25),
1440 }],
1441 asks: vec![OrderSummary {
1442 price: dec!(0.53),
1443 size: dec!(45),
1444 }],
1445 hash: None,
1446 })
1447 .unwrap();
1448
1449 assert_eq!(book.timestamp.timestamp_millis(), timestamp as i64 + 1);
1450 assert_eq!(book.bids(None).len(), 1);
1451 assert_eq!(book.asks(None).len(), 1);
1452 assert_eq!(book.best_bid().unwrap().price, dec!(0.49));
1453 assert_eq!(book.best_bid().unwrap().size, dec!(25));
1454 assert_eq!(book.best_ask().unwrap().price, dec!(0.53));
1455 assert_eq!(book.best_ask().unwrap().size, dec!(45));
1456 }
1457
1458 #[test]
1459 fn test_ws_snapshot_timestamp_does_not_block_delta_sequence() {
1460 let mut book = OrderBook::new("test_token".to_string(), 10);
1461 let snapshot_timestamp_ms = 1_757_908_892_351;
1462 let levels = [
1463 ParsedBookLevel {
1464 side: Side::BUY,
1465 price_ticks: 5_000,
1466 size_units: 100_000,
1467 },
1468 ParsedBookLevel {
1469 side: Side::SELL,
1470 price_ticks: 6_000,
1471 size_units: 100_000,
1472 },
1473 ];
1474
1475 assert!(book
1476 .apply_ws_book_snapshot_fast("test_token", snapshot_timestamp_ms, None, &levels)
1477 .unwrap());
1478
1479 assert_eq!(book.sequence, 0);
1480 assert_eq!(book.last_delta_sequence, 0);
1481 assert_eq!(book.last_snapshot_timestamp_ms, snapshot_timestamp_ms);
1482
1483 book.apply_delta(OrderDelta {
1484 token_id: "test_token".to_string(),
1485 timestamp: Utc::now(),
1486 side: Side::BUY,
1487 price: dec!(0.51),
1488 size: dec!(11),
1489 sequence: 1,
1490 })
1491 .unwrap();
1492
1493 assert_eq!(book.sequence, 1);
1494 assert_eq!(book.last_delta_sequence, 1);
1495 assert_eq!(book.last_snapshot_timestamp_ms, snapshot_timestamp_ms);
1496 assert_eq!(book.best_bid().unwrap().price, dec!(0.51));
1497 }
1498
1499 #[test]
1500 fn test_delta_sequence_does_not_block_snapshot_timestamp() {
1501 let mut book = OrderBook::new("test_token".to_string(), 10);
1502
1503 book.apply_delta(OrderDelta {
1504 token_id: "test_token".to_string(),
1505 timestamp: Utc::now(),
1506 side: Side::BUY,
1507 price: dec!(0.40),
1508 size: dec!(10),
1509 sequence: 10_000,
1510 })
1511 .unwrap();
1512
1513 book.apply_book_update(&BookUpdate {
1514 asset_id: "test_token".to_string(),
1515 market: "0xabc".to_string(),
1516 timestamp: 1_000,
1517 bids: vec![OrderSummary {
1518 price: dec!(0.50),
1519 size: dec!(20),
1520 }],
1521 asks: vec![OrderSummary {
1522 price: dec!(0.60),
1523 size: dec!(30),
1524 }],
1525 hash: None,
1526 })
1527 .unwrap();
1528
1529 assert_eq!(book.sequence, 10_000);
1530 assert_eq!(book.last_delta_sequence, 10_000);
1531 assert_eq!(book.last_snapshot_timestamp_ms, 1_000);
1532 assert_eq!(book.best_bid().unwrap().price, dec!(0.50));
1533 assert_eq!(book.best_ask().unwrap().price, dec!(0.60));
1534
1535 let snapshot = book.snapshot();
1536 assert_eq!(snapshot.sequence, 10_000);
1537 assert_eq!(snapshot.last_delta_sequence, 10_000);
1538 assert_eq!(snapshot.last_snapshot_timestamp_ms, 1_000);
1539 }
1540
1541 #[test]
1542 fn test_book_update_allows_same_timestamp_with_different_hash() {
1543 let mut book = OrderBook::new("test_token".to_string(), 10);
1544 let timestamp = 1_757_908_892_351;
1545
1546 book.apply_book_update(&BookUpdate {
1547 asset_id: "test_token".to_string(),
1548 market: "0xabc".to_string(),
1549 timestamp,
1550 bids: vec![OrderSummary {
1551 price: dec!(0.48),
1552 size: dec!(10),
1553 }],
1554 asks: vec![OrderSummary {
1555 price: dec!(0.52),
1556 size: dec!(20),
1557 }],
1558 hash: Some("hash_a".to_string()),
1559 })
1560 .unwrap();
1561
1562 book.apply_book_update(&BookUpdate {
1563 asset_id: "test_token".to_string(),
1564 market: "0xabc".to_string(),
1565 timestamp,
1566 bids: vec![OrderSummary {
1567 price: dec!(0.49),
1568 size: dec!(30),
1569 }],
1570 asks: vec![OrderSummary {
1571 price: dec!(0.53),
1572 size: dec!(40),
1573 }],
1574 hash: Some("hash_b".to_string()),
1575 })
1576 .unwrap();
1577
1578 assert_eq!(book.best_bid().unwrap().price, dec!(0.49));
1579 assert_eq!(book.best_bid().unwrap().size, dec!(30));
1580 assert_eq!(book.best_ask().unwrap().price, dec!(0.53));
1581 assert_eq!(book.best_ask().unwrap().size, dec!(40));
1582
1583 book.apply_book_update(&BookUpdate {
1584 asset_id: "test_token".to_string(),
1585 market: "0xabc".to_string(),
1586 timestamp,
1587 bids: vec![OrderSummary {
1588 price: dec!(0.50),
1589 size: dec!(50),
1590 }],
1591 asks: vec![OrderSummary {
1592 price: dec!(0.54),
1593 size: dec!(60),
1594 }],
1595 hash: Some("hash_b".to_string()),
1596 })
1597 .unwrap();
1598
1599 assert_eq!(book.best_bid().unwrap().price, dec!(0.49));
1600 assert_eq!(book.best_ask().unwrap().price, dec!(0.53));
1601 }
1602
1603 #[test]
1604 fn test_book_update_rejects_same_timestamp_without_hash_tie_breaker() {
1605 let mut book = OrderBook::new("test_token".to_string(), 10);
1606 let timestamp = 1_757_908_892_351;
1607
1608 book.apply_book_update(&BookUpdate {
1609 asset_id: "test_token".to_string(),
1610 market: "0xabc".to_string(),
1611 timestamp,
1612 bids: vec![OrderSummary {
1613 price: dec!(0.48),
1614 size: dec!(10),
1615 }],
1616 asks: vec![OrderSummary {
1617 price: dec!(0.52),
1618 size: dec!(20),
1619 }],
1620 hash: None,
1621 })
1622 .unwrap();
1623
1624 book.apply_book_update(&BookUpdate {
1625 asset_id: "test_token".to_string(),
1626 market: "0xabc".to_string(),
1627 timestamp,
1628 bids: vec![OrderSummary {
1629 price: dec!(0.49),
1630 size: dec!(30),
1631 }],
1632 asks: vec![OrderSummary {
1633 price: dec!(0.53),
1634 size: dec!(40),
1635 }],
1636 hash: None,
1637 })
1638 .unwrap();
1639
1640 assert_eq!(book.best_bid().unwrap().price, dec!(0.48));
1641 assert_eq!(book.best_ask().unwrap().price, dec!(0.52));
1642 }
1643
1644 #[test]
1645 fn test_book_update_error_keeps_existing_snapshot() {
1646 let mut book = OrderBook::new("test_token".to_string(), 10);
1647 book.set_tick_size_ticks(100);
1648
1649 book.apply_book_update(&BookUpdate {
1650 asset_id: "test_token".to_string(),
1651 market: "0xabc".to_string(),
1652 timestamp: 100,
1653 bids: vec![OrderSummary {
1654 price: dec!(0.50),
1655 size: dec!(10),
1656 }],
1657 asks: vec![OrderSummary {
1658 price: dec!(0.60),
1659 size: dec!(20),
1660 }],
1661 hash: None,
1662 })
1663 .unwrap();
1664
1665 let err = book
1666 .apply_book_update(&BookUpdate {
1667 asset_id: "test_token".to_string(),
1668 market: "0xabc".to_string(),
1669 timestamp: 101,
1670 bids: vec![
1671 OrderSummary {
1672 price: dec!(0.51),
1673 size: dec!(11),
1674 },
1675 OrderSummary {
1676 price: dec!(0.515),
1677 size: dec!(12),
1678 },
1679 ],
1680 asks: vec![OrderSummary {
1681 price: dec!(0.61),
1682 size: dec!(21),
1683 }],
1684 hash: None,
1685 })
1686 .unwrap_err();
1687
1688 assert!(err.to_string().contains("Price not aligned"));
1689 assert_eq!(book.sequence, 0);
1690 assert_eq!(book.last_delta_sequence, 0);
1691 assert_eq!(book.last_snapshot_timestamp_ms, 100);
1692 assert_eq!(book.bids(None).len(), 1);
1693 assert_eq!(book.asks(None).len(), 1);
1694 assert_eq!(book.best_bid().unwrap().price, dec!(0.50));
1695 assert_eq!(book.best_bid().unwrap().size, dec!(10));
1696 assert_eq!(book.best_ask().unwrap().price, dec!(0.60));
1697 assert_eq!(book.best_ask().unwrap().size, dec!(20));
1698 }
1699
1700 #[test]
1701 fn test_book_update_depth_keeps_best_prices_independent_of_payload_order() {
1702 let mut book = OrderBook::new("test_token".to_string(), 2);
1703
1704 book.apply_book_update(&BookUpdate {
1705 asset_id: "test_token".to_string(),
1706 market: "0xabc".to_string(),
1707 timestamp: 1_757_908_892_351,
1708 bids: vec![
1709 OrderSummary {
1710 price: dec!(0.49),
1711 size: dec!(10),
1712 },
1713 OrderSummary {
1714 price: dec!(0.50),
1715 size: dec!(20),
1716 },
1717 OrderSummary {
1718 price: dec!(0.48),
1719 size: dec!(30),
1720 },
1721 ],
1722 asks: vec![
1723 OrderSummary {
1724 price: dec!(0.52),
1725 size: dec!(40),
1726 },
1727 OrderSummary {
1728 price: dec!(0.54),
1729 size: dec!(50),
1730 },
1731 OrderSummary {
1732 price: dec!(0.53),
1733 size: dec!(60),
1734 },
1735 ],
1736 hash: None,
1737 })
1738 .unwrap();
1739
1740 let bids = book.bids(None);
1741 assert_eq!(bids.len(), 2);
1742 assert_eq!(bids[0].price, dec!(0.50));
1743 assert_eq!(bids[1].price, dec!(0.49));
1744
1745 let asks = book.asks(None);
1746 assert_eq!(asks.len(), 2);
1747 assert_eq!(asks[0].price, dec!(0.52));
1748 assert_eq!(asks[1].price, dec!(0.53));
1749 }
1750
1751 #[test]
1752 fn test_spread_calculation() {
1753 let mut book = OrderBook::new("test_token".to_string(), 10);
1755
1756 book.apply_delta(OrderDelta {
1758 token_id: "test_token".to_string(),
1759 timestamp: Utc::now(),
1760 side: Side::BUY,
1761 price: dec!(0.5),
1762 size: dec!(100),
1763 sequence: 1,
1764 })
1765 .unwrap();
1766
1767 book.apply_delta(OrderDelta {
1769 token_id: "test_token".to_string(),
1770 timestamp: Utc::now(),
1771 side: Side::SELL,
1772 price: dec!(0.52),
1773 size: dec!(100),
1774 sequence: 2,
1775 })
1776 .unwrap();
1777
1778 let spread = book.spread().unwrap();
1779 assert_eq!(spread, dec!(0.02)); }
1781
1782 #[test]
1783 fn test_market_impact() {
1784 let mut book = OrderBook::new("test_token".to_string(), 10);
1786
1787 for (i, price) in [dec!(0.50), dec!(0.51), dec!(0.52)].iter().enumerate() {
1790 book.apply_delta(OrderDelta {
1791 token_id: "test_token".to_string(),
1792 timestamp: Utc::now(),
1793 side: Side::SELL,
1794 price: *price,
1795 size: dec!(100),
1796 sequence: i as u64 + 1,
1797 })
1798 .unwrap();
1799 }
1800
1801 let impact = book.calculate_market_impact(Side::BUY, dec!(150)).unwrap();
1803 assert!(impact.average_price > dec!(0.50)); assert!(impact.average_price < dec!(0.51)); }
1806
1807 #[test]
1808 fn test_apply_bid_delta_legacy() {
1809 let mut book = OrderBook::new("test_token".to_string(), 10);
1810
1811 book.apply_bid_delta(
1813 Decimal::from_str("0.75").unwrap(),
1814 Decimal::from_str("100.0").unwrap(),
1815 );
1816
1817 let best_bid = book.best_bid();
1818 assert!(best_bid.is_some());
1819 let bid = best_bid.unwrap();
1820 assert_eq!(bid.price, Decimal::from_str("0.75").unwrap());
1821 assert_eq!(bid.size, Decimal::from_str("100.0").unwrap());
1822
1823 book.apply_bid_delta(
1825 Decimal::from_str("0.75").unwrap(),
1826 Decimal::from_str("150.0").unwrap(),
1827 );
1828 let updated_bid = book.best_bid().unwrap();
1829 assert_eq!(updated_bid.size, Decimal::from_str("150.0").unwrap());
1830
1831 book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::ZERO);
1833 assert!(book.best_bid().is_none());
1834 }
1835
1836 #[test]
1837 fn test_apply_ask_delta_legacy() {
1838 let mut book = OrderBook::new("test_token".to_string(), 10);
1839
1840 book.apply_ask_delta(
1842 Decimal::from_str("0.76").unwrap(),
1843 Decimal::from_str("50.0").unwrap(),
1844 );
1845
1846 let best_ask = book.best_ask();
1847 assert!(best_ask.is_some());
1848 let ask = best_ask.unwrap();
1849 assert_eq!(ask.price, Decimal::from_str("0.76").unwrap());
1850 assert_eq!(ask.size, Decimal::from_str("50.0").unwrap());
1851
1852 book.apply_ask_delta(
1854 Decimal::from_str("0.76").unwrap(),
1855 Decimal::from_str("75.0").unwrap(),
1856 );
1857 let updated_ask = book.best_ask().unwrap();
1858 assert_eq!(updated_ask.size, Decimal::from_str("75.0").unwrap());
1859
1860 book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::ZERO);
1862 assert!(book.best_ask().is_none());
1863 }
1864
1865 #[test]
1866 fn test_liquidity_analysis() {
1867 let mut book = OrderBook::new("test_token".to_string(), 10);
1868
1869 book.apply_bid_delta(
1871 Decimal::from_str("0.75").unwrap(),
1872 Decimal::from_str("100.0").unwrap(),
1873 );
1874 book.apply_bid_delta(
1875 Decimal::from_str("0.74").unwrap(),
1876 Decimal::from_str("50.0").unwrap(),
1877 );
1878 book.apply_ask_delta(
1879 Decimal::from_str("0.76").unwrap(),
1880 Decimal::from_str("80.0").unwrap(),
1881 );
1882 book.apply_ask_delta(
1883 Decimal::from_str("0.77").unwrap(),
1884 Decimal::from_str("120.0").unwrap(),
1885 );
1886
1887 let buy_liquidity = book.liquidity_at_price(Decimal::from_str("0.76").unwrap(), Side::BUY);
1889 assert_eq!(buy_liquidity, Decimal::from_str("80.0").unwrap());
1890
1891 let sell_liquidity =
1893 book.liquidity_at_price(Decimal::from_str("0.75").unwrap(), Side::SELL);
1894 assert_eq!(sell_liquidity, Decimal::from_str("100.0").unwrap());
1895
1896 let buy_range_liquidity = book.liquidity_in_range(
1898 Decimal::from_str("0.74").unwrap(),
1899 Decimal::from_str("0.77").unwrap(),
1900 Side::BUY,
1901 );
1902 assert_eq!(buy_range_liquidity, Decimal::from_str("200.0").unwrap());
1904
1905 let sell_range_liquidity = book.liquidity_in_range(
1907 Decimal::from_str("0.74").unwrap(),
1908 Decimal::from_str("0.77").unwrap(),
1909 Side::SELL,
1910 );
1911 assert_eq!(sell_range_liquidity, Decimal::from_str("150.0").unwrap());
1913 }
1914
1915 #[test]
1916 fn test_book_validation() {
1917 let mut book = OrderBook::new("test_token".to_string(), 10);
1918
1919 assert!(book.is_valid());
1921
1922 book.apply_bid_delta(
1924 Decimal::from_str("0.75").unwrap(),
1925 Decimal::from_str("100.0").unwrap(),
1926 );
1927 book.apply_ask_delta(
1928 Decimal::from_str("0.76").unwrap(),
1929 Decimal::from_str("80.0").unwrap(),
1930 );
1931 assert!(book.is_valid());
1932
1933 book.apply_bid_delta(
1935 Decimal::from_str("0.77").unwrap(),
1936 Decimal::from_str("50.0").unwrap(),
1937 );
1938 assert!(!book.is_valid());
1939 }
1940
1941 #[test]
1942 fn test_book_staleness() {
1943 let mut book = OrderBook::new("test_token".to_string(), 10);
1944
1945 assert!(!book.is_stale(Duration::from_secs(60))); book.apply_bid_delta(
1950 Decimal::from_str("0.75").unwrap(),
1951 Decimal::from_str("100.0").unwrap(),
1952 );
1953 assert!(!book.is_stale(Duration::from_secs(60)));
1954
1955 }
1958
1959 #[test]
1960 fn test_depth_management() {
1961 let mut book = OrderBook::new("test_token".to_string(), 3); book.apply_bid_delta(
1965 Decimal::from_str("0.75").unwrap(),
1966 Decimal::from_str("100.0").unwrap(),
1967 );
1968 book.apply_bid_delta(
1969 Decimal::from_str("0.74").unwrap(),
1970 Decimal::from_str("50.0").unwrap(),
1971 );
1972 book.apply_bid_delta(
1973 Decimal::from_str("0.73").unwrap(),
1974 Decimal::from_str("20.0").unwrap(),
1975 );
1976
1977 book.apply_ask_delta(
1978 Decimal::from_str("0.76").unwrap(),
1979 Decimal::from_str("80.0").unwrap(),
1980 );
1981 book.apply_ask_delta(
1982 Decimal::from_str("0.77").unwrap(),
1983 Decimal::from_str("40.0").unwrap(),
1984 );
1985 book.apply_ask_delta(
1986 Decimal::from_str("0.78").unwrap(),
1987 Decimal::from_str("30.0").unwrap(),
1988 );
1989
1990 let bids = book.bids(Some(3));
1992 let asks = book.asks(Some(3));
1993
1994 assert!(bids.len() <= 3);
1995 assert!(asks.len() <= 3);
1996
1997 assert_eq!(
1999 book.best_bid().unwrap().price,
2000 Decimal::from_str("0.75").unwrap()
2001 );
2002 assert_eq!(
2003 book.best_ask().unwrap().price,
2004 Decimal::from_str("0.76").unwrap()
2005 );
2006 }
2007
2008 #[test]
2009 fn test_fast_operations() {
2010 let mut book = OrderBook::new("test_token".to_string(), 10);
2011
2012 book.apply_bid_delta(
2014 Decimal::from_str("0.75").unwrap(),
2015 Decimal::from_str("100.0").unwrap(),
2016 );
2017 book.apply_ask_delta(
2018 Decimal::from_str("0.76").unwrap(),
2019 Decimal::from_str("80.0").unwrap(),
2020 );
2021
2022 let best_bid_fast = book.best_bid_fast();
2023 let best_ask_fast = book.best_ask_fast();
2024
2025 assert!(best_bid_fast.is_some());
2026 assert!(best_ask_fast.is_some());
2027
2028 let spread_fast = book.spread_fast();
2030 let mid_fast = book.mid_price_fast();
2031
2032 assert!(spread_fast.is_some()); assert!(mid_fast.is_some()); }
2035}