Skip to main content

nautilus_model/orderbook/
level.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Represents a discrete price level in an order book.
17
18use 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/// Represents a discrete price level in an order book.
39///
40/// Orders are stored in an [`IndexMap`] which preserves FIFO (insertion) order.
41#[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    /// Creates a new [`BookLevel`] instance.
57    #[must_use]
58    pub fn new(price: BookPrice) -> Self {
59        Self {
60            price,
61            orders: IndexMap::new(),
62        }
63    }
64
65    /// Creates a new [`BookLevel`] from an order, using the order's price and side.
66    #[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    /// Returns the number of orders at this price level.
82    #[must_use]
83    pub fn len(&self) -> usize {
84        self.orders.len()
85    }
86
87    /// Returns true if this price level has no orders.
88    #[must_use]
89    pub fn is_empty(&self) -> bool {
90        self.orders.is_empty()
91    }
92
93    /// Returns a reference to the first order at this price level in FIFO order.
94    #[inline]
95    #[must_use]
96    pub fn first(&self) -> Option<&BookOrder> {
97        self.orders.get_index(0).map(|(_key, order)| order)
98    }
99
100    /// Returns an iterator over the orders at this price level in FIFO order.
101    pub fn iter(&self) -> impl Iterator<Item = &BookOrder> {
102        self.orders.values()
103    }
104
105    /// Returns all orders at this price level in FIFO insertion order.
106    #[must_use]
107    pub fn get_orders(&self) -> Vec<BookOrder> {
108        self.orders.values().copied().collect()
109    }
110
111    /// Returns the total size of all orders at this price level as a float.
112    #[must_use]
113    pub fn size(&self) -> f64 {
114        self.orders.values().map(|o| o.size.as_f64()).sum()
115    }
116
117    /// Returns the total size of all orders at this price level as raw integer units.
118    ///
119    /// # Panics
120    ///
121    /// Panics if the total raw size exceeds [`QuantityRaw::MAX`].
122    #[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    /// Returns the total size of all orders at this price level as raw integer units.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the total raw size exceeds [`QuantityRaw::MAX`].
133    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    /// Returns the total size of all orders at this price level as a decimal.
145    #[must_use]
146    pub fn size_decimal(&self) -> Decimal {
147        self.orders.values().map(|o| o.size.as_decimal()).sum()
148    }
149
150    /// Returns the total exposure (price * size) of all orders at this price level as a float.
151    #[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    /// Returns the total exposure (price * size) of all orders at this price level as raw integer units.
160    ///
161    /// Fixed-scale orders contribute `price.raw * size.raw / FIXED_SCALAR`.
162    /// Native DeFi scales are normalized to the same fixed-scale result.
163    /// Division truncates toward zero.
164    /// Non-positive prices contribute zero.
165    /// Saturates at `QuantityRaw::MAX` if the total exposure would overflow.
166    #[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    /// Adds multiple orders to this price level in FIFO order. Orders must match the level's price.
182    pub fn add_bulk(&mut self, orders: &[BookOrder]) {
183        for order in orders {
184            self.add(*order);
185        }
186    }
187
188    /// Adds an order to this price level. Order must match the level's price.
189    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    /// Updates an order at this price level, inserting it if missing. Updated order
205    /// must match the level's price. Removes the order if the size becomes zero.
206    pub fn update(&mut self, order: BookOrder) {
207        debug_assert_eq!(order.price, self.price.value);
208
209        if order.size.is_zero() {
210            // Updating non-existent order to zero size is a no-op, which is valid
211            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    /// Deletes an order from this price level.
223    pub fn delete(&mut self, order: &BookOrder) {
224        self.orders.shift_remove(&order.order_id);
225    }
226
227    /// Removes an order by its ID.
228    ///
229    /// # Panics
230    ///
231    /// Panics if no order with the given `order_id` exists at this level.
232    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), // Incorrect price
353        ];
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); // Checks FIFO order maintained
429        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        // Update order1 size
485        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); // First order still first
492        assert_eq!(orders[1], order2); // Second order still second
493    }
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        // Update order2 (should keep its position)
507        let updated_order2 =
508            BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(25), 2);
509        level.update(updated_order2);
510
511        // Remove order1; order2 (updated) should now be first
512        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        // Add initial order at correct price level
524        let initial_order =
525            BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
526        level.add(initial_order);
527
528        // Attempt to update with order at incorrect price level
529        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}