1use std::cmp::Ordering;
19
20#[cfg(feature = "defi")]
21use alloy_primitives::U256;
22use indexmap::IndexMap;
23use nautilus_core::{
24 UnixNanos,
25 correctness::{CorrectnessError, CorrectnessResult, CorrectnessResultExt},
26};
27use rust_decimal::Decimal;
28
29#[cfg(feature = "defi")]
30use crate::types::fixed::FIXED_PRECISION;
31use crate::{
32 data::order::{BookOrder, OrderId},
33 enums::OrderSide,
34 orderbook::{BookIntegrityError, BookPrice},
35 types::{fixed::checked_mul_div_fixed, price::PriceRaw, quantity::QuantityRaw},
36};
37
38#[derive(Clone, Debug, Eq)]
42#[cfg_attr(
43 feature = "python",
44 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
45)]
46#[cfg_attr(
47 feature = "python",
48 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
49)]
50pub struct BookLevel {
51 pub price: BookPrice,
52 pub(crate) orders: IndexMap<OrderId, BookOrder>,
53}
54
55impl BookLevel {
56 #[must_use]
58 pub fn new(price: BookPrice) -> Self {
59 Self {
60 price,
61 orders: IndexMap::new(),
62 }
63 }
64
65 #[must_use]
67 pub fn from_order(order: BookOrder) -> Self {
68 let mut level = Self {
69 price: order.to_book_price(),
70 orders: IndexMap::new(),
71 };
72 level.add(order);
73 level
74 }
75
76 #[must_use]
77 pub fn side(&self) -> OrderSide {
78 self.price.side
79 }
80
81 #[must_use]
83 pub fn len(&self) -> usize {
84 self.orders.len()
85 }
86
87 #[must_use]
89 pub fn is_empty(&self) -> bool {
90 self.orders.is_empty()
91 }
92
93 #[inline]
95 #[must_use]
96 pub fn first(&self) -> Option<&BookOrder> {
97 self.orders.get_index(0).map(|(_key, order)| order)
98 }
99
100 pub fn iter(&self) -> impl Iterator<Item = &BookOrder> {
102 self.orders.values()
103 }
104
105 #[must_use]
107 pub fn get_orders(&self) -> Vec<BookOrder> {
108 self.orders.values().copied().collect()
109 }
110
111 #[must_use]
113 pub fn size(&self) -> f64 {
114 self.orders.values().map(|o| o.size.as_f64()).sum()
115 }
116
117 #[must_use]
123 pub fn size_raw(&self) -> QuantityRaw {
124 self.size_raw_checked()
125 .expect_display("Overflow occurred when summing `BookLevel` raw size")
126 }
127
128 pub(crate) fn size_raw_checked(&self) -> CorrectnessResult<QuantityRaw> {
134 self.orders
135 .values()
136 .try_fold(0, |total: QuantityRaw, order| {
137 total.checked_add(order.size.raw())
138 })
139 .ok_or_else(|| CorrectnessError::PredicateViolation {
140 message: "Overflow occurred when summing `BookLevel` raw size".to_string(),
141 })
142 }
143
144 #[must_use]
146 pub fn size_decimal(&self) -> Decimal {
147 self.orders.values().map(|o| o.size.as_decimal()).sum()
148 }
149
150 #[must_use]
152 pub fn exposure(&self) -> f64 {
153 self.orders
154 .values()
155 .map(|o| o.price.as_f64() * o.size.as_f64())
156 .sum()
157 }
158
159 #[must_use]
167 pub fn exposure_raw(&self) -> QuantityRaw {
168 self.orders
169 .values()
170 .map(|order| {
171 calculate_exposure_raw(
172 order.price.raw(),
173 order.size.raw(),
174 order.price.precision,
175 order.size.precision,
176 )
177 })
178 .fold(0, QuantityRaw::saturating_add)
179 }
180
181 pub fn add_bulk(&mut self, orders: &[BookOrder]) {
183 for order in orders {
184 self.add(*order);
185 }
186 }
187
188 pub fn add(&mut self, order: BookOrder) {
190 debug_assert_eq!(order.price, self.price.value);
191
192 if !order.size.is_positive() {
193 log::warn!(
194 "Attempted to add order with non-positive size: order_id={order_id}, size={size}, ignoring",
195 order_id = order.order_id,
196 size = order.size
197 );
198 return;
199 }
200
201 self.orders.insert(order.order_id, order);
202 }
203
204 pub fn update(&mut self, order: BookOrder) {
207 debug_assert_eq!(order.price, self.price.value);
208
209 if order.size.is_zero() {
210 self.orders.shift_remove(&order.order_id);
212 } else {
213 debug_assert!(
214 order.size.is_positive(),
215 "Order size must be positive: {}",
216 order.size
217 );
218 self.orders.insert(order.order_id, order);
219 }
220 }
221
222 pub fn delete(&mut self, order: &BookOrder) {
224 self.orders.shift_remove(&order.order_id);
225 }
226
227 pub fn remove_by_id(&mut self, order_id: OrderId, sequence: u64, ts_event: UnixNanos) {
233 assert!(
234 self.orders.shift_remove(&order_id).is_some(),
235 "{}",
236 &BookIntegrityError::OrderNotFound(order_id, sequence, ts_event)
237 );
238 }
239}
240
241fn calculate_exposure_raw(
242 price_raw: PriceRaw,
243 size_raw: QuantityRaw,
244 price_precision: u8,
245 size_precision: u8,
246) -> QuantityRaw {
247 let Ok(price_raw) = QuantityRaw::try_from(price_raw) else {
248 return 0;
249 };
250
251 #[cfg(feature = "defi")]
252 if price_precision > FIXED_PRECISION || size_precision > FIXED_PRECISION {
253 return calculate_exposure_raw_native(price_raw, size_raw, price_precision, size_precision);
254 }
255
256 #[cfg(not(feature = "defi"))]
257 let _ = (price_precision, size_precision);
258
259 checked_mul_div_fixed(price_raw, size_raw).unwrap_or(QuantityRaw::MAX)
260}
261
262#[cfg(feature = "defi")]
263fn calculate_exposure_raw_native(
264 price_raw: QuantityRaw,
265 size_raw: QuantityRaw,
266 price_precision: u8,
267 size_precision: u8,
268) -> QuantityRaw {
269 let scale_precision = price_precision.max(FIXED_PRECISION)
270 + size_precision.max(FIXED_PRECISION)
271 - FIXED_PRECISION;
272 let scalar = 10_u128.pow(u32::from(scale_precision));
273 let exposure = U256::from(price_raw)
274 .checked_mul(U256::from(size_raw))
275 .expect("a positive i128 times a u128 fits U256")
276 / U256::from(scalar);
277
278 QuantityRaw::try_from(exposure).unwrap_or(QuantityRaw::MAX)
279}
280
281impl PartialEq for BookLevel {
282 fn eq(&self, other: &Self) -> bool {
283 self.price == other.price
284 }
285}
286
287impl PartialOrd for BookLevel {
288 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
289 Some(self.cmp(other))
290 }
291}
292
293impl Ord for BookLevel {
294 fn cmp(&self, other: &Self) -> Ordering {
295 self.price.cmp(&other.price)
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use rstest::rstest;
302 use rust_decimal_macros::dec;
303
304 #[cfg(feature = "high-precision")]
305 use super::calculate_exposure_raw;
306 use crate::{
307 data::order::BookOrder,
308 enums::OrderSide,
309 orderbook::{BookLevel, BookPrice},
310 types::{
311 Price, Quantity,
312 fixed::{FIXED_PRECISION, FIXED_SCALAR},
313 price::PriceRaw,
314 quantity::QuantityRaw,
315 },
316 };
317
318 #[rstest]
319 fn test_empty_level() {
320 let level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
321 assert!(level.first().is_none());
322 assert_eq!(level.side(), OrderSide::Buy);
323 }
324
325 #[rstest]
326 fn test_level_from_order() {
327 let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
328 let level = BookLevel::from_order(order);
329
330 assert_eq!(level.price.value, Price::from("1.00"));
331 assert_eq!(level.price.side, OrderSide::Buy);
332 assert_eq!(level.len(), 1);
333 assert_eq!(level.first().unwrap(), &order);
334 assert_eq!(level.size(), 10.0);
335 }
336
337 #[rstest]
338 #[should_panic(expected = "assertion `left == right` failed")]
339 fn test_add_order_incorrect_price_level() {
340 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
341 let incorrect_price_order =
342 BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 1);
343 level.add(incorrect_price_order);
344 }
345
346 #[rstest]
347 #[should_panic(expected = "assertion `left == right` failed")]
348 fn test_add_bulk_orders_incorrect_price() {
349 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
350 let orders = [
351 BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1),
352 BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 2), ];
354 level.add_bulk(&orders);
355 }
356
357 #[rstest]
358 fn test_add_bulk_empty() {
359 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
360 level.add_bulk(&[]);
361 assert!(level.is_empty());
362 }
363
364 #[rstest]
365 #[case::bid(OrderSide::Buy, true)]
366 #[case::ask(OrderSide::Sell, false)]
367 fn test_comparisons(#[case] side: OrderSide, #[case] first_is_greater: bool) {
368 let level0 = BookLevel::new(BookPrice::new(Price::from("1.00"), side));
369 let same = BookLevel::new(BookPrice::new(Price::from("1.00"), side));
370 let level1 = BookLevel::new(BookPrice::new(Price::from("1.01"), side));
371
372 assert_eq!(level0, same);
373 assert_ne!(level0, level1);
374 assert_eq!(level0 > level1, first_is_greater);
375 assert_eq!(level0 < level1, !first_is_greater);
376 }
377
378 #[rstest]
379 fn test_book_level_sorting() {
380 let mut levels = [
381 BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Sell)),
382 BookLevel::new(BookPrice::new(Price::from("1.02"), OrderSide::Sell)),
383 BookLevel::new(BookPrice::new(Price::from("1.01"), OrderSide::Sell)),
384 ];
385 levels.sort();
386 assert_eq!(levels[0].price.value, Price::from("1.00"));
387 assert_eq!(levels[1].price.value, Price::from("1.01"));
388 assert_eq!(levels[2].price.value, Price::from("1.02"));
389 }
390
391 #[rstest]
392 fn test_add_single_order() {
393 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
394 let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
395
396 level.add(order);
397 assert!(!level.is_empty());
398 assert_eq!(level.len(), 1);
399 assert_eq!(level.size(), 10.0);
400 assert_eq!(level.first().unwrap(), &order);
401 }
402
403 #[rstest]
404 fn test_add_multiple_orders() {
405 let mut level = BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSide::Buy));
406 let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
407 let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
408
409 level.add(order1);
410 level.add(order2);
411 assert_eq!(level.len(), 2);
412 assert_eq!(level.size(), 30.0);
413 assert_eq!(level.exposure(), 60.0);
414 assert_eq!(level.first().unwrap(), &order1);
415 }
416
417 #[rstest]
418 fn test_get_orders() {
419 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
420 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
421 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
422
423 level.add(order1);
424 level.add(order2);
425
426 let orders = level.get_orders();
427 assert_eq!(orders.len(), 2);
428 assert_eq!(orders[0], order1); assert_eq!(orders[1], order2);
430 }
431
432 #[rstest]
433 fn test_iter_returns_fifo() {
434 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
435 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
436 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
437 level.add(order1);
438 level.add(order2);
439
440 let orders: Vec<_> = level.iter().copied().collect();
441 assert_eq!(orders, vec![order1, order2]);
442 }
443
444 #[rstest]
445 fn test_update_order() {
446 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
447 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
448 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 0);
449
450 level.add(order1);
451 level.update(order2);
452 assert_eq!(level.len(), 1);
453 assert_eq!(level.size(), 20.0);
454 assert_eq!(level.exposure(), 20.0);
455 }
456
457 #[rstest]
458 fn test_update_inserts_if_missing() {
459 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
460 let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
461 level.update(order);
462 assert_eq!(level.len(), 1);
463 assert_eq!(level.first().unwrap(), &order);
464 }
465
466 #[rstest]
467 fn test_update_zero_size_nonexistent() {
468 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
469 let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::zero(0), 1);
470 level.update(order);
471 assert_eq!(level.len(), 0);
472 }
473
474 #[rstest]
475 fn test_fifo_order_after_updates() {
476 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
477
478 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
479 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
480
481 level.add(order1);
482 level.add(order2);
483
484 let updated_order1 =
486 BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(15), 1);
487 level.update(updated_order1);
488
489 let orders = level.get_orders();
490 assert_eq!(orders.len(), 2);
491 assert_eq!(orders[0], updated_order1); assert_eq!(orders[1], order2); }
494
495 #[rstest]
496 fn test_insertion_order_after_mixed_operations() {
497 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
498 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
499 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
500 let order3 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(30), 3);
501
502 level.add(order1);
503 level.add(order2);
504 level.add(order3);
505
506 let updated_order2 =
508 BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(25), 2);
509 level.update(updated_order2);
510
511 level.delete(&order1);
513
514 let orders = level.get_orders();
515 assert_eq!(orders, vec![updated_order2, order3]);
516 }
517
518 #[rstest]
519 #[should_panic(expected = "assertion `left == right` failed")]
520 fn test_update_order_incorrect_price() {
521 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
522
523 let initial_order =
525 BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
526 level.add(initial_order);
527
528 let updated_order =
530 BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
531 level.update(updated_order);
532 }
533
534 #[rstest]
535 fn test_update_order_with_zero_size() {
536 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
537 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
538 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::zero(0), 0);
539
540 level.add(order1);
541 level.update(order2);
542 assert_eq!(level.len(), 0);
543 assert_eq!(level.size(), 0.0);
544 assert_eq!(level.exposure(), 0.0);
545 }
546
547 #[rstest]
548 fn test_delete_nonexistent_order() {
549 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
550 let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
551 level.delete(&order);
552 assert_eq!(level.len(), 0);
553 }
554
555 #[rstest]
556 fn test_delete_order() {
557 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
558 let order1_id = 0;
559 let order1 = BookOrder::new(
560 OrderSide::Buy,
561 Price::from("1.00"),
562 Quantity::from(10),
563 order1_id,
564 );
565 let order2_id = 1;
566 let order2 = BookOrder::new(
567 OrderSide::Buy,
568 Price::from("1.00"),
569 Quantity::from(20),
570 order2_id,
571 );
572
573 level.add(order1);
574 level.add(order2);
575 level.delete(&order1);
576 assert_eq!(level.len(), 1);
577 assert_eq!(level.size(), 20.0);
578 assert!(level.orders.contains_key(&order2_id));
579 assert_eq!(level.exposure(), 20.0);
580 }
581
582 #[rstest]
583 fn test_remove_order_by_id() {
584 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
585 let order1_id = 0;
586 let order1 = BookOrder::new(
587 OrderSide::Buy,
588 Price::from("1.00"),
589 Quantity::from(10),
590 order1_id,
591 );
592 let order2_id = 1;
593 let order2 = BookOrder::new(
594 OrderSide::Buy,
595 Price::from("1.00"),
596 Quantity::from(20),
597 order2_id,
598 );
599
600 level.add(order1);
601 level.add(order2);
602 level.remove_by_id(order2_id, 0, 0.into());
603 assert_eq!(level.len(), 1);
604 assert!(level.orders.contains_key(&order1_id));
605 assert_eq!(level.size(), 10.0);
606 assert_eq!(level.exposure(), 10.0);
607 }
608
609 #[rstest]
610 fn test_add_bulk_orders() {
611 let mut level = BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSide::Buy));
612 let order1_id = 0;
613 let order1 = BookOrder::new(
614 OrderSide::Buy,
615 Price::from("2.00"),
616 Quantity::from(10),
617 order1_id,
618 );
619 let order2_id = 1;
620 let order2 = BookOrder::new(
621 OrderSide::Buy,
622 Price::from("2.00"),
623 Quantity::from(20),
624 order2_id,
625 );
626
627 let orders = [order1, order2];
628 level.add_bulk(&orders);
629 assert_eq!(level.len(), 2);
630 assert_eq!(level.size(), 30.0);
631 assert_eq!(level.exposure(), 60.0);
632 }
633
634 #[rstest]
635 fn test_maximum_order_id() {
636 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
637
638 let order = BookOrder::new(
639 OrderSide::Buy,
640 Price::from("1.00"),
641 Quantity::from(10),
642 u64::MAX,
643 );
644 level.add(order);
645
646 assert_eq!(level.len(), 1);
647 assert_eq!(level.first().unwrap(), &order);
648 }
649
650 #[rstest]
651 #[should_panic(
652 expected = "Integrity error: order not found: order_id=1, sequence=2, ts_event=3"
653 )]
654 fn test_remove_nonexistent_order() {
655 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
656 level.remove_by_id(1, 2, 3.into());
657 }
658
659 #[rstest]
660 fn test_size_raw() {
661 let mut level = BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSide::Buy));
662 let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
663 let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
664
665 level.add(order1);
666 level.add(order2);
667 assert_eq!(
668 level.size_raw(),
669 (30.0 * FIXED_SCALAR).round() as QuantityRaw
670 );
671 }
672
673 #[rstest]
674 fn test_size_decimal() {
675 let mut level = BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSide::Buy));
676 let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
677 let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
678
679 level.add(order1);
680 level.add(order2);
681 assert_eq!(level.size_decimal(), dec!(30.0));
682 }
683
684 #[rstest]
685 #[case::negative("-2", "10", 0)]
686 #[case::zero("0", "1", 0)]
687 #[case::small("2", "10", 20)]
688 fn test_exposure_raw_exact_whole(
689 #[case] price: &str,
690 #[case] size: &str,
691 #[case] expected_units: QuantityRaw,
692 ) {
693 let price = Price::from(price);
694 let mut level = BookLevel::new(BookPrice::new(price, OrderSide::Buy));
695 level.add(BookOrder::new(
696 OrderSide::Buy,
697 price,
698 Quantity::from(size),
699 0,
700 ));
701
702 assert_eq!(
703 level.exposure_raw(),
704 expected_units * FIXED_SCALAR as QuantityRaw
705 );
706 }
707
708 #[rstest]
709 fn test_exposure_raw_truncates_sub_raw_unit() {
710 let scalar = FIXED_SCALAR as QuantityRaw;
711 let price = Price::from_raw((scalar + 1) as PriceRaw, FIXED_PRECISION);
712 let size = Quantity::from_raw(scalar + 1, FIXED_PRECISION);
713 let mut level = BookLevel::new(BookPrice::new(price, OrderSide::Buy));
714 level.add(BookOrder::new(OrderSide::Buy, price, size, 0));
715
716 assert_eq!(level.exposure_raw(), scalar + 2);
717 }
718
719 #[rstest]
720 fn test_exposure_raw_accumulates_exactly() {
721 let mut level = BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSide::Buy));
722 let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
723 let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
724
725 level.add(order1);
726 level.add(order2);
727 assert_eq!(level.exposure_raw(), 60 * FIXED_SCALAR as QuantityRaw);
728 }
729
730 #[cfg(not(feature = "high-precision"))]
731 #[rstest]
732 fn test_exposure_raw_preserves_non_saturating_raw_units() {
733 let price = Price::from("9007199253.999999999");
734 let size = Quantity::from("2.000000001");
735 let mut level = BookLevel::new(BookPrice::new(price, OrderSide::Buy));
736 level.add(BookOrder::new(OrderSide::Buy, price, size, 0));
737
738 assert_eq!(level.exposure_raw(), 18_014_398_517_007_199_251);
739 }
740
741 #[cfg(feature = "high-precision")]
742 #[rstest]
743 fn test_exposure_raw_avoids_phantom_overflow() {
744 let scalar = FIXED_SCALAR as QuantityRaw;
745 let price_raw = 100_000 * scalar;
746 let size_raw = 100 * scalar;
747
748 assert_eq!(price_raw.checked_mul(size_raw), None);
749 assert_eq!(
750 calculate_exposure_raw(
751 price_raw as PriceRaw,
752 size_raw,
753 FIXED_PRECISION,
754 FIXED_PRECISION,
755 ),
756 10_000_000 * scalar
757 );
758 }
759
760 #[rstest]
761 fn test_exposure_raw_saturates_single_order() {
762 #[cfg(feature = "high-precision")]
763 let (price_str, qty_str) = ("1000000000000.00", "1000000000000.00");
764 #[cfg(not(feature = "high-precision"))]
765 let (price_str, qty_str) = ("100000000.00", "1000000000.00");
766
767 let mut level = BookLevel::new(BookPrice::new(Price::from(price_str), OrderSide::Buy));
768 let order = BookOrder::new(
769 OrderSide::Buy,
770 Price::from(price_str),
771 Quantity::from(qty_str),
772 0,
773 );
774
775 level.add(order);
776
777 assert_eq!(level.exposure_raw(), QuantityRaw::MAX);
778 }
779
780 #[rstest]
781 fn test_exposure_raw_accumulation_saturates() {
782 #[cfg(feature = "high-precision")]
783 let (price_str, qty_str, expected_single) = (
784 "100000000000.0",
785 "200000000000.0",
786 200_000_000_000_000_000_000_000_000_000_000_000_000,
787 );
788 #[cfg(not(feature = "high-precision"))]
789 let (price_str, qty_str, expected_single) =
790 ("2.0", "5000000000.0", 10_000_000_000_000_000_000);
791
792 let mut level = BookLevel::new(BookPrice::new(Price::from(price_str), OrderSide::Buy));
793 level.add(BookOrder::new(
794 OrderSide::Buy,
795 Price::from(price_str),
796 Quantity::from(qty_str),
797 0,
798 ));
799 assert_eq!(level.exposure_raw(), expected_single);
800
801 level.add(BookOrder::new(
802 OrderSide::Buy,
803 Price::from(price_str),
804 Quantity::from(qty_str),
805 1,
806 ));
807 assert_eq!(level.exposure_raw(), QuantityRaw::MAX);
808 }
809
810 #[cfg(feature = "defi")]
811 #[rstest]
812 fn test_exposure_raw_preserves_native_defi_scales() {
813 let price_precision = FIXED_PRECISION + 1;
814 let size_precision = FIXED_PRECISION + 2;
815 let price = Price::from_raw(
816 125 * 10_i128.pow(u32::from(price_precision - 2)),
817 price_precision,
818 );
819 let size = Quantity::from_raw(
820 24 * 10_u128.pow(u32::from(size_precision - 1)),
821 size_precision,
822 );
823 let mut level = BookLevel::new(BookPrice::new(price, OrderSide::Buy));
824 level.add(BookOrder::new(OrderSide::Buy, price, size, 0));
825
826 assert_eq!(level.exposure_raw(), 3 * FIXED_SCALAR as QuantityRaw);
827 }
828
829 #[cfg(feature = "defi")]
830 #[rstest]
831 #[case::native_price(1_250_000_000_000_000_000, 24_000_000_000_000_000, 18, 8)]
832 #[case::native_size(12_500_000_000_000_000, 2_400_000_000_000_000_000, 8, 18)]
833 fn test_exposure_raw_preserves_mixed_defi_scales(
834 #[case] price_raw: PriceRaw,
835 #[case] size_raw: QuantityRaw,
836 #[case] price_precision: u8,
837 #[case] size_precision: u8,
838 ) {
839 assert_eq!(
840 calculate_exposure_raw(price_raw, size_raw, price_precision, size_precision),
841 3 * FIXED_SCALAR as QuantityRaw
842 );
843 }
844}