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