1use std::{
19 cmp::Ordering,
20 collections::{BTreeMap, HashMap},
21 fmt::{Debug, Display},
22};
23
24use nautilus_core::UnixNanos;
25
26use crate::{
27 data::order::{BookOrder, OrderId},
28 enums::{BookType, OrderSideSpecified, RecordFlag},
29 orderbook::BookLevel,
30 types::{Price, Quantity},
31};
32
33#[derive(Clone, Copy, Debug, Eq)]
45#[cfg_attr(
46 feature = "python",
47 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
48)]
49pub struct BookPrice {
50 pub value: Price,
51 pub side: OrderSideSpecified,
52}
53
54impl BookPrice {
55 #[must_use]
57 pub fn new(value: Price, side: OrderSideSpecified) -> Self {
58 Self { value, side }
59 }
60}
61
62impl PartialOrd for BookPrice {
63 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
64 Some(self.cmp(other))
65 }
66}
67
68impl PartialEq for BookPrice {
69 fn eq(&self, other: &Self) -> bool {
70 self.side == other.side && self.value == other.value
71 }
72}
73
74impl Ord for BookPrice {
75 fn cmp(&self, other: &Self) -> Ordering {
76 assert_eq!(
77 self.side, other.side,
78 "BookPrice compared across sides: {:?} vs {:?}",
79 self.side, other.side
80 );
81
82 match self.side.cmp(&other.side) {
83 Ordering::Equal => match self.side {
84 OrderSideSpecified::Buy => other.value.cmp(&self.value),
85 OrderSideSpecified::Sell => self.value.cmp(&other.value),
86 },
87 non_equal => non_equal,
88 }
89 }
90}
91
92impl Display for BookPrice {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 write!(f, "{}", self.value)
95 }
96}
97
98#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
105enum L1BatchState {
106 #[default]
108 None,
109 MbpBatch,
111 SnapshotBatch,
113}
114
115#[derive(Clone, Debug)]
117pub(crate) struct BookLadder {
118 pub side: OrderSideSpecified,
119 pub book_type: BookType,
120 pub levels: BTreeMap<BookPrice, BookLevel>,
121 pub cache: HashMap<u64, BookPrice>,
122 batch_state: L1BatchState,
123}
124
125impl BookLadder {
126 #[must_use]
128 pub fn new(side: OrderSideSpecified, book_type: BookType) -> Self {
129 Self {
130 side,
131 book_type,
132 levels: BTreeMap::new(),
133 cache: HashMap::new(),
134 batch_state: L1BatchState::None,
135 }
136 }
137
138 #[must_use]
140 pub fn len(&self) -> usize {
141 self.levels.len()
142 }
143
144 #[must_use]
146 #[allow(dead_code)]
147 pub fn is_empty(&self) -> bool {
148 self.levels.is_empty()
149 }
150
151 pub fn clear(&mut self) {
155 self.levels.clear();
156 self.cache.clear();
157 self.batch_state = L1BatchState::None;
158 }
159
160 pub fn add(&mut self, order: BookOrder, flags: u8) {
168 if self.book_type == BookType::L1_MBP && !self.handle_l1_add(&order, flags) {
169 return;
170 }
171
172 if self.book_type != BookType::L1_MBP && !order.size.is_positive() {
173 log::warn!(
174 "Attempted to add order with non-positive size: order_id={}, size={}, ignoring",
175 order.order_id,
176 order.size,
177 );
178 return;
179 }
180
181 let book_price = order.to_book_price();
182 self.cache.insert(order.order_id, book_price);
183
184 if let Some(level) = self.levels.get_mut(&book_price) {
185 level.add(order);
186 } else {
187 let level = BookLevel::from_order(order);
188 self.levels.insert(book_price, level);
189 }
190
191 let is_batch = RecordFlag::F_MBP.matches(flags) || RecordFlag::F_SNAPSHOT.matches(flags);
194 if self.book_type == BookType::L1_MBP && is_batch {
195 self.retain_best_only();
196
197 if RecordFlag::F_LAST.matches(flags) {
198 self.batch_state = L1BatchState::None;
199 }
200 }
201 }
202
203 fn handle_l1_add(&mut self, order: &BookOrder, flags: u8) -> bool {
219 if !order.size.is_positive() {
220 self.clear();
221 let side = self.side;
222 log::debug!("L1 zero-size add cleared ladder: side={side:?}");
223 return false;
224 }
225
226 let is_mbp = RecordFlag::F_MBP.matches(flags);
227 let is_snapshot = RecordFlag::F_SNAPSHOT.matches(flags);
228 let is_last = RecordFlag::F_LAST.matches(flags);
229
230 if is_snapshot && is_last {
231 if self.batch_state != L1BatchState::SnapshotBatch {
235 self.clear();
236 }
237 } else if is_snapshot {
238 if self.batch_state != L1BatchState::SnapshotBatch {
240 self.clear();
241 self.batch_state = L1BatchState::SnapshotBatch;
242 }
243 } else if is_mbp && is_last {
244 if self.batch_state != L1BatchState::MbpBatch {
246 self.clear();
247 }
248 } else if is_mbp {
249 self.clear();
251 self.batch_state = L1BatchState::MbpBatch;
252 } else {
253 self.clear();
255 }
256
257 true
258 }
259
260 pub fn update(&mut self, order: BookOrder, flags: u8) {
262 let price = self.cache.get(&order.order_id).copied();
263 if let Some(price) = price
264 && let Some(level) = self.levels.get_mut(&price)
265 {
266 if order.price == level.price.value {
267 let level_len_before = level.len();
268 level.update(order);
269
270 if order.size.raw == 0 {
272 self.cache.remove(&order.order_id);
273 debug_assert_eq!(
274 level.len(),
275 level_len_before - 1,
276 "Level should have one less order after zero-size update"
277 );
278 } else {
279 debug_assert!(
280 self.cache.contains_key(&order.order_id),
281 "Cache should still contain order {0} after update",
282 order.order_id
283 );
284 }
285
286 if level.is_empty() {
287 self.levels.remove(&price);
288 debug_assert!(
289 !self.cache.values().any(|p| *p == price),
290 "Cache should not contain removed price level {price:?}"
291 );
292 }
293
294 debug_assert_eq!(
295 self.cache.len(),
296 self.levels.values().map(|level| level.len()).sum::<usize>(),
297 "Cache size should equal total orders across all levels"
298 );
299 return;
300 }
301
302 self.cache.remove(&order.order_id);
304 level.delete(&order);
305
306 if level.is_empty() {
307 self.levels.remove(&price);
308 debug_assert!(
309 !self.cache.values().any(|p| *p == price),
310 "Cache should not contain removed price level {price:?}"
311 );
312 }
313 }
314
315 if order.size.is_positive() {
317 self.add(order, flags);
318 }
319
320 debug_assert_eq!(
322 self.cache.len(),
323 self.levels.values().map(|level| level.len()).sum::<usize>(),
324 "Cache size should equal total orders across all levels"
325 );
326 }
327
328 pub fn delete(&mut self, order: BookOrder, sequence: u64, ts_event: UnixNanos) {
330 self.remove_order(order.order_id, sequence, ts_event);
331 }
332
333 pub fn remove_order(&mut self, order_id: OrderId, sequence: u64, ts_event: UnixNanos) {
335 if let Some(price) = self.cache.get(&order_id).copied()
336 && let Some(level) = self.levels.get_mut(&price)
337 {
338 if level.orders.contains_key(&order_id) {
340 let level_len_before = level.len();
341
342 self.cache.remove(&order_id);
344 level.remove_by_id(order_id, sequence, ts_event);
345
346 debug_assert_eq!(
347 level.len(),
348 level_len_before - 1,
349 "Level should have exactly one less order after removal"
350 );
351
352 if level.is_empty() {
353 self.levels.remove(&price);
354 debug_assert!(
355 !self.cache.values().any(|p| *p == price),
356 "Cache should not contain removed price level {price:?}"
357 );
358 }
359 }
360 }
361
362 debug_assert_eq!(
364 self.cache.len(),
365 self.levels.values().map(|level| level.len()).sum::<usize>(),
366 "Cache size should equal total orders across all levels"
367 );
368 }
369
370 pub fn remove_level(&mut self, price: BookPrice) -> Option<BookLevel> {
372 if let Some(level) = self.levels.remove(&price) {
373 for order_id in level.orders.keys() {
375 self.cache.remove(order_id);
376 }
377
378 debug_assert_eq!(
379 self.cache.len(),
380 self.levels.values().map(|level| level.len()).sum::<usize>(),
381 "Cache size should equal total orders across all levels"
382 );
383
384 Some(level)
385 } else {
386 None
387 }
388 }
389
390 fn retain_best_only(&mut self) {
396 if self.levels.len() <= 1 {
397 return;
398 }
399
400 let best_price = match self.levels.keys().next().copied() {
401 Some(price) => price,
402 None => return,
403 };
404
405 self.levels.retain(|price, _| *price == best_price);
408
409 self.cache.clear();
412
413 for (book_price, level) in &self.levels {
414 for order_id in level.orders.keys() {
415 self.cache.insert(*order_id, *book_price);
416 }
417 }
418
419 debug_assert!(
420 self.levels.len() <= 1,
421 "L1 ladder should have at most 1 level after retain_best_only"
422 );
423 debug_assert_eq!(
424 self.cache.len(),
425 self.levels.values().map(|l| l.len()).sum::<usize>(),
426 "Cache size should equal total orders across all levels"
427 );
428 }
429
430 #[must_use]
432 #[allow(dead_code)]
433 pub fn sizes(&self) -> f64 {
434 self.levels.values().map(BookLevel::size).sum()
435 }
436
437 #[must_use]
439 #[allow(dead_code)]
440 pub fn exposures(&self) -> f64 {
441 self.levels.values().map(BookLevel::exposure).sum()
442 }
443
444 #[must_use]
446 pub fn top(&self) -> Option<&BookLevel> {
447 match self.levels.iter().next() {
448 Some((_, l)) => Option::Some(l),
449 None => Option::None,
450 }
451 }
452
453 #[must_use]
456 pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
457 let is_reversed = self.side == OrderSideSpecified::Buy;
458 let mut fills = Vec::new();
459 let mut cumulative_denominator = Quantity::zero(order.size.precision);
460 let target = order.size;
461
462 for level in self.levels.values() {
463 if (is_reversed && level.price.value < order.price)
464 || (!is_reversed && level.price.value > order.price)
465 {
466 break;
467 }
468
469 for book_order in level.orders.values() {
470 let current = book_order.size;
471 if cumulative_denominator + current >= target {
472 let remainder = target - cumulative_denominator;
474 if remainder.is_positive() {
475 fills.push((book_order.price, remainder));
476 }
477 return fills;
478 }
479
480 fills.push((book_order.price, current));
482 cumulative_denominator = cumulative_denominator + current;
483 }
484 }
485
486 fills
487 }
488}
489
490impl Display for BookLadder {
491 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492 writeln!(f, "{}(side={})", stringify!(BookLadder), self.side)?;
493 for (price, level) in &self.levels {
494 writeln!(f, " {} -> {} orders", price, level.len())?;
495 }
496 Ok(())
497 }
498}
499
500#[cfg(test)]
501impl BookLadder {
502 pub fn add_bulk(&mut self, orders: &[BookOrder]) {
504 for order in orders {
505 self.add(*order, 0);
506 }
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use rstest::rstest;
513
514 use crate::{
515 data::order::BookOrder,
516 enums::{BookType, OrderSide, OrderSideSpecified, RecordFlag},
517 orderbook::ladder::{BookLadder, BookPrice},
518 types::{Price, Quantity},
519 };
520
521 #[rstest]
522 fn test_is_empty() {
523 let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
524 assert!(ladder.is_empty(), "A new ladder should be empty");
525 }
526
527 #[rstest]
528 fn test_is_empty_after_add() {
529 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
530 assert!(ladder.is_empty(), "Ladder should start empty");
531 let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(100), 1);
532 ladder.add(order, 0);
533 assert!(
534 !ladder.is_empty(),
535 "Ladder should not be empty after adding an order"
536 );
537 }
538
539 #[rstest]
540 fn test_add_bulk_empty() {
541 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
542 ladder.add_bulk(&[]);
543 assert!(
544 ladder.is_empty(),
545 "Adding an empty vector should leave the ladder empty"
546 );
547 }
548
549 #[rstest]
550 fn test_add_bulk_orders() {
551 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
552 let orders = [
553 BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1),
554 BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2),
555 BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(50), 3),
556 ];
557 ladder.add_bulk(&orders);
558 assert_eq!(ladder.len(), 1, "Ladder should have one price level");
560 let orders_in_level = ladder.top().unwrap().get_orders();
561 assert_eq!(
562 orders_in_level.len(),
563 3,
564 "Price level should contain all bulk orders"
565 );
566 }
567
568 #[rstest]
569 fn test_book_price_bid_sorting() {
570 let mut bid_prices = [
571 BookPrice::new(Price::from("2.0"), OrderSideSpecified::Buy),
572 BookPrice::new(Price::from("4.0"), OrderSideSpecified::Buy),
573 BookPrice::new(Price::from("1.0"), OrderSideSpecified::Buy),
574 BookPrice::new(Price::from("3.0"), OrderSideSpecified::Buy),
575 ];
576 bid_prices.sort();
577 assert_eq!(bid_prices[0].value, Price::from("4.0"));
578 }
579
580 #[rstest]
581 fn test_book_price_ask_sorting() {
582 let mut ask_prices = [
583 BookPrice::new(Price::from("2.0"), OrderSideSpecified::Sell),
584 BookPrice::new(Price::from("4.0"), OrderSideSpecified::Sell),
585 BookPrice::new(Price::from("1.0"), OrderSideSpecified::Sell),
586 BookPrice::new(Price::from("3.0"), OrderSideSpecified::Sell),
587 ];
588
589 ask_prices.sort();
590 assert_eq!(ask_prices[0].value, Price::from("1.0"));
591 }
592
593 #[rstest]
594 fn test_add_single_order() {
595 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
596 let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 0);
597
598 ladder.add(order, 0);
599 assert_eq!(ladder.len(), 1);
600 assert_eq!(ladder.sizes(), 20.0);
601 assert_eq!(ladder.exposures(), 200.0);
602 assert_eq!(ladder.top().unwrap().price.value, Price::from("10.0"));
603 }
604
605 #[rstest]
606 fn test_add_multiple_buy_orders() {
607 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
608 let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 0);
609 let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(30), 1);
610 let order3 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(50), 2);
611 let order4 = BookOrder::new(OrderSide::Buy, Price::from("8.00"), Quantity::from(200), 3);
612
613 ladder.add_bulk(&[order1, order2, order3, order4]);
614 assert_eq!(ladder.len(), 3);
615 assert_eq!(ladder.sizes(), 300.0);
616 assert_eq!(ladder.exposures(), 2520.0);
617 assert_eq!(ladder.top().unwrap().price.value, Price::from("10.0"));
618 }
619
620 #[rstest]
621 fn test_add_multiple_sell_orders() {
622 let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
623 let order1 = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(20), 0);
624 let order2 = BookOrder::new(OrderSide::Sell, Price::from("12.00"), Quantity::from(30), 1);
625 let order3 = BookOrder::new(OrderSide::Sell, Price::from("12.00"), Quantity::from(50), 2);
626 let order4 = BookOrder::new(
627 OrderSide::Sell,
628 Price::from("13.00"),
629 Quantity::from(200),
630 0,
631 );
632
633 ladder.add_bulk(&[order1, order2, order3, order4]);
634 assert_eq!(ladder.len(), 3);
635 assert_eq!(ladder.sizes(), 300.0);
636 assert_eq!(ladder.exposures(), 3780.0);
637 assert_eq!(ladder.top().unwrap().price.value, Price::from("11.0"));
638 }
639
640 #[rstest]
641 fn test_add_to_same_price_level() {
642 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
643 let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
644 let order2 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2);
645
646 ladder.add(order1, 0);
647 ladder.add(order2, 0);
648
649 assert_eq!(ladder.len(), 1);
650 assert_eq!(ladder.sizes(), 50.0);
651 assert_eq!(ladder.exposures(), 500.0);
652 }
653
654 #[rstest]
655 fn test_add_descending_buy_orders() {
656 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
657 let order1 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(20), 1);
658 let order2 = BookOrder::new(OrderSide::Buy, Price::from("8.00"), Quantity::from(30), 2);
659
660 ladder.add(order1, 0);
661 ladder.add(order2, 0);
662
663 assert_eq!(ladder.top().unwrap().price.value, Price::from("9.00"));
664 }
665
666 #[rstest]
667 fn test_add_ascending_sell_orders() {
668 let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
669 let order1 = BookOrder::new(OrderSide::Sell, Price::from("8.00"), Quantity::from(20), 1);
670 let order2 = BookOrder::new(OrderSide::Sell, Price::from("9.00"), Quantity::from(30), 2);
671
672 ladder.add(order1, 0);
673 ladder.add(order2, 0);
674
675 assert_eq!(ladder.top().unwrap().price.value, Price::from("8.00"));
676 }
677
678 #[rstest]
679 fn test_update_buy_order_price() {
680 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
681 let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(20), 1);
682
683 ladder.add(order, 0);
684 let order = BookOrder::new(OrderSide::Buy, Price::from("11.10"), Quantity::from(20), 1);
685
686 ladder.update(order, 0);
687 assert_eq!(ladder.len(), 1);
688 assert_eq!(ladder.sizes(), 20.0);
689 assert_eq!(ladder.exposures(), 222.0);
690 assert_eq!(ladder.top().unwrap().price.value, Price::from("11.1"));
691 }
692
693 #[rstest]
694 fn test_update_sell_order_price() {
695 let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
696 let order = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(20), 1);
697
698 ladder.add(order, 0);
699
700 let order = BookOrder::new(OrderSide::Sell, Price::from("11.10"), Quantity::from(20), 1);
701
702 ladder.update(order, 0);
703 assert_eq!(ladder.len(), 1);
704 assert_eq!(ladder.sizes(), 20.0);
705 assert_eq!(ladder.exposures(), 222.0);
706 assert_eq!(ladder.top().unwrap().price.value, Price::from("11.1"));
707 }
708
709 #[rstest]
710 fn test_update_buy_order_size() {
711 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
712 let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(20), 1);
713
714 ladder.add(order, 0);
715
716 let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(10), 1);
717
718 ladder.update(order, 0);
719 assert_eq!(ladder.len(), 1);
720 assert_eq!(ladder.sizes(), 10.0);
721 assert_eq!(ladder.exposures(), 110.0);
722 assert_eq!(ladder.top().unwrap().price.value, Price::from("11.0"));
723 }
724
725 #[rstest]
726 fn test_update_sell_order_size() {
727 let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
728 let order = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(20), 1);
729
730 ladder.add(order, 0);
731
732 let order = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(10), 1);
733
734 ladder.update(order, 0);
735 assert_eq!(ladder.len(), 1);
736 assert_eq!(ladder.sizes(), 10.0);
737 assert_eq!(ladder.exposures(), 110.0);
738 assert_eq!(ladder.top().unwrap().price.value, Price::from("11.0"));
739 }
740
741 #[rstest]
742 fn test_delete_non_existing_order() {
743 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
744 let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
745
746 ladder.delete(order, 0, 0.into());
747
748 assert_eq!(ladder.len(), 0);
749 }
750
751 #[rstest]
752 fn test_delete_buy_order() {
753 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
754 let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(20), 1);
755
756 ladder.add(order, 0);
757
758 let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(10), 1);
759
760 ladder.delete(order, 0, 0.into());
761 assert_eq!(ladder.len(), 0);
762 assert_eq!(ladder.sizes(), 0.0);
763 assert_eq!(ladder.exposures(), 0.0);
764 assert_eq!(ladder.top(), None);
765 }
766
767 #[rstest]
768 fn test_delete_sell_order() {
769 let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
770 let order = BookOrder::new(OrderSide::Sell, Price::from("10.00"), Quantity::from(10), 1);
771
772 ladder.add(order, 0);
773
774 let order = BookOrder::new(OrderSide::Sell, Price::from("10.00"), Quantity::from(10), 1);
775
776 ladder.delete(order, 0, 0.into());
777 assert_eq!(ladder.len(), 0);
778 assert_eq!(ladder.sizes(), 0.0);
779 assert_eq!(ladder.exposures(), 0.0);
780 assert_eq!(ladder.top(), None);
781 }
782
783 #[rstest]
784 fn test_ladder_sizes_empty() {
785 let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
786 assert_eq!(
787 ladder.sizes(),
788 0.0,
789 "An empty ladder should have total size 0.0"
790 );
791 }
792
793 #[rstest]
794 fn test_ladder_exposures_empty() {
795 let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
796 assert_eq!(
797 ladder.exposures(),
798 0.0,
799 "An empty ladder should have total exposure 0.0"
800 );
801 }
802
803 #[rstest]
804 fn test_ladder_sizes() {
805 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
806 let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
807 let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.50"), Quantity::from(30), 2);
808 ladder.add(order1, 0);
809 ladder.add(order2, 0);
810
811 let expected_size = 20.0 + 30.0;
812 assert_eq!(
813 ladder.sizes(),
814 expected_size,
815 "Ladder total size should match the sum of order sizes"
816 );
817 }
818
819 #[rstest]
820 fn test_ladder_exposures() {
821 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
822 let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
823 let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.50"), Quantity::from(30), 2);
824 ladder.add(order1, 0);
825 ladder.add(order2, 0);
826
827 let expected_exposure = 10.00 * 20.0 + 9.50 * 30.0;
828 assert_eq!(
829 ladder.exposures(),
830 expected_exposure,
831 "Ladder total exposure should match the sum of individual exposures"
832 );
833 }
834
835 #[rstest]
836 fn test_iter_returns_fifo() {
837 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
838 let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
839 let order2 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2);
840 ladder.add(order1, 0);
841 ladder.add(order2, 0);
842 let orders: Vec<BookOrder> = ladder.top().unwrap().iter().copied().collect();
843 assert_eq!(
844 orders,
845 vec![order1, order2],
846 "Iterator should return orders in FIFO order"
847 );
848 }
849
850 #[rstest]
851 fn test_update_missing_order_inserts() {
852 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
853 let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
854 ladder.update(order, 0);
856 assert_eq!(
857 ladder.len(),
858 1,
859 "Ladder should have one level after upsert update"
860 );
861 let orders = ladder.top().unwrap().get_orders();
862 assert_eq!(
863 orders.len(),
864 1,
865 "Price level should contain the inserted order"
866 );
867 assert_eq!(orders[0], order, "The inserted order should match");
868 }
869
870 #[rstest]
871 fn test_cache_consistency_after_operations() {
872 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
873 let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
874 let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(30), 2);
875 ladder.add(order1, 0);
876 ladder.add(order2, 0);
877
878 for (order_id, price) in &ladder.cache {
880 let level = ladder
881 .levels
882 .get(price)
883 .expect("Every price in the cache should have a corresponding level");
884 assert!(
885 level.orders.contains_key(order_id),
886 "Order id {order_id} should be present in the level for price {price}",
887 );
888 }
889 }
890
891 #[rstest]
892 fn test_simulate_fills_with_empty_book() {
893 let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
894 let order = BookOrder::new(OrderSide::Buy, Price::max(2), Quantity::from(500), 1);
895
896 let fills = ladder.simulate_fills(&order);
897
898 assert!(fills.is_empty());
899 }
900
901 #[rstest]
902 #[case(OrderSide::Buy, Price::max(2), OrderSideSpecified::Sell)]
903 #[case(OrderSide::Sell, Price::min(2), OrderSideSpecified::Buy)]
904 fn test_simulate_order_fills_with_no_size(
905 #[case] side: OrderSide,
906 #[case] price: Price,
907 #[case] ladder_side: OrderSideSpecified,
908 ) {
909 let ladder = BookLadder::new(ladder_side, BookType::L3_MBO);
910 let order = BookOrder {
911 price, size: Quantity::from(500),
913 side,
914 order_id: 2,
915 };
916
917 let fills = ladder.simulate_fills(&order);
918
919 assert!(fills.is_empty());
920 }
921
922 #[rstest]
923 #[case(OrderSide::Buy, OrderSideSpecified::Sell, Price::from("60.0"))]
924 #[case(OrderSide::Sell, OrderSideSpecified::Buy, Price::from("40.0"))]
925 fn test_simulate_order_fills_buy_when_far_from_market(
926 #[case] order_side: OrderSide,
927 #[case] ladder_side: OrderSideSpecified,
928 #[case] ladder_price: Price,
929 ) {
930 let mut ladder = BookLadder::new(ladder_side, BookType::L3_MBO);
931
932 ladder.add(
933 BookOrder {
934 price: ladder_price,
935 size: Quantity::from(100),
936 side: ladder_side.as_order_side(),
937 order_id: 1,
938 },
939 0,
940 );
941
942 let order = BookOrder {
943 price: Price::from("50.00"),
944 size: Quantity::from(500),
945 side: order_side,
946 order_id: 2,
947 };
948
949 let fills = ladder.simulate_fills(&order);
950
951 assert!(fills.is_empty());
952 }
953
954 #[rstest]
955 fn test_simulate_order_fills_sell_when_far_from_market() {
956 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
957
958 ladder.add(
959 BookOrder {
960 price: Price::from("100.00"),
961 size: Quantity::from(100),
962 side: OrderSide::Buy,
963 order_id: 1,
964 },
965 0,
966 );
967
968 let order = BookOrder {
969 price: Price::from("150.00"), size: Quantity::from(500),
971 side: OrderSide::Buy,
972 order_id: 2,
973 };
974
975 let fills = ladder.simulate_fills(&order);
976
977 assert!(fills.is_empty());
978 }
979
980 #[rstest]
981 fn test_simulate_order_fills_buy() {
982 let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
983
984 ladder.add_bulk(&[
985 BookOrder {
986 price: Price::from("100.00"),
987 size: Quantity::from(100),
988 side: OrderSide::Sell,
989 order_id: 1,
990 },
991 BookOrder {
992 price: Price::from("101.00"),
993 size: Quantity::from(200),
994 side: OrderSide::Sell,
995 order_id: 2,
996 },
997 BookOrder {
998 price: Price::from("102.00"),
999 size: Quantity::from(400),
1000 side: OrderSide::Sell,
1001 order_id: 3,
1002 },
1003 ]);
1004
1005 let order = BookOrder {
1006 price: Price::max(2), size: Quantity::from(500),
1008 side: OrderSide::Buy,
1009 order_id: 4,
1010 };
1011
1012 let fills = ladder.simulate_fills(&order);
1013
1014 assert_eq!(fills.len(), 3);
1015
1016 let (price1, size1) = fills[0];
1017 assert_eq!(price1, Price::from("100.00"));
1018 assert_eq!(size1, Quantity::from(100));
1019
1020 let (price2, size2) = fills[1];
1021 assert_eq!(price2, Price::from("101.00"));
1022 assert_eq!(size2, Quantity::from(200));
1023
1024 let (price3, size3) = fills[2];
1025 assert_eq!(price3, Price::from("102.00"));
1026 assert_eq!(size3, Quantity::from(200));
1027 }
1028
1029 #[rstest]
1030 fn test_simulate_order_fills_sell() {
1031 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1032
1033 ladder.add_bulk(&[
1034 BookOrder {
1035 price: Price::from("102.00"),
1036 size: Quantity::from(100),
1037 side: OrderSide::Buy,
1038 order_id: 1,
1039 },
1040 BookOrder {
1041 price: Price::from("101.00"),
1042 size: Quantity::from(200),
1043 side: OrderSide::Buy,
1044 order_id: 2,
1045 },
1046 BookOrder {
1047 price: Price::from("100.00"),
1048 size: Quantity::from(400),
1049 side: OrderSide::Buy,
1050 order_id: 3,
1051 },
1052 ]);
1053
1054 let order = BookOrder {
1055 price: Price::min(2), size: Quantity::from(500),
1057 side: OrderSide::Sell,
1058 order_id: 4,
1059 };
1060
1061 let fills = ladder.simulate_fills(&order);
1062
1063 assert_eq!(fills.len(), 3);
1064
1065 let (price1, size1) = fills[0];
1066 assert_eq!(price1, Price::from("102.00"));
1067 assert_eq!(size1, Quantity::from(100));
1068
1069 let (price2, size2) = fills[1];
1070 assert_eq!(price2, Price::from("101.00"));
1071 assert_eq!(size2, Quantity::from(200));
1072
1073 let (price3, size3) = fills[2];
1074 assert_eq!(price3, Price::from("100.00"));
1075 assert_eq!(size3, Quantity::from(200));
1076 }
1077
1078 #[rstest]
1079 fn test_simulate_order_fills_sell_with_size_at_limit_of_precision() {
1080 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1081
1082 ladder.add_bulk(&[
1083 BookOrder {
1084 price: Price::from("102.00"),
1085 size: Quantity::from("100.000000000"),
1086 side: OrderSide::Buy,
1087 order_id: 1,
1088 },
1089 BookOrder {
1090 price: Price::from("101.00"),
1091 size: Quantity::from("200.000000000"),
1092 side: OrderSide::Buy,
1093 order_id: 2,
1094 },
1095 BookOrder {
1096 price: Price::from("100.00"),
1097 size: Quantity::from("400.000000000"),
1098 side: OrderSide::Buy,
1099 order_id: 3,
1100 },
1101 ]);
1102
1103 let order = BookOrder {
1104 price: Price::min(2), size: Quantity::from("699.999999999"), side: OrderSide::Sell,
1107 order_id: 4,
1108 };
1109
1110 let fills = ladder.simulate_fills(&order);
1111
1112 assert_eq!(fills.len(), 3);
1113
1114 let (price1, size1) = fills[0];
1115 assert_eq!(price1, Price::from("102.00"));
1116 assert_eq!(size1, Quantity::from("100.000000000"));
1117
1118 let (price2, size2) = fills[1];
1119 assert_eq!(price2, Price::from("101.00"));
1120 assert_eq!(size2, Quantity::from("200.000000000"));
1121
1122 let (price3, size3) = fills[2];
1123 assert_eq!(price3, Price::from("100.00"));
1124 assert_eq!(size3, Quantity::from("399.999999999"));
1125 }
1126
1127 #[rstest]
1128 fn test_boundary_prices() {
1129 let max_price = Price::max(1);
1130 let min_price = Price::min(1);
1131
1132 let mut ladder_buy = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1133 let mut ladder_sell = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
1134
1135 let order_buy = BookOrder::new(OrderSide::Buy, min_price, Quantity::from(1), 1);
1136 let order_sell = BookOrder::new(OrderSide::Sell, max_price, Quantity::from(1), 1);
1137
1138 ladder_buy.add(order_buy, 0);
1139 ladder_sell.add(order_sell, 0);
1140
1141 assert_eq!(ladder_buy.top().unwrap().price.value, min_price);
1142 assert_eq!(ladder_sell.top().unwrap().price.value, max_price);
1143 }
1144
1145 #[rstest]
1146 fn test_l1_single_delta_batches_replace_each_other() {
1147 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1150 let side_constant = OrderSide::Buy as u64;
1151
1152 let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1154
1155 let order1 = BookOrder {
1157 side: OrderSide::Buy,
1158 price: Price::from("100.00"),
1159 size: Quantity::from(50),
1160 order_id: side_constant,
1161 };
1162 ladder.add(order1, batch_flags);
1163
1164 assert_eq!(ladder.len(), 1, "Should have one level after first add");
1165 assert_eq!(
1166 ladder.top().unwrap().price.value,
1167 Price::from("100.00"),
1168 "Top level should be at 100.00"
1169 );
1170
1171 let order2 = BookOrder {
1172 side: OrderSide::Buy,
1173 price: Price::from("101.00"),
1174 size: Quantity::from(60),
1175 order_id: side_constant,
1176 };
1177 ladder.add(order2, batch_flags);
1178
1179 assert_eq!(ladder.len(), 1, "Should have only one level");
1180 assert_eq!(
1181 ladder.top().unwrap().price.value,
1182 Price::from("101.00"),
1183 "Top level should be at 101.00"
1184 );
1185
1186 let order3 = BookOrder {
1188 side: OrderSide::Buy,
1189 price: Price::from("100.50"),
1190 size: Quantity::from(70),
1191 order_id: side_constant,
1192 };
1193 ladder.add(order3, batch_flags);
1194
1195 assert_eq!(ladder.len(), 1, "Should have only one level");
1196 assert_eq!(
1197 ladder.top().unwrap().price.value,
1198 Price::from("100.50"),
1199 "Top level should be at 100.50 (new batch replaced old)"
1200 );
1201 }
1202
1203 #[rstest]
1204 fn test_l2_orders_not_affected_by_l1_fix() {
1205 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1206
1207 let order1 = BookOrder {
1208 side: OrderSide::Buy,
1209 price: Price::from("100.00"),
1210 size: Quantity::from(50),
1211 order_id: Price::from("100.00").raw as u64,
1212 };
1213 ladder.add(order1, 0);
1214
1215 let order2 = BookOrder {
1216 side: OrderSide::Buy,
1217 price: Price::from("99.00"),
1218 size: Quantity::from(60),
1219 order_id: Price::from("99.00").raw as u64,
1220 };
1221 ladder.add(order2, 0);
1222
1223 assert_eq!(ladder.len(), 2, "L2 orders should create multiple levels");
1224 assert_eq!(
1225 ladder.top().unwrap().price.value,
1226 Price::from("100.00"),
1227 "Top level should be best bid"
1228 );
1229 }
1230
1231 #[rstest]
1232 fn test_zero_size_l1_order_clears_top() {
1233 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1235 let side_constant = OrderSide::Buy as u64;
1236
1237 let order1 = BookOrder {
1238 side: OrderSide::Buy,
1239 price: Price::from("100.00"),
1240 size: Quantity::from(50),
1241 order_id: side_constant,
1242 };
1243 ladder.add(order1, 0);
1244
1245 assert_eq!(ladder.len(), 1);
1246 assert_eq!(ladder.top().unwrap().price.value, Price::from("100.00"));
1247 assert!(ladder.top().unwrap().first().is_some());
1248
1249 let order2 = BookOrder {
1251 side: OrderSide::Buy,
1252 price: Price::from("101.00"),
1253 size: Quantity::zero(9), order_id: side_constant,
1255 };
1256 ladder.add(order2, 0);
1257
1258 assert_eq!(ladder.len(), 0, "Zero-size L1 add should clear the book");
1260 assert!(ladder.top().is_none(), "Book should be empty after clear");
1261
1262 assert!(
1264 ladder.cache.is_empty(),
1265 "Cache should be empty after L1 clear"
1266 );
1267 }
1268
1269 #[rstest]
1270 fn test_zero_size_order_to_empty_ladder() {
1271 let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L1_MBP);
1273 let side_constant = OrderSide::Sell as u64;
1274
1275 let order = BookOrder {
1276 side: OrderSide::Sell,
1277 price: Price::from("100.00"),
1278 size: Quantity::zero(9),
1279 order_id: side_constant,
1280 };
1281 ladder.add(order, 0);
1282
1283 assert_eq!(ladder.len(), 0, "Empty ladder should remain empty");
1284 assert!(ladder.top().is_none(), "Top should be None");
1285 assert!(
1286 ladder.cache.is_empty(),
1287 "Cache should remain empty for zero-size add"
1288 );
1289 }
1290
1291 #[rstest]
1292 fn test_l3_order_id_collision_no_ghost_levels() {
1293 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1296
1297 let order1 = BookOrder {
1299 side: OrderSide::Buy,
1300 price: Price::from("100.00"),
1301 size: Quantity::from(50),
1302 order_id: 1, };
1304 ladder.add(order1, 0);
1305
1306 assert_eq!(ladder.len(), 1);
1307
1308 let order2 = BookOrder {
1311 side: OrderSide::Buy,
1312 price: Price::from("99.00"),
1313 size: Quantity::from(60),
1314 order_id: 1, };
1316 ladder.add(order2, 0);
1317
1318 assert_eq!(
1320 ladder.len(),
1321 2,
1322 "L3 should allow order ID 1 at multiple price levels"
1323 );
1324
1325 let prices: Vec<Price> = ladder.levels.keys().map(|bp| bp.value).collect();
1326 assert!(
1327 prices.contains(&Price::from("100.00")),
1328 "Level at 100.00 should still exist"
1329 );
1330 assert!(
1331 prices.contains(&Price::from("99.00")),
1332 "Level at 99.00 should exist"
1333 );
1334 }
1335
1336 #[rstest]
1337 fn test_l1_vs_l3_different_behavior_same_order_id() {
1338 let mut l1_ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1342 let side_constant = OrderSide::Buy as u64;
1343
1344 let order1 = BookOrder {
1345 side: OrderSide::Buy,
1346 price: Price::from("100.00"),
1347 size: Quantity::from(50),
1348 order_id: side_constant,
1349 };
1350 l1_ladder.add(order1, 0);
1351
1352 let order2 = BookOrder {
1353 side: OrderSide::Buy,
1354 price: Price::from("101.00"),
1355 size: Quantity::from(60),
1356 order_id: side_constant, };
1358 l1_ladder.add(order2, 0);
1359
1360 assert_eq!(l1_ladder.len(), 1, "L1 should have only 1 level");
1361 assert_eq!(
1362 l1_ladder.top().unwrap().price.value,
1363 Price::from("101.00"),
1364 "L1 should have replaced the old level"
1365 );
1366
1367 let mut l3_ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1369
1370 let order3 = BookOrder {
1371 side: OrderSide::Buy,
1372 price: Price::from("100.00"),
1373 size: Quantity::from(50),
1374 order_id: 1, };
1376 l3_ladder.add(order3, 0);
1377
1378 let order4 = BookOrder {
1379 side: OrderSide::Buy,
1380 price: Price::from("101.00"),
1381 size: Quantity::from(60),
1382 order_id: 1, };
1384 l3_ladder.add(order4, 0);
1385
1386 assert_eq!(l3_ladder.len(), 2, "L3 should have 2 levels");
1387 }
1388
1389 #[rstest]
1390 #[case::bids_worst_to_best(OrderSideSpecified::Buy, OrderSide::Buy, &["99.00", "100.00", "101.00", "102.00"], "102.00")]
1391 #[case::bids_best_to_worst(OrderSideSpecified::Buy, OrderSide::Buy, &["102.00", "101.00", "100.00", "99.00"], "100.00")]
1392 #[case::asks_worst_to_best(OrderSideSpecified::Sell, OrderSide::Sell, &["105.00", "104.00", "103.00", "102.00"], "102.00")]
1393 #[case::asks_best_to_worst(OrderSideSpecified::Sell, OrderSide::Sell, &["102.00", "103.00", "104.00", "105.00"], "104.00")]
1394 fn test_l1_multi_delta_batch_keeps_best_of_final_two(
1395 #[case] side_spec: OrderSideSpecified,
1396 #[case] side: OrderSide,
1397 #[case] prices: &[&str],
1398 #[case] expected_best: &str,
1399 ) {
1400 let mut ladder = BookLadder::new(side_spec, BookType::L1_MBP);
1403
1404 let batch_size = prices.len();
1405 for (i, price_str) in prices.iter().enumerate() {
1406 let order = BookOrder {
1407 side,
1408 price: Price::from(*price_str),
1409 size: Quantity::from((i + 1) as u64 * 10),
1410 order_id: (i + 100) as u64,
1411 };
1412 let flags = if i == batch_size - 1 {
1413 RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1414 } else {
1415 RecordFlag::F_MBP as u8
1416 };
1417 ladder.add(order, flags);
1418 }
1419
1420 assert_eq!(ladder.len(), 1, "L1 should have only 1 level");
1421 assert_eq!(
1422 ladder.top().unwrap().price.value,
1423 Price::from(expected_best),
1424 "Should keep best of final two deltas"
1425 );
1426 }
1427
1428 #[rstest]
1429 fn test_l1_retain_best_only_cache_consistency() {
1430 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1432 let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1433 let prices = ["100.00", "101.00", "102.00", "103.00", "104.00"];
1434
1435 for (i, price_str) in prices.iter().enumerate() {
1436 let order = BookOrder {
1437 side: OrderSide::Buy,
1438 price: Price::from(*price_str),
1439 size: Quantity::from(10),
1440 order_id: (i + 1) as u64,
1441 };
1442 ladder.add(order, batch_flags);
1443 }
1444
1445 assert_eq!(ladder.len(), 1);
1446 assert_eq!(
1447 ladder.cache.len(),
1448 1,
1449 "Cache should have exactly 1 entry for L1"
1450 );
1451
1452 let total_orders: usize = ladder.levels.values().map(|l| l.len()).sum();
1453 assert_eq!(
1454 ladder.cache.len(),
1455 total_orders,
1456 "Cache should be consistent with levels"
1457 );
1458 }
1459
1460 #[rstest]
1461 fn test_l1_sequential_replacement_allows_price_degradation() {
1462 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1465 let side_constant = OrderSide::Buy as u64;
1466
1467 let order1 = BookOrder {
1469 side: OrderSide::Buy,
1470 price: Price::from("101.00"),
1471 size: Quantity::from(50),
1472 order_id: side_constant,
1473 };
1474 ladder.add(order1, 0); assert_eq!(ladder.len(), 1);
1477 assert_eq!(
1478 ladder.top().unwrap().price.value,
1479 Price::from("101.00"),
1480 "Should have bid at 101.00"
1481 );
1482
1483 let order2 = BookOrder {
1486 side: OrderSide::Buy,
1487 price: Price::from("100.00"),
1488 size: Quantity::from(60),
1489 order_id: side_constant,
1490 };
1491 ladder.add(order2, 0); assert_eq!(ladder.len(), 1);
1494 assert_eq!(
1495 ladder.top().unwrap().price.value,
1496 Price::from("100.00"),
1497 "Sequential replacement should allow price to degrade from 101 to 100"
1498 );
1499
1500 assert_eq!(
1502 ladder.top().unwrap().first().unwrap().size,
1503 Quantity::from(60),
1504 "Size should be from the new order"
1505 );
1506 }
1507
1508 #[rstest]
1509 #[case::bids(OrderSideSpecified::Buy, OrderSide::Buy, &["100.00", "101.00", "102.00"], "102.00", &["97.00", "98.00", "99.00"], "99.00")]
1510 #[case::asks(OrderSideSpecified::Sell, OrderSide::Sell, &["100.00", "101.00", "102.00"], "101.00", &["103.00", "104.00", "105.00"], "104.00")]
1511 fn test_l1_consecutive_batches_clear_between(
1512 #[case] side_spec: OrderSideSpecified,
1513 #[case] side: OrderSide,
1514 #[case] batch1_prices: &[&str],
1515 #[case] expected1: &str,
1516 #[case] batch2_prices: &[&str],
1517 #[case] expected2: &str,
1518 ) {
1519 let mut ladder = BookLadder::new(side_spec, BookType::L1_MBP);
1521
1522 for (i, price_str) in batch1_prices.iter().enumerate() {
1524 let order = BookOrder {
1525 side,
1526 price: Price::from(*price_str),
1527 size: Quantity::from(10),
1528 order_id: (i + 100) as u64,
1529 };
1530 let flags = if i == batch1_prices.len() - 1 {
1531 RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1532 } else {
1533 RecordFlag::F_MBP as u8
1534 };
1535 ladder.add(order, flags);
1536 }
1537
1538 assert_eq!(ladder.len(), 1);
1539 assert_eq!(
1540 ladder.top().unwrap().price.value,
1541 Price::from(expected1),
1542 "After batch 1"
1543 );
1544
1545 for (i, price_str) in batch2_prices.iter().enumerate() {
1547 let order = BookOrder {
1548 side,
1549 price: Price::from(*price_str),
1550 size: Quantity::from(20),
1551 order_id: (i + 200) as u64,
1552 };
1553 let flags = if i == batch2_prices.len() - 1 {
1554 RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1555 } else {
1556 RecordFlag::F_MBP as u8
1557 };
1558 ladder.add(order, flags);
1559 }
1560
1561 assert_eq!(ladder.len(), 1);
1562 assert_eq!(
1563 ladder.top().unwrap().price.value,
1564 Price::from(expected2),
1565 "After batch 2: batch 1 data cleared"
1566 );
1567 }
1568
1569 #[rstest]
1570 fn test_l1_zero_size_clears_regardless_of_order_id() {
1571 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1574
1575 let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1577 let order = BookOrder {
1578 side: OrderSide::Buy,
1579 price: Price::from("100.00"),
1580 size: Quantity::from(50),
1581 order_id: 12345, };
1583 ladder.add(order, batch_flags);
1584 assert_eq!(ladder.len(), 1);
1585
1586 let clear_order = BookOrder {
1588 side: OrderSide::Buy,
1589 price: Price::from("100.00"),
1590 size: Quantity::zero(9),
1591 order_id: OrderSide::Buy as u64, };
1593 ladder.add(clear_order, 0);
1594
1595 assert_eq!(
1597 ladder.len(),
1598 0,
1599 "Zero-size should clear L1 regardless of order_id"
1600 );
1601 assert!(ladder.cache.is_empty(), "Cache should be empty after clear");
1602 }
1603
1604 #[rstest]
1605 fn test_l1_f_mbp_without_f_last_does_not_accumulate() {
1606 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1609 let flags = RecordFlag::F_MBP as u8; let prices = [
1613 "100.00", "99.00", "98.00", "97.00", "96.00", "95.00", "94.00", "93.00", "92.00",
1614 "91.00",
1615 ];
1616
1617 for (i, price_str) in prices.iter().enumerate() {
1618 let order = BookOrder {
1619 side: OrderSide::Buy,
1620 price: Price::from(*price_str),
1621 size: Quantity::from(10),
1622 order_id: (i + 100) as u64,
1623 };
1624 ladder.add(order, flags);
1625
1626 assert_eq!(
1627 ladder.len(),
1628 1,
1629 "L1 should always have at most 1 level, iteration {i}"
1630 );
1631 }
1632
1633 assert_eq!(
1635 ladder.top().unwrap().price.value,
1636 Price::from("91.00"),
1637 "Should show last price (91), allowing degradation"
1638 );
1639 }
1640
1641 #[rstest]
1642 fn test_l1_f_mbp_two_delta_batch_retains_best() {
1643 let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L1_MBP);
1645
1646 let order1 = BookOrder {
1648 side: OrderSide::Sell,
1649 price: Price::from("100.00"),
1650 size: Quantity::from(10),
1651 order_id: 100,
1652 };
1653 ladder.add(order1, RecordFlag::F_MBP as u8);
1654
1655 let order2 = BookOrder {
1658 side: OrderSide::Sell,
1659 price: Price::from("101.00"),
1660 size: Quantity::from(20),
1661 order_id: 101,
1662 };
1663 ladder.add(order2, RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8);
1664
1665 assert_eq!(ladder.len(), 1);
1666 assert_eq!(
1667 ladder.top().unwrap().price.value,
1668 Price::from("100.00"),
1669 "2-delta batch keeps best ask (100) from both deltas"
1670 );
1671 }
1672
1673 #[rstest]
1674 fn test_l1_snapshot_batch_accumulates_all_levels_bids() {
1675 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1677 let prices = ["98.00", "99.00", "100.00", "101.00"];
1678 let batch_size = prices.len();
1679
1680 for (i, price_str) in prices.iter().enumerate() {
1681 let order = BookOrder {
1682 side: OrderSide::Buy,
1683 price: Price::from(*price_str),
1684 size: Quantity::from(10),
1685 order_id: (i + 100) as u64,
1686 };
1687 let flags = if i == batch_size - 1 {
1688 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1689 } else {
1690 RecordFlag::F_SNAPSHOT as u8
1691 };
1692 ladder.add(order, flags);
1693 }
1694
1695 assert_eq!(
1696 ladder.len(),
1697 1,
1698 "L1 should have only 1 level after snapshot"
1699 );
1700 assert_eq!(
1701 ladder.top().unwrap().price.value,
1702 Price::from("101.00"),
1703 "F_SNAPSHOT batch should keep best bid (101) from ALL deltas"
1704 );
1705 }
1706
1707 #[rstest]
1708 fn test_l1_snapshot_batch_accumulates_all_levels_asks() {
1709 let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L1_MBP);
1711 let prices = ["104.00", "103.00", "102.00", "101.00"];
1712 let batch_size = prices.len();
1713
1714 for (i, price_str) in prices.iter().enumerate() {
1715 let order = BookOrder {
1716 side: OrderSide::Sell,
1717 price: Price::from(*price_str),
1718 size: Quantity::from(10),
1719 order_id: (i + 100) as u64,
1720 };
1721 let flags = if i == batch_size - 1 {
1722 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1723 } else {
1724 RecordFlag::F_SNAPSHOT as u8
1725 };
1726 ladder.add(order, flags);
1727 }
1728
1729 assert_eq!(
1730 ladder.len(),
1731 1,
1732 "L1 should have only 1 level after snapshot"
1733 );
1734 assert_eq!(
1735 ladder.top().unwrap().price.value,
1736 Price::from("101.00"),
1737 "F_SNAPSHOT batch should keep best ask (101) from ALL deltas"
1738 );
1739 }
1740
1741 #[rstest]
1742 fn test_l1_snapshot_vs_mbp_different_accumulation_behavior() {
1743 let mut mbp_ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1745 let prices = ["98.00", "99.00", "100.00", "101.00"];
1746 for (i, price_str) in prices.iter().enumerate() {
1747 let order = BookOrder {
1748 side: OrderSide::Buy,
1749 price: Price::from(*price_str),
1750 size: Quantity::from(10),
1751 order_id: (i + 100) as u64,
1752 };
1753 let flags = if i == prices.len() - 1 {
1754 RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1755 } else {
1756 RecordFlag::F_MBP as u8
1757 };
1758 mbp_ladder.add(order, flags);
1759 }
1760 assert_eq!(
1761 mbp_ladder.top().unwrap().price.value,
1762 Price::from("101.00"),
1763 "F_MBP keeps best of final two (100, 101)"
1764 );
1765
1766 let mut snapshot_ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1767
1768 for (i, price_str) in prices.iter().enumerate() {
1769 let order = BookOrder {
1770 side: OrderSide::Buy,
1771 price: Price::from(*price_str),
1772 size: Quantity::from(10),
1773 order_id: (i + 200) as u64,
1774 };
1775 let flags = if i == prices.len() - 1 {
1776 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1777 } else {
1778 RecordFlag::F_SNAPSHOT as u8
1779 };
1780 snapshot_ladder.add(order, flags);
1781 }
1782 assert_eq!(
1783 snapshot_ladder.top().unwrap().price.value,
1784 Price::from("101.00"),
1785 "F_SNAPSHOT keeps best of ALL deltas (98, 99, 100, 101)"
1786 );
1787 }
1788
1789 #[rstest]
1790 fn test_l1_snapshot_after_incomplete_mbp_stream() {
1791 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1793
1794 let stale_order = BookOrder {
1796 side: OrderSide::Buy,
1797 price: Price::from("101.00"),
1798 size: Quantity::from(10),
1799 order_id: 100,
1800 };
1801 ladder.add(stale_order, RecordFlag::F_MBP as u8);
1802 assert_eq!(ladder.top().unwrap().price.value, Price::from("101.00"));
1803
1804 ladder.clear();
1806
1807 for (i, price_str) in ["98.00", "99.00", "100.00"].iter().enumerate() {
1809 let order = BookOrder {
1810 side: OrderSide::Buy,
1811 price: Price::from(*price_str),
1812 size: Quantity::from(10),
1813 order_id: (i + 200) as u64,
1814 };
1815 let flags = if i == 2 {
1816 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1817 } else {
1818 RecordFlag::F_SNAPSHOT as u8
1819 };
1820 ladder.add(order, flags);
1821 }
1822
1823 assert_eq!(
1824 ladder.top().unwrap().price.value,
1825 Price::from("100.00"),
1826 "Snapshot replaces stale MBP state: best is 100, not stale 101"
1827 );
1828 }
1829
1830 #[rstest]
1831 fn test_l1_snapshot_clears_previous_batch() {
1832 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1834
1835 for (i, price_str) in ["100.00", "101.00", "102.00"].iter().enumerate() {
1836 let order = BookOrder {
1837 side: OrderSide::Buy,
1838 price: Price::from(*price_str),
1839 size: Quantity::from(10),
1840 order_id: (i + 100) as u64,
1841 };
1842 let flags = if i == 2 {
1843 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1844 } else {
1845 RecordFlag::F_SNAPSHOT as u8
1846 };
1847 ladder.add(order, flags);
1848 }
1849 assert_eq!(ladder.top().unwrap().price.value, Price::from("102.00"));
1850
1851 for (i, price_str) in ["95.00", "96.00", "97.00"].iter().enumerate() {
1853 let order = BookOrder {
1854 side: OrderSide::Buy,
1855 price: Price::from(*price_str),
1856 size: Quantity::from(20),
1857 order_id: (i + 200) as u64,
1858 };
1859 let flags = if i == 2 {
1860 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1861 } else {
1862 RecordFlag::F_SNAPSHOT as u8
1863 };
1864 ladder.add(order, flags);
1865 }
1866 assert_eq!(
1867 ladder.top().unwrap().price.value,
1868 Price::from("97.00"),
1869 "Second batch clears first: best is 97, not 102"
1870 );
1871 }
1872
1873 #[rstest]
1874 fn test_l1_single_delta_snapshot_after_mbp_batch() {
1875 let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1877
1878 let mbp_order1 = BookOrder {
1879 side: OrderSide::Buy,
1880 price: Price::from("100.00"),
1881 size: Quantity::from(10),
1882 order_id: 1,
1883 };
1884 let mbp_order2 = BookOrder {
1885 side: OrderSide::Buy,
1886 price: Price::from("101.00"),
1887 size: Quantity::from(10),
1888 order_id: 2,
1889 };
1890 ladder.add(mbp_order1, RecordFlag::F_MBP as u8);
1891 ladder.add(
1892 mbp_order2,
1893 RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8,
1894 );
1895
1896 assert_eq!(ladder.top().unwrap().price.value, Price::from("101.00"));
1897
1898 let snapshot_order = BookOrder {
1900 side: OrderSide::Buy,
1901 price: Price::from("95.00"),
1902 size: Quantity::from(20),
1903 order_id: 100,
1904 };
1905 ladder.add(
1906 snapshot_order,
1907 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
1908 );
1909
1910 assert_eq!(
1911 ladder.top().unwrap().price.value,
1912 Price::from("95.00"),
1913 "Single-delta snapshot clears MBP state: best is 95, not stale 101"
1914 );
1915 assert_eq!(ladder.len(), 1);
1916 }
1917}