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