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