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