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