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 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 ) {
1378 let mut ladder = BookLadder::new(side_spec, BookType::L3_MBO);
1379
1380 let moved = BookOrder {
1381 side,
1382 price: Price::from(old_price),
1383 size: Quantity::from(50),
1384 order_id: 1,
1385 };
1386 let staying = BookOrder {
1387 side,
1388 price: Price::from(old_price),
1389 size: Quantity::from(30),
1390 order_id: 2,
1391 };
1392 ladder.add(moved, 0);
1393 ladder.add(staying, 0);
1394
1395 let moved_new = BookOrder {
1396 side,
1397 price: Price::from(new_price),
1398 size: Quantity::from(60),
1399 order_id: 1,
1400 };
1401 ladder.add(moved_new, 0);
1402
1403 assert_eq!(
1404 ladder.len(),
1405 2,
1406 "Old level must survive with the remaining order"
1407 );
1408 let old_level = ladder
1409 .levels
1410 .get(&BookPrice::new(Price::from(old_price), side_spec))
1411 .expect("Old level should remain");
1412 assert_eq!(old_level.get_orders(), vec![staying]);
1413 let new_level = ladder
1414 .levels
1415 .get(&BookPrice::new(Price::from(new_price), side_spec))
1416 .expect("New level should exist");
1417 assert_eq!(new_level.get_orders(), vec![moved_new]);
1418 }
1419
1420 #[rstest]
1421 fn test_l1_vs_l3_duplicate_order_id_replacement() {
1422 let mut l1_ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1424 let side_constant = OrderSide::Buy as u64;
1425
1426 let order1 = BookOrder {
1427 side: OrderSide::Buy.into(),
1428 price: Price::from("100.00"),
1429 size: Quantity::from(50),
1430 order_id: side_constant,
1431 };
1432 l1_ladder.add(order1, 0);
1433
1434 let order2 = BookOrder {
1435 side: OrderSide::Buy.into(),
1436 price: Price::from("101.00"),
1437 size: Quantity::from(60),
1438 order_id: side_constant, };
1440 l1_ladder.add(order2, 0);
1441
1442 assert_eq!(l1_ladder.len(), 1, "L1 should have only 1 level");
1443 assert_eq!(
1444 l1_ladder.top().unwrap().price.value,
1445 Price::from("101.00"),
1446 "L1 should have replaced the old level"
1447 );
1448
1449 let mut l3_ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
1450
1451 let order3 = BookOrder {
1452 side: OrderSide::Buy.into(),
1453 price: Price::from("100.00"),
1454 size: Quantity::from(50),
1455 order_id: 1, };
1457 l3_ladder.add(order3, 0);
1458
1459 let order4 = BookOrder {
1460 side: OrderSide::Buy.into(),
1461 price: Price::from("101.00"),
1462 size: Quantity::from(60),
1463 order_id: 1,
1464 };
1465 l3_ladder.add(order4, 0);
1466
1467 assert_eq!(
1468 l3_ladder.len(),
1469 1,
1470 "L3 should move the order to its new price level"
1471 );
1472 assert_eq!(
1473 l3_ladder.top().unwrap().price.value,
1474 Price::from("101.00"),
1475 "L3 order should have moved to 101.00"
1476 );
1477 }
1478
1479 #[rstest]
1480 #[case::bids_worst_to_best(OrderSide::Buy, Some(OrderSide::Buy), &["99.00", "100.00", "101.00", "102.00"], "102.00")]
1481 #[case::bids_best_to_worst(OrderSide::Buy, Some(OrderSide::Buy), &["102.00", "101.00", "100.00", "99.00"], "100.00")]
1482 #[case::asks_worst_to_best(OrderSide::Sell, Some(OrderSide::Sell), &["105.00", "104.00", "103.00", "102.00"], "102.00")]
1483 #[case::asks_best_to_worst(OrderSide::Sell, Some(OrderSide::Sell), &["102.00", "103.00", "104.00", "105.00"], "104.00")]
1484 fn test_l1_multi_delta_batch_keeps_best_of_final_two(
1485 #[case] side_spec: OrderSide,
1486 #[case] side: Option<OrderSide>,
1487 #[case] prices: &[&str],
1488 #[case] expected_best: &str,
1489 ) {
1490 let mut ladder = BookLadder::new(side_spec, BookType::L1_MBP);
1493
1494 let batch_size = prices.len();
1495 for (i, price_str) in prices.iter().enumerate() {
1496 let order = BookOrder {
1497 side,
1498 price: Price::from(*price_str),
1499 size: Quantity::from((i + 1) as u64 * 10),
1500 order_id: (i + 100) as u64,
1501 };
1502 let flags = if i == batch_size - 1 {
1503 RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1504 } else {
1505 RecordFlag::F_MBP as u8
1506 };
1507 ladder.add(order, flags);
1508 }
1509
1510 assert_eq!(ladder.len(), 1, "L1 should have only 1 level");
1511 assert_eq!(
1512 ladder.top().unwrap().price.value,
1513 Price::from(expected_best),
1514 "Should keep best of final two deltas"
1515 );
1516 }
1517
1518 #[rstest]
1519 fn test_l1_retain_best_only_cache_consistency() {
1520 let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1522 let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1523 let prices = ["100.00", "101.00", "102.00", "103.00", "104.00"];
1524
1525 for (i, price_str) in prices.iter().enumerate() {
1526 let order = BookOrder {
1527 side: OrderSide::Buy.into(),
1528 price: Price::from(*price_str),
1529 size: Quantity::from(10),
1530 order_id: (i + 1) as u64,
1531 };
1532 ladder.add(order, batch_flags);
1533 }
1534
1535 assert_eq!(ladder.len(), 1);
1536 assert_eq!(
1537 ladder.cache.len(),
1538 1,
1539 "Cache should have exactly 1 entry for L1"
1540 );
1541
1542 let total_orders: usize = ladder.levels.values().map(BookLevel::len).sum();
1543 assert_eq!(
1544 ladder.cache.len(),
1545 total_orders,
1546 "Cache should be consistent with levels"
1547 );
1548 }
1549
1550 #[rstest]
1551 fn test_l1_sequential_replacement_allows_price_degradation() {
1552 let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1555 let side_constant = OrderSide::Buy as u64;
1556
1557 let order1 = BookOrder {
1559 side: OrderSide::Buy.into(),
1560 price: Price::from("101.00"),
1561 size: Quantity::from(50),
1562 order_id: side_constant,
1563 };
1564 ladder.add(order1, 0); assert_eq!(ladder.len(), 1);
1567 assert_eq!(
1568 ladder.top().unwrap().price.value,
1569 Price::from("101.00"),
1570 "Should have bid at 101.00"
1571 );
1572
1573 let order2 = BookOrder {
1576 side: OrderSide::Buy.into(),
1577 price: Price::from("100.00"),
1578 size: Quantity::from(60),
1579 order_id: side_constant,
1580 };
1581 ladder.add(order2, 0); assert_eq!(ladder.len(), 1);
1584 assert_eq!(
1585 ladder.top().unwrap().price.value,
1586 Price::from("100.00"),
1587 "Sequential replacement should allow price to degrade from 101 to 100"
1588 );
1589
1590 assert_eq!(
1592 ladder.top().unwrap().first().unwrap().size,
1593 Quantity::from(60),
1594 "Size should be from the new order"
1595 );
1596 }
1597
1598 #[rstest]
1599 #[case::bids(OrderSide::Buy, Some(OrderSide::Buy), &["100.00", "101.00", "102.00"], "102.00", &["97.00", "98.00", "99.00"], "99.00")]
1600 #[case::asks(OrderSide::Sell, Some(OrderSide::Sell), &["100.00", "101.00", "102.00"], "101.00", &["103.00", "104.00", "105.00"], "104.00")]
1601 fn test_l1_consecutive_batches_clear_between(
1602 #[case] side_spec: OrderSide,
1603 #[case] side: Option<OrderSide>,
1604 #[case] batch1_prices: &[&str],
1605 #[case] expected1: &str,
1606 #[case] batch2_prices: &[&str],
1607 #[case] expected2: &str,
1608 ) {
1609 let mut ladder = BookLadder::new(side_spec, BookType::L1_MBP);
1611
1612 for (i, price_str) in batch1_prices.iter().enumerate() {
1614 let order = BookOrder {
1615 side,
1616 price: Price::from(*price_str),
1617 size: Quantity::from(10),
1618 order_id: (i + 100) as u64,
1619 };
1620 let flags = if i == batch1_prices.len() - 1 {
1621 RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1622 } else {
1623 RecordFlag::F_MBP as u8
1624 };
1625 ladder.add(order, flags);
1626 }
1627
1628 assert_eq!(ladder.len(), 1);
1629 assert_eq!(
1630 ladder.top().unwrap().price.value,
1631 Price::from(expected1),
1632 "After batch 1"
1633 );
1634
1635 for (i, price_str) in batch2_prices.iter().enumerate() {
1637 let order = BookOrder {
1638 side,
1639 price: Price::from(*price_str),
1640 size: Quantity::from(20),
1641 order_id: (i + 200) as u64,
1642 };
1643 let flags = if i == batch2_prices.len() - 1 {
1644 RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1645 } else {
1646 RecordFlag::F_MBP as u8
1647 };
1648 ladder.add(order, flags);
1649 }
1650
1651 assert_eq!(ladder.len(), 1);
1652 assert_eq!(
1653 ladder.top().unwrap().price.value,
1654 Price::from(expected2),
1655 "After batch 2: batch 1 data cleared"
1656 );
1657 }
1658
1659 #[rstest]
1660 fn test_l1_zero_size_clears_regardless_of_order_id() {
1661 let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1664
1665 let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1667 let order = BookOrder {
1668 side: OrderSide::Buy.into(),
1669 price: Price::from("100.00"),
1670 size: Quantity::from(50),
1671 order_id: 12345, };
1673 ladder.add(order, batch_flags);
1674 assert_eq!(ladder.len(), 1);
1675
1676 let clear_order = BookOrder {
1678 side: OrderSide::Buy.into(),
1679 price: Price::from("100.00"),
1680 size: Quantity::zero(9),
1681 order_id: OrderSide::Buy as u64, };
1683 ladder.add(clear_order, 0);
1684
1685 assert_eq!(
1687 ladder.len(),
1688 0,
1689 "Zero-size should clear L1 regardless of order_id"
1690 );
1691 assert!(ladder.cache.is_empty(), "Cache should be empty after clear");
1692 }
1693
1694 #[rstest]
1695 fn test_l1_f_mbp_without_f_last_does_not_accumulate() {
1696 let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1699 let flags = RecordFlag::F_MBP as u8; let prices = [
1703 "100.00", "99.00", "98.00", "97.00", "96.00", "95.00", "94.00", "93.00", "92.00",
1704 "91.00",
1705 ];
1706
1707 for (i, price_str) in prices.iter().enumerate() {
1708 let order = BookOrder {
1709 side: OrderSide::Buy.into(),
1710 price: Price::from(*price_str),
1711 size: Quantity::from(10),
1712 order_id: (i + 100) as u64,
1713 };
1714 ladder.add(order, flags);
1715
1716 assert_eq!(
1717 ladder.len(),
1718 1,
1719 "L1 should always have at most 1 level, iteration {i}"
1720 );
1721 }
1722
1723 assert_eq!(
1725 ladder.top().unwrap().price.value,
1726 Price::from("91.00"),
1727 "Should show last price (91), allowing degradation"
1728 );
1729 }
1730
1731 #[rstest]
1732 fn test_l1_f_mbp_two_delta_batch_retains_best() {
1733 let mut ladder = BookLadder::new(OrderSide::Sell, BookType::L1_MBP);
1735
1736 let order1 = BookOrder {
1738 side: OrderSide::Sell.into(),
1739 price: Price::from("100.00"),
1740 size: Quantity::from(10),
1741 order_id: 100,
1742 };
1743 ladder.add(order1, RecordFlag::F_MBP as u8);
1744
1745 let order2 = BookOrder {
1748 side: OrderSide::Sell.into(),
1749 price: Price::from("101.00"),
1750 size: Quantity::from(20),
1751 order_id: 101,
1752 };
1753 ladder.add(order2, RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8);
1754
1755 assert_eq!(ladder.len(), 1);
1756 assert_eq!(
1757 ladder.top().unwrap().price.value,
1758 Price::from("100.00"),
1759 "2-delta batch keeps best ask (100) from both deltas"
1760 );
1761 }
1762
1763 #[rstest]
1764 fn test_l1_snapshot_batch_accumulates_all_levels_bids() {
1765 let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1767 let prices = ["98.00", "99.00", "100.00", "101.00"];
1768 let batch_size = prices.len();
1769
1770 for (i, price_str) in prices.iter().enumerate() {
1771 let order = BookOrder {
1772 side: OrderSide::Buy.into(),
1773 price: Price::from(*price_str),
1774 size: Quantity::from(10),
1775 order_id: (i + 100) as u64,
1776 };
1777 let flags = if i == batch_size - 1 {
1778 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1779 } else {
1780 RecordFlag::F_SNAPSHOT as u8
1781 };
1782 ladder.add(order, flags);
1783 }
1784
1785 assert_eq!(
1786 ladder.len(),
1787 1,
1788 "L1 should have only 1 level after snapshot"
1789 );
1790 assert_eq!(
1791 ladder.top().unwrap().price.value,
1792 Price::from("101.00"),
1793 "F_SNAPSHOT batch should keep best bid (101) from ALL deltas"
1794 );
1795 }
1796
1797 #[rstest]
1798 fn test_l1_snapshot_batch_accumulates_all_levels_asks() {
1799 let mut ladder = BookLadder::new(OrderSide::Sell, BookType::L1_MBP);
1801 let prices = ["104.00", "103.00", "102.00", "101.00"];
1802 let batch_size = prices.len();
1803
1804 for (i, price_str) in prices.iter().enumerate() {
1805 let order = BookOrder {
1806 side: OrderSide::Sell.into(),
1807 price: Price::from(*price_str),
1808 size: Quantity::from(10),
1809 order_id: (i + 100) as u64,
1810 };
1811 let flags = if i == batch_size - 1 {
1812 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1813 } else {
1814 RecordFlag::F_SNAPSHOT as u8
1815 };
1816 ladder.add(order, flags);
1817 }
1818
1819 assert_eq!(
1820 ladder.len(),
1821 1,
1822 "L1 should have only 1 level after snapshot"
1823 );
1824 assert_eq!(
1825 ladder.top().unwrap().price.value,
1826 Price::from("101.00"),
1827 "F_SNAPSHOT batch should keep best ask (101) from ALL deltas"
1828 );
1829 }
1830
1831 #[rstest]
1832 fn test_l1_snapshot_vs_mbp_different_accumulation_behavior() {
1833 let mut mbp_ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1835 let prices = ["98.00", "99.00", "100.00", "101.00"];
1836 for (i, price_str) in prices.iter().enumerate() {
1837 let order = BookOrder {
1838 side: OrderSide::Buy.into(),
1839 price: Price::from(*price_str),
1840 size: Quantity::from(10),
1841 order_id: (i + 100) as u64,
1842 };
1843 let flags = if i == prices.len() - 1 {
1844 RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1845 } else {
1846 RecordFlag::F_MBP as u8
1847 };
1848 mbp_ladder.add(order, flags);
1849 }
1850 assert_eq!(
1851 mbp_ladder.top().unwrap().price.value,
1852 Price::from("101.00"),
1853 "F_MBP keeps best of final two (100, 101)"
1854 );
1855
1856 let mut snapshot_ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1857
1858 for (i, price_str) in prices.iter().enumerate() {
1859 let order = BookOrder {
1860 side: OrderSide::Buy.into(),
1861 price: Price::from(*price_str),
1862 size: Quantity::from(10),
1863 order_id: (i + 200) as u64,
1864 };
1865 let flags = if i == prices.len() - 1 {
1866 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1867 } else {
1868 RecordFlag::F_SNAPSHOT as u8
1869 };
1870 snapshot_ladder.add(order, flags);
1871 }
1872 assert_eq!(
1873 snapshot_ladder.top().unwrap().price.value,
1874 Price::from("101.00"),
1875 "F_SNAPSHOT keeps best of ALL deltas (98, 99, 100, 101)"
1876 );
1877 }
1878
1879 #[rstest]
1880 fn test_l1_snapshot_after_incomplete_mbp_stream() {
1881 let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1883
1884 let stale_order = BookOrder {
1886 side: OrderSide::Buy.into(),
1887 price: Price::from("101.00"),
1888 size: Quantity::from(10),
1889 order_id: 100,
1890 };
1891 ladder.add(stale_order, RecordFlag::F_MBP as u8);
1892 assert_eq!(ladder.top().unwrap().price.value, Price::from("101.00"));
1893
1894 ladder.clear();
1896
1897 for (i, price_str) in ["98.00", "99.00", "100.00"].iter().enumerate() {
1899 let order = BookOrder {
1900 side: OrderSide::Buy.into(),
1901 price: Price::from(*price_str),
1902 size: Quantity::from(10),
1903 order_id: (i + 200) as u64,
1904 };
1905 let flags = if i == 2 {
1906 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1907 } else {
1908 RecordFlag::F_SNAPSHOT as u8
1909 };
1910 ladder.add(order, flags);
1911 }
1912
1913 assert_eq!(
1914 ladder.top().unwrap().price.value,
1915 Price::from("100.00"),
1916 "Snapshot replaces stale MBP state: best is 100, not stale 101"
1917 );
1918 }
1919
1920 #[rstest]
1921 fn test_l1_snapshot_clears_previous_batch() {
1922 let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1924
1925 for (i, price_str) in ["100.00", "101.00", "102.00"].iter().enumerate() {
1926 let order = BookOrder {
1927 side: OrderSide::Buy.into(),
1928 price: Price::from(*price_str),
1929 size: Quantity::from(10),
1930 order_id: (i + 100) as u64,
1931 };
1932 let flags = if i == 2 {
1933 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1934 } else {
1935 RecordFlag::F_SNAPSHOT as u8
1936 };
1937 ladder.add(order, flags);
1938 }
1939 assert_eq!(ladder.top().unwrap().price.value, Price::from("102.00"));
1940
1941 for (i, price_str) in ["95.00", "96.00", "97.00"].iter().enumerate() {
1943 let order = BookOrder {
1944 side: OrderSide::Buy.into(),
1945 price: Price::from(*price_str),
1946 size: Quantity::from(20),
1947 order_id: (i + 200) as u64,
1948 };
1949 let flags = if i == 2 {
1950 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1951 } else {
1952 RecordFlag::F_SNAPSHOT as u8
1953 };
1954 ladder.add(order, flags);
1955 }
1956 assert_eq!(
1957 ladder.top().unwrap().price.value,
1958 Price::from("97.00"),
1959 "Second batch clears first: best is 97, not 102"
1960 );
1961 }
1962
1963 #[rstest]
1964 fn test_l1_single_delta_snapshot_after_mbp_batch() {
1965 let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1967
1968 let mbp_order1 = BookOrder {
1969 side: OrderSide::Buy.into(),
1970 price: Price::from("100.00"),
1971 size: Quantity::from(10),
1972 order_id: 1,
1973 };
1974 let mbp_order2 = BookOrder {
1975 side: OrderSide::Buy.into(),
1976 price: Price::from("101.00"),
1977 size: Quantity::from(10),
1978 order_id: 2,
1979 };
1980 ladder.add(mbp_order1, RecordFlag::F_MBP as u8);
1981 ladder.add(
1982 mbp_order2,
1983 RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8,
1984 );
1985
1986 assert_eq!(ladder.top().unwrap().price.value, Price::from("101.00"));
1987
1988 let snapshot_order = BookOrder {
1990 side: OrderSide::Buy.into(),
1991 price: Price::from("95.00"),
1992 size: Quantity::from(20),
1993 order_id: 100,
1994 };
1995 ladder.add(
1996 snapshot_order,
1997 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
1998 );
1999
2000 assert_eq!(
2001 ladder.top().unwrap().price.value,
2002 Price::from("95.00"),
2003 "Single-delta snapshot clears MBP state: best is 95, not stale 101"
2004 );
2005 assert_eq!(ladder.len(), 1);
2006 }
2007}