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_eq!(level0 > level1, first_is_greater);
374 assert_eq!(level0 < level1, !first_is_greater);
375 }
376
377 #[rstest]
378 fn test_book_level_sorting() {
379 let mut levels = [
380 BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Sell)),
381 BookLevel::new(BookPrice::new(Price::from("1.02"), OrderSide::Sell)),
382 BookLevel::new(BookPrice::new(Price::from("1.01"), OrderSide::Sell)),
383 ];
384 levels.sort();
385 assert_eq!(levels[0].price.value, Price::from("1.00"));
386 assert_eq!(levels[1].price.value, Price::from("1.01"));
387 assert_eq!(levels[2].price.value, Price::from("1.02"));
388 }
389
390 #[rstest]
391 fn test_add_single_order() {
392 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
393 let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
394
395 level.add(order);
396 assert!(!level.is_empty());
397 assert_eq!(level.len(), 1);
398 assert_eq!(level.size(), 10.0);
399 assert_eq!(level.first().unwrap(), &order);
400 }
401
402 #[rstest]
403 fn test_add_multiple_orders() {
404 let mut level = BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSide::Buy));
405 let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
406 let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
407
408 level.add(order1);
409 level.add(order2);
410 assert_eq!(level.len(), 2);
411 assert_eq!(level.size(), 30.0);
412 assert_eq!(level.exposure(), 60.0);
413 assert_eq!(level.first().unwrap(), &order1);
414 }
415
416 #[rstest]
417 fn test_get_orders() {
418 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
419 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
420 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
421
422 level.add(order1);
423 level.add(order2);
424
425 let orders = level.get_orders();
426 assert_eq!(orders.len(), 2);
427 assert_eq!(orders[0], order1); assert_eq!(orders[1], order2);
429 }
430
431 #[rstest]
432 fn test_iter_returns_fifo() {
433 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
434 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
435 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
436 level.add(order1);
437 level.add(order2);
438
439 let orders: Vec<_> = level.iter().copied().collect();
440 assert_eq!(orders, vec![order1, order2]);
441 }
442
443 #[rstest]
444 fn test_update_order() {
445 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
446 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
447 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 0);
448
449 level.add(order1);
450 level.update(order2);
451 assert_eq!(level.len(), 1);
452 assert_eq!(level.size(), 20.0);
453 assert_eq!(level.exposure(), 20.0);
454 }
455
456 #[rstest]
457 fn test_update_inserts_if_missing() {
458 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
459 let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
460 level.update(order);
461 assert_eq!(level.len(), 1);
462 assert_eq!(level.first().unwrap(), &order);
463 }
464
465 #[rstest]
466 fn test_update_zero_size_nonexistent() {
467 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
468 let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::zero(0), 1);
469 level.update(order);
470 assert_eq!(level.len(), 0);
471 }
472
473 #[rstest]
474 fn test_fifo_order_after_updates() {
475 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
476
477 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
478 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
479
480 level.add(order1);
481 level.add(order2);
482
483 let updated_order1 =
485 BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(15), 1);
486 level.update(updated_order1);
487
488 let orders = level.get_orders();
489 assert_eq!(orders.len(), 2);
490 assert_eq!(orders[0], updated_order1); assert_eq!(orders[1], order2); }
493
494 #[rstest]
495 fn test_insertion_order_after_mixed_operations() {
496 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
497 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
498 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
499 let order3 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(30), 3);
500
501 level.add(order1);
502 level.add(order2);
503 level.add(order3);
504
505 let updated_order2 =
507 BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(25), 2);
508 level.update(updated_order2);
509
510 level.delete(&order1);
512
513 let orders = level.get_orders();
514 assert_eq!(orders, vec![updated_order2, order3]);
515 }
516
517 #[rstest]
518 #[should_panic(expected = "assertion `left == right` failed")]
519 fn test_update_order_incorrect_price() {
520 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
521
522 let initial_order =
524 BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
525 level.add(initial_order);
526
527 let updated_order =
529 BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
530 level.update(updated_order);
531 }
532
533 #[rstest]
534 fn test_update_order_with_zero_size() {
535 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
536 let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
537 let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::zero(0), 0);
538
539 level.add(order1);
540 level.update(order2);
541 assert_eq!(level.len(), 0);
542 assert_eq!(level.size(), 0.0);
543 assert_eq!(level.exposure(), 0.0);
544 }
545
546 #[rstest]
547 fn test_delete_nonexistent_order() {
548 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
549 let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
550 level.delete(&order);
551 assert_eq!(level.len(), 0);
552 }
553
554 #[rstest]
555 fn test_delete_order() {
556 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
557 let order1_id = 0;
558 let order1 = BookOrder::new(
559 OrderSide::Buy,
560 Price::from("1.00"),
561 Quantity::from(10),
562 order1_id,
563 );
564 let order2_id = 1;
565 let order2 = BookOrder::new(
566 OrderSide::Buy,
567 Price::from("1.00"),
568 Quantity::from(20),
569 order2_id,
570 );
571
572 level.add(order1);
573 level.add(order2);
574 level.delete(&order1);
575 assert_eq!(level.len(), 1);
576 assert_eq!(level.size(), 20.0);
577 assert!(level.orders.contains_key(&order2_id));
578 assert_eq!(level.exposure(), 20.0);
579 }
580
581 #[rstest]
582 fn test_remove_order_by_id() {
583 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
584 let order1_id = 0;
585 let order1 = BookOrder::new(
586 OrderSide::Buy,
587 Price::from("1.00"),
588 Quantity::from(10),
589 order1_id,
590 );
591 let order2_id = 1;
592 let order2 = BookOrder::new(
593 OrderSide::Buy,
594 Price::from("1.00"),
595 Quantity::from(20),
596 order2_id,
597 );
598
599 level.add(order1);
600 level.add(order2);
601 level.remove_by_id(order2_id, 0, 0.into());
602 assert_eq!(level.len(), 1);
603 assert!(level.orders.contains_key(&order1_id));
604 assert_eq!(level.size(), 10.0);
605 assert_eq!(level.exposure(), 10.0);
606 }
607
608 #[rstest]
609 fn test_add_bulk_orders() {
610 let mut level = BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSide::Buy));
611 let order1_id = 0;
612 let order1 = BookOrder::new(
613 OrderSide::Buy,
614 Price::from("2.00"),
615 Quantity::from(10),
616 order1_id,
617 );
618 let order2_id = 1;
619 let order2 = BookOrder::new(
620 OrderSide::Buy,
621 Price::from("2.00"),
622 Quantity::from(20),
623 order2_id,
624 );
625
626 let orders = [order1, order2];
627 level.add_bulk(&orders);
628 assert_eq!(level.len(), 2);
629 assert_eq!(level.size(), 30.0);
630 assert_eq!(level.exposure(), 60.0);
631 }
632
633 #[rstest]
634 fn test_maximum_order_id() {
635 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
636
637 let order = BookOrder::new(
638 OrderSide::Buy,
639 Price::from("1.00"),
640 Quantity::from(10),
641 u64::MAX,
642 );
643 level.add(order);
644
645 assert_eq!(level.len(), 1);
646 assert_eq!(level.first().unwrap(), &order);
647 }
648
649 #[rstest]
650 #[should_panic(
651 expected = "Integrity error: order not found: order_id=1, sequence=2, ts_event=3"
652 )]
653 fn test_remove_nonexistent_order() {
654 let mut level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSide::Buy));
655 level.remove_by_id(1, 2, 3.into());
656 }
657
658 #[rstest]
659 fn test_size_raw() {
660 let mut level = BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSide::Buy));
661 let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
662 let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
663
664 level.add(order1);
665 level.add(order2);
666 assert_eq!(
667 level.size_raw(),
668 (30.0 * FIXED_SCALAR).round() as QuantityRaw
669 );
670 }
671
672 #[rstest]
673 fn test_size_decimal() {
674 let mut level = BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSide::Buy));
675 let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
676 let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
677
678 level.add(order1);
679 level.add(order2);
680 assert_eq!(level.size_decimal(), dec!(30.0));
681 }
682
683 #[rstest]
684 #[case::negative("-2", "10", 0)]
685 #[case::zero("0", "1", 0)]
686 #[case::small("2", "10", 20)]
687 fn test_exposure_raw_exact_whole(
688 #[case] price: &str,
689 #[case] size: &str,
690 #[case] expected_units: QuantityRaw,
691 ) {
692 let price = Price::from(price);
693 let mut level = BookLevel::new(BookPrice::new(price, OrderSide::Buy));
694 level.add(BookOrder::new(
695 OrderSide::Buy,
696 price,
697 Quantity::from(size),
698 0,
699 ));
700
701 assert_eq!(
702 level.exposure_raw(),
703 expected_units * FIXED_SCALAR as QuantityRaw
704 );
705 }
706
707 #[rstest]
708 fn test_exposure_raw_truncates_sub_raw_unit() {
709 let scalar = FIXED_SCALAR as QuantityRaw;
710 let price = Price::from_raw((scalar + 1) as PriceRaw, FIXED_PRECISION);
711 let size = Quantity::from_raw(scalar + 1, FIXED_PRECISION);
712 let mut level = BookLevel::new(BookPrice::new(price, OrderSide::Buy));
713 level.add(BookOrder::new(OrderSide::Buy, price, size, 0));
714
715 assert_eq!(level.exposure_raw(), scalar + 2);
716 }
717
718 #[rstest]
719 fn test_exposure_raw_accumulates_exactly() {
720 let mut level = BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSide::Buy));
721 let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
722 let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
723
724 level.add(order1);
725 level.add(order2);
726 assert_eq!(level.exposure_raw(), 60 * FIXED_SCALAR as QuantityRaw);
727 }
728
729 #[cfg(not(feature = "high-precision"))]
730 #[rstest]
731 fn test_exposure_raw_preserves_non_saturating_raw_units() {
732 let price = Price::from("9007199253.999999999");
733 let size = Quantity::from("2.000000001");
734 let mut level = BookLevel::new(BookPrice::new(price, OrderSide::Buy));
735 level.add(BookOrder::new(OrderSide::Buy, price, size, 0));
736
737 assert_eq!(level.exposure_raw(), 18_014_398_517_007_199_251);
738 }
739
740 #[cfg(feature = "high-precision")]
741 #[rstest]
742 fn test_exposure_raw_avoids_phantom_overflow() {
743 let scalar = FIXED_SCALAR as QuantityRaw;
744 let price_raw = 100_000 * scalar;
745 let size_raw = 100 * scalar;
746
747 assert_eq!(price_raw.checked_mul(size_raw), None);
748 assert_eq!(
749 calculate_exposure_raw(
750 price_raw as PriceRaw,
751 size_raw,
752 FIXED_PRECISION,
753 FIXED_PRECISION,
754 ),
755 10_000_000 * scalar
756 );
757 }
758
759 #[rstest]
760 fn test_exposure_raw_saturates_single_order() {
761 #[cfg(feature = "high-precision")]
762 let (price_str, qty_str) = ("1000000000000.00", "1000000000000.00");
763 #[cfg(not(feature = "high-precision"))]
764 let (price_str, qty_str) = ("100000000.00", "1000000000.00");
765
766 let mut level = BookLevel::new(BookPrice::new(Price::from(price_str), OrderSide::Buy));
767 let order = BookOrder::new(
768 OrderSide::Buy,
769 Price::from(price_str),
770 Quantity::from(qty_str),
771 0,
772 );
773
774 level.add(order);
775
776 assert_eq!(level.exposure_raw(), QuantityRaw::MAX);
777 }
778
779 #[rstest]
780 fn test_exposure_raw_accumulation_saturates() {
781 #[cfg(feature = "high-precision")]
782 let (price_str, qty_str, expected_single) = (
783 "100000000000.0",
784 "200000000000.0",
785 200_000_000_000_000_000_000_000_000_000_000_000_000,
786 );
787 #[cfg(not(feature = "high-precision"))]
788 let (price_str, qty_str, expected_single) =
789 ("2.0", "5000000000.0", 10_000_000_000_000_000_000);
790
791 let mut level = BookLevel::new(BookPrice::new(Price::from(price_str), OrderSide::Buy));
792 level.add(BookOrder::new(
793 OrderSide::Buy,
794 Price::from(price_str),
795 Quantity::from(qty_str),
796 0,
797 ));
798 assert_eq!(level.exposure_raw(), expected_single);
799
800 level.add(BookOrder::new(
801 OrderSide::Buy,
802 Price::from(price_str),
803 Quantity::from(qty_str),
804 1,
805 ));
806 assert_eq!(level.exposure_raw(), QuantityRaw::MAX);
807 }
808
809 #[cfg(feature = "defi")]
810 #[rstest]
811 fn test_exposure_raw_preserves_native_defi_scales() {
812 let price_precision = FIXED_PRECISION + 1;
813 let size_precision = FIXED_PRECISION + 2;
814 let price = Price::from_raw(
815 125 * 10_i128.pow(u32::from(price_precision - 2)),
816 price_precision,
817 );
818 let size = Quantity::from_raw(
819 24 * 10_u128.pow(u32::from(size_precision - 1)),
820 size_precision,
821 );
822 let mut level = BookLevel::new(BookPrice::new(price, OrderSide::Buy));
823 level.add(BookOrder::new(OrderSide::Buy, price, size, 0));
824
825 assert_eq!(level.exposure_raw(), 3 * FIXED_SCALAR as QuantityRaw);
826 }
827
828 #[cfg(feature = "defi")]
829 #[rstest]
830 #[case::native_price(1_250_000_000_000_000_000, 24_000_000_000_000_000, 18, 8)]
831 #[case::native_size(12_500_000_000_000_000, 2_400_000_000_000_000_000, 8, 18)]
832 fn test_exposure_raw_preserves_mixed_defi_scales(
833 #[case] price_raw: PriceRaw,
834 #[case] size_raw: QuantityRaw,
835 #[case] price_precision: u8,
836 #[case] size_precision: u8,
837 ) {
838 assert_eq!(
839 calculate_exposure_raw(price_raw, size_raw, price_precision, size_precision),
840 3 * FIXED_SCALAR as QuantityRaw
841 );
842 }
843}