Skip to main content

nautilus_model/orderbook/
own.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//! An `OwnBookOrder` for use with tracking own/user orders in L3 order books.
17//! It organizes orders into bid and ask ladders, maintains timestamps for state changes,
18//! and provides various methods for adding, updating, deleting, and querying orders.
19
20use std::{
21    cmp::Ordering,
22    collections::BTreeMap,
23    fmt::{Debug, Display},
24    hash::{Hash, Hasher},
25};
26
27use ahash::AHashSet;
28use indexmap::IndexMap;
29use nautilus_core::{DurationNanos, UnixNanos};
30use rust_decimal::Decimal;
31
32use super::{BookViewError, OwnBookError, display::pprint_own_book};
33use crate::{
34    enums::{OrderSide, OrderStatus, OrderType, TimeInForce},
35    identifiers::{ClientOrderId, InstrumentId, TraderId, VenueOrderId},
36    orderbook::BookPrice,
37    orders::{Order, OrderAny},
38    types::{Price, Quantity},
39};
40
41/// Represents an own/user order for a book.
42///
43/// This struct models an order that may be in-flight to the trading venue or actively working,
44/// depending on the value of the `status` field.
45#[repr(C)]
46#[derive(Clone, Copy, Eq)]
47#[cfg_attr(
48    feature = "python",
49    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
50)]
51#[cfg_attr(
52    feature = "python",
53    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
54)]
55pub struct OwnBookOrder {
56    /// The trader ID.
57    pub trader_id: TraderId,
58    /// The client order ID.
59    pub client_order_id: ClientOrderId,
60    /// The venue order ID (if assigned by the venue).
61    pub venue_order_id: Option<VenueOrderId>,
62    /// The order side (BUY or SELL).
63    pub side: OrderSide,
64    /// The order price.
65    pub price: Price,
66    /// The remaining order size (leaves quantity).
67    pub size: Quantity,
68    /// The order type.
69    pub order_type: OrderType,
70    /// The order time in force.
71    pub time_in_force: TimeInForce,
72    /// The current order status.
73    pub status: OrderStatus,
74    /// UNIX timestamp (nanoseconds) when the last order event occurred for this order.
75    pub ts_last: UnixNanos,
76    /// UNIX timestamp (nanoseconds) when the order was accepted (zero unless accepted).
77    pub ts_accepted: UnixNanos,
78    /// UNIX timestamp (nanoseconds) when the order was submitted (zero unless submitted).
79    pub ts_submitted: UnixNanos,
80    /// UNIX timestamp (nanoseconds) when the order was initialized.
81    pub ts_init: UnixNanos,
82}
83
84impl OwnBookOrder {
85    /// Creates a new [`OwnBookOrder`] instance.
86    #[must_use]
87    #[expect(clippy::too_many_arguments)]
88    pub fn new(
89        trader_id: TraderId,
90        client_order_id: ClientOrderId,
91        venue_order_id: Option<VenueOrderId>,
92        side: OrderSide,
93        price: Price,
94        size: Quantity,
95        order_type: OrderType,
96        time_in_force: TimeInForce,
97        status: OrderStatus,
98        ts_last: UnixNanos,
99        ts_accepted: UnixNanos,
100        ts_submitted: UnixNanos,
101        ts_init: UnixNanos,
102    ) -> Self {
103        Self {
104            trader_id,
105            client_order_id,
106            venue_order_id,
107            side,
108            price,
109            size,
110            order_type,
111            time_in_force,
112            status,
113            ts_last,
114            ts_accepted,
115            ts_submitted,
116            ts_init,
117        }
118    }
119
120    /// Returns a [`BookPrice`] from this order.
121    #[must_use]
122    pub fn to_book_price(&self) -> BookPrice {
123        BookPrice::new(self.price, self.side)
124    }
125
126    /// Returns the order exposure as an `f64`.
127    #[must_use]
128    pub fn exposure(&self) -> f64 {
129        self.price.as_f64() * self.size.as_f64()
130    }
131
132    /// Returns the signed order exposure as an `f64`.
133    #[must_use]
134    pub fn signed_size(&self) -> f64 {
135        match self.side {
136            OrderSide::Buy => self.size.as_f64(),
137            OrderSide::Sell => -(self.size.as_f64()),
138        }
139    }
140}
141
142impl Ord for OwnBookOrder {
143    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
144        self.client_order_id.cmp(&other.client_order_id)
145    }
146}
147
148impl PartialOrd for OwnBookOrder {
149    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
150        Some(self.cmp(other))
151    }
152}
153
154impl PartialEq for OwnBookOrder {
155    fn eq(&self, other: &Self) -> bool {
156        self.client_order_id == other.client_order_id
157    }
158}
159
160impl Hash for OwnBookOrder {
161    fn hash<H: Hasher>(&self, state: &mut H) {
162        self.client_order_id.hash(state);
163    }
164}
165
166impl Debug for OwnBookOrder {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        write!(
169            f,
170            "{}(trader_id={}, client_order_id={}, venue_order_id={:?}, side={}, price={}, size={}, order_type={}, time_in_force={}, status={}, ts_last={}, ts_accepted={}, ts_submitted={}, ts_init={})",
171            stringify!(OwnBookOrder),
172            self.trader_id,
173            self.client_order_id,
174            self.venue_order_id,
175            self.side,
176            self.price,
177            self.size,
178            self.order_type,
179            self.time_in_force,
180            self.status,
181            self.ts_last,
182            self.ts_accepted,
183            self.ts_submitted,
184            self.ts_init,
185        )
186    }
187}
188
189impl Display for OwnBookOrder {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        write!(
192            f,
193            "{},{},{:?},{},{},{},{},{},{},{},{},{},{}",
194            self.trader_id,
195            self.client_order_id,
196            self.venue_order_id,
197            self.side,
198            self.price,
199            self.size,
200            self.order_type,
201            self.time_in_force,
202            self.status,
203            self.ts_last,
204            self.ts_accepted,
205            self.ts_submitted,
206            self.ts_init,
207        )
208    }
209}
210
211#[derive(Clone, Debug)]
212#[cfg_attr(
213    feature = "python",
214    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
215)]
216#[cfg_attr(
217    feature = "python",
218    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
219)]
220pub struct OwnOrderBook {
221    /// The instrument ID for the order book.
222    pub instrument_id: InstrumentId,
223    /// The timestamp of the last event applied to the order book.
224    pub ts_last: UnixNanos,
225    /// The current count of updates applied to the order book.
226    pub update_count: u64,
227    pub(crate) bids: OwnBookLadder,
228    pub(crate) asks: OwnBookLadder,
229}
230
231impl PartialEq for OwnOrderBook {
232    fn eq(&self, other: &Self) -> bool {
233        self.instrument_id == other.instrument_id
234    }
235}
236
237impl Display for OwnOrderBook {
238    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239        write!(
240            f,
241            "{}(instrument_id={}, orders={}, update_count={})",
242            stringify!(OwnOrderBook),
243            self.instrument_id,
244            self.bids.cache.len() + self.asks.cache.len(),
245            self.update_count,
246        )
247    }
248}
249
250impl OwnOrderBook {
251    /// Creates a new [`OwnOrderBook`] instance.
252    #[must_use]
253    pub fn new(instrument_id: InstrumentId) -> Self {
254        Self {
255            instrument_id,
256            ts_last: UnixNanos::default(),
257            update_count: 0,
258            bids: OwnBookLadder::new(OrderSide::Buy),
259            asks: OwnBookLadder::new(OrderSide::Sell),
260        }
261    }
262
263    fn increment(&mut self, order: &OwnBookOrder) {
264        self.ts_last = order.ts_last;
265        self.update_count += 1;
266    }
267
268    /// Resets the order book to its initial empty state.
269    pub fn reset(&mut self) {
270        self.bids.clear();
271        self.asks.clear();
272        self.ts_last = UnixNanos::default();
273        self.update_count = 0;
274    }
275
276    /// Adds an own order to its side of the book, replacing any order there with the same client
277    /// order ID.
278    pub fn add(&mut self, order: OwnBookOrder) {
279        self.increment(&order);
280        match order.side {
281            OrderSide::Buy => self.bids.add(order),
282            OrderSide::Sell => self.asks.add(order),
283        }
284    }
285
286    /// Updates an existing own order in the book, removing it if the size becomes zero.
287    ///
288    /// # Errors
289    ///
290    /// Returns an error if the order is not found.
291    pub fn update(&mut self, order: OwnBookOrder) -> Result<(), OwnBookError> {
292        let result = match order.side {
293            OrderSide::Buy => self.bids.update(order),
294            OrderSide::Sell => self.asks.update(order),
295        };
296
297        if result.is_ok() {
298            self.increment(&order);
299        }
300
301        result
302    }
303
304    /// Deletes an own order from the book.
305    ///
306    /// # Errors
307    ///
308    /// Returns an error if the order is not found.
309    pub fn delete(&mut self, order: OwnBookOrder) -> Result<(), OwnBookError> {
310        let result = match order.side {
311            OrderSide::Buy => self.bids.delete(order),
312            OrderSide::Sell => self.asks.delete(order),
313        };
314
315        if result.is_ok() {
316            self.increment(&order);
317        }
318
319        result
320    }
321
322    /// Clears all orders from both sides of the book.
323    pub fn clear(&mut self) {
324        self.bids.clear();
325        self.asks.clear();
326    }
327
328    /// Returns an iterator over bid price levels.
329    pub fn bids(&self) -> impl Iterator<Item = &OwnBookLevel> {
330        self.bids.levels.values()
331    }
332
333    /// Returns an iterator over ask price levels.
334    pub fn asks(&self) -> impl Iterator<Item = &OwnBookLevel> {
335        self.asks.levels.values()
336    }
337
338    /// Returns the client order IDs currently on the bid side.
339    #[must_use]
340    pub fn bid_client_order_ids(&self) -> Vec<ClientOrderId> {
341        self.bids.cache.keys().copied().collect()
342    }
343
344    /// Returns the client order IDs currently on the ask side.
345    #[must_use]
346    pub fn ask_client_order_ids(&self) -> Vec<ClientOrderId> {
347        self.asks.cache.keys().copied().collect()
348    }
349
350    /// Return whether the given client order ID is in the own book.
351    #[must_use]
352    pub fn is_order_in_book(&self, client_order_id: &ClientOrderId) -> bool {
353        self.asks.cache.contains_key(client_order_id)
354            || self.bids.cache.contains_key(client_order_id)
355    }
356
357    /// Maps bid price levels to their own orders, excluding empty levels after filtering.
358    ///
359    /// Filters by `status` if provided. When `ts_now` is provided, only includes orders whose
360    /// acceptance time plus `accepted_buffer_ns` is at or before `ts_now`. When `ts_now` is
361    /// `None`, acceptance-time filtering is disabled.
362    ///
363    /// # Panics
364    ///
365    /// Panics if `accepted_buffer_ns` is positive and `ts_now` is `None`.
366    #[must_use]
367    pub fn bids_as_map(
368        &self,
369        status: Option<&AHashSet<OrderStatus>>,
370        accepted_buffer_ns: Option<u64>,
371        ts_now: Option<u64>,
372    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
373        filter_orders(self.bids(), status, accepted_buffer_ns, ts_now)
374    }
375
376    /// Maps ask price levels to their own orders, excluding empty levels after filtering.
377    ///
378    /// Filters by `status` if provided. When `ts_now` is provided, only includes orders whose
379    /// acceptance time plus `accepted_buffer_ns` is at or before `ts_now`. When `ts_now` is
380    /// `None`, acceptance-time filtering is disabled.
381    ///
382    /// # Panics
383    ///
384    /// Panics if `accepted_buffer_ns` is positive and `ts_now` is `None`.
385    #[must_use]
386    pub fn asks_as_map(
387        &self,
388        status: Option<&AHashSet<OrderStatus>>,
389        accepted_buffer_ns: Option<u64>,
390        ts_now: Option<u64>,
391    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
392        filter_orders(self.asks(), status, accepted_buffer_ns, ts_now)
393    }
394
395    /// Aggregates own bid quantities per price level, omitting zero-quantity levels.
396    ///
397    /// Filters by `status` if provided, including only matching orders. When `ts_now` is provided,
398    /// only includes orders whose acceptance time plus `accepted_buffer_ns` is at or before
399    /// `ts_now`. When `ts_now` is `None`, acceptance-time filtering is disabled.
400    ///
401    /// If `group_size` is provided, groups quantities into price buckets.
402    /// If `depth` is provided, limits the number of price levels returned.
403    ///
404    /// # Panics
405    ///
406    /// Panics if `accepted_buffer_ns` is positive and `ts_now` is `None`.
407    #[must_use]
408    pub fn bid_quantity(
409        &self,
410        status: Option<&AHashSet<OrderStatus>>,
411        depth: Option<usize>,
412        group_size: Option<Decimal>,
413        accepted_buffer_ns: Option<u64>,
414        ts_now: Option<u64>,
415    ) -> IndexMap<Decimal, Decimal> {
416        let quantities = self
417            .bids_as_map(status, accepted_buffer_ns, ts_now)
418            .into_iter()
419            .map(|(price, orders)| (price, sum_order_sizes(orders.iter())))
420            .filter(|(_, quantity)| *quantity > Decimal::ZERO)
421            .collect::<IndexMap<Decimal, Decimal>>();
422
423        if let Some(group_size) = group_size {
424            group_quantities(quantities, group_size, depth, true)
425        } else if let Some(depth) = depth {
426            quantities.into_iter().take(depth).collect()
427        } else {
428            quantities
429        }
430    }
431
432    /// Aggregates own ask quantities per price level, omitting zero-quantity levels.
433    ///
434    /// Filters by `status` if provided, including only matching orders. When `ts_now` is provided,
435    /// only includes orders whose acceptance time plus `accepted_buffer_ns` is at or before
436    /// `ts_now`. When `ts_now` is `None`, acceptance-time filtering is disabled.
437    ///
438    /// If `group_size` is provided, groups quantities into price buckets.
439    /// If `depth` is provided, limits the number of price levels returned.
440    ///
441    /// # Panics
442    ///
443    /// Panics if `accepted_buffer_ns` is positive and `ts_now` is `None`.
444    #[must_use]
445    pub fn ask_quantity(
446        &self,
447        status: Option<&AHashSet<OrderStatus>>,
448        depth: Option<usize>,
449        group_size: Option<Decimal>,
450        accepted_buffer_ns: Option<u64>,
451        ts_now: Option<u64>,
452    ) -> IndexMap<Decimal, Decimal> {
453        let quantities = self
454            .asks_as_map(status, accepted_buffer_ns, ts_now)
455            .into_iter()
456            .map(|(price, orders)| {
457                let quantity = sum_order_sizes(orders.iter());
458                (price, quantity)
459            })
460            .filter(|(_, quantity)| *quantity > Decimal::ZERO)
461            .collect::<IndexMap<Decimal, Decimal>>();
462
463        if let Some(group_size) = group_size {
464            group_quantities(quantities, group_size, depth, false)
465        } else if let Some(depth) = depth {
466            quantities.into_iter().take(depth).collect()
467        } else {
468            quantities
469        }
470    }
471
472    /// Returns a new own book containing this books orders plus parity-transformed opposite orders.
473    ///
474    /// Opposite asks are transformed into bids with price `1 - price`.
475    /// Opposite bids are transformed into asks with price `1 - price`.
476    ///
477    /// # Errors
478    ///
479    /// Returns an error if `self` and `opposite` have the same instrument ID.
480    pub fn combined_with_opposite(&self, opposite: &Self) -> Result<Self, BookViewError> {
481        if self.instrument_id == opposite.instrument_id {
482            return Err(BookViewError::OppositeInstrumentMatch(
483                self.instrument_id,
484                opposite.instrument_id,
485            ));
486        }
487
488        let mut combined = self.clone();
489
490        for level in opposite.asks() {
491            for order in level.iter() {
492                combined.add(transform_opposite_order(*order, OrderSide::Buy));
493            }
494        }
495
496        for level in opposite.bids() {
497            for order in level.iter() {
498                combined.add(transform_opposite_order(*order, OrderSide::Sell));
499            }
500        }
501
502        Ok(combined)
503    }
504
505    /// Return a formatted string representation of the order book.
506    #[must_use]
507    pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
508        pprint_own_book(self, num_levels, group_size)
509    }
510
511    pub fn audit_open_orders(&mut self, open_order_ids: &AHashSet<ClientOrderId>) {
512        log::debug!("Auditing {self}");
513
514        // Audit bids
515        let bids_to_remove: Vec<ClientOrderId> = self
516            .bids
517            .cache
518            .keys()
519            .filter(|&key| !open_order_ids.contains(key))
520            .copied()
521            .collect();
522
523        // Audit asks
524        let asks_to_remove: Vec<ClientOrderId> = self
525            .asks
526            .cache
527            .keys()
528            .filter(|&key| !open_order_ids.contains(key))
529            .copied()
530            .collect();
531
532        for client_order_id in bids_to_remove {
533            log_audit_removal(&client_order_id);
534            if let Err(e) = self.bids.remove(&client_order_id) {
535                log::error!("{e}");
536            }
537        }
538
539        for client_order_id in asks_to_remove {
540            log_audit_removal(&client_order_id);
541            if let Err(e) = self.asks.remove(&client_order_id) {
542                log::error!("{e}");
543            }
544        }
545    }
546}
547
548fn log_audit_removal(client_order_id: &ClientOrderId) {
549    log::warn!("Audit removing {client_order_id} from own book, absent from valid order IDs");
550}
551
552fn transform_opposite_order(order: OwnBookOrder, side: OrderSide) -> OwnBookOrder {
553    let parity_price = Price::from_decimal(Decimal::ONE - order.price.as_decimal())
554        .expect("Invalid parity transformed price for OwnOrderBook::combined_with_opposite");
555
556    OwnBookOrder::new(
557        order.trader_id,
558        order.client_order_id,
559        order.venue_order_id,
560        side,
561        parity_price,
562        order.size,
563        order.order_type,
564        order.time_in_force,
565        order.status,
566        order.ts_last,
567        order.ts_accepted,
568        order.ts_submitted,
569        order.ts_init,
570    )
571}
572
573/// Validates the acceptance-time filter arguments.
574///
575/// # Errors
576///
577/// Returns an error if `accepted_buffer_ns` is positive and `ts_now` is `None`.
578pub(crate) fn validate_accepted_buffer(
579    accepted_buffer_ns: Option<u64>,
580    ts_now: Option<u64>,
581) -> Result<(), &'static str> {
582    if accepted_buffer_ns.is_some_and(|buffer| buffer > 0) && ts_now.is_none() {
583        Err("ts_now must be provided when accepted_buffer_ns > 0")
584    } else {
585        Ok(())
586    }
587}
588
589/// Filters orders by status and accepted timestamp.
590///
591/// `accepted_buffer_ns` acts as a grace period after `ts_accepted`. Orders whose
592/// `ts_accepted` is still zero (e.g. SUBMITTED/PENDING state before an ACCEPTED
593/// event) will pass the buffer check once `ts_now` exceeds the buffer, even though
594/// they have not been venue-acknowledged yet. Callers that want to hide inflight
595/// orders must additionally filter by `OrderStatus` (for example, include only
596/// `ACCEPTED` / `PARTIALLY_FILLED`).
597///
598/// # Panics
599///
600/// Panics if `accepted_buffer_ns` is positive and `ts_now` is `None`.
601fn filter_orders<'a>(
602    levels: impl Iterator<Item = &'a OwnBookLevel>,
603    status: Option<&AHashSet<OrderStatus>>,
604    accepted_buffer_ns: Option<u64>,
605    ts_now: Option<u64>,
606) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
607    validate_accepted_buffer(accepted_buffer_ns, ts_now).unwrap_or_else(|e| panic!("{e}"));
608    let accepted_buffer_ns = accepted_buffer_ns.unwrap_or(0);
609
610    levels
611        .map(|level| {
612            let orders = level
613                .orders
614                .values()
615                .filter(|order| status.is_none_or(|f| f.contains(&order.status)))
616                .filter(|order| {
617                    ts_now.is_none_or(|ts_now| {
618                        order
619                            .ts_accepted
620                            .checked_add(DurationNanos::new(accepted_buffer_ns))
621                            .is_some_and(|eligible_at| eligible_at.as_u64() <= ts_now)
622                    })
623                })
624                .copied()
625                .collect::<Vec<OwnBookOrder>>();
626
627            (level.price.value.as_decimal(), orders)
628        })
629        .filter(|(_, orders)| !orders.is_empty())
630        .collect::<IndexMap<Decimal, Vec<OwnBookOrder>>>()
631}
632
633fn group_quantities(
634    quantities: IndexMap<Decimal, Decimal>,
635    group_size: Decimal,
636    depth: Option<usize>,
637    is_bid: bool,
638) -> IndexMap<Decimal, Decimal> {
639    if group_size <= Decimal::ZERO {
640        log::warn!("Invalid group_size: {group_size}, must be positive; returning empty map");
641        return IndexMap::new();
642    }
643
644    let mut grouped = IndexMap::new();
645    let depth = depth.unwrap_or(usize::MAX);
646
647    for (price, size) in quantities {
648        let grouped_price = if is_bid {
649            (price / group_size).floor() * group_size
650        } else {
651            (price / group_size).ceil() * group_size
652        };
653
654        grouped
655            .entry(grouped_price)
656            .and_modify(|total| *total += size)
657            .or_insert(size);
658
659        if grouped.len() > depth {
660            if is_bid {
661                // For bids, remove the lowest price level
662                if let Some((lowest_price, _)) = grouped.iter().min_by_key(|(price, _)| *price) {
663                    let lowest_price = *lowest_price;
664                    grouped.shift_remove(&lowest_price);
665                }
666            } else {
667                // For asks, remove the highest price level
668                if let Some((highest_price, _)) = grouped.iter().max_by_key(|(price, _)| *price) {
669                    let highest_price = *highest_price;
670                    grouped.shift_remove(&highest_price);
671                }
672            }
673        }
674    }
675
676    grouped
677}
678
679fn sum_order_sizes<'a, I>(orders: I) -> Decimal
680where
681    I: Iterator<Item = &'a OwnBookOrder>,
682{
683    orders.map(|order| order.size.as_decimal()).sum()
684}
685
686/// Represents a ladder of price levels for one side of an order book.
687#[derive(Clone)]
688pub(crate) struct OwnBookLadder {
689    pub side: OrderSide,
690    pub levels: BTreeMap<BookPrice, OwnBookLevel>,
691    pub cache: IndexMap<ClientOrderId, BookPrice>,
692}
693
694impl OwnBookLadder {
695    /// Creates a new [`OwnBookLadder`] instance.
696    #[must_use]
697    pub(crate) fn new(side: OrderSide) -> Self {
698        Self {
699            side,
700            levels: BTreeMap::new(),
701            cache: IndexMap::new(),
702        }
703    }
704
705    /// Returns the number of price levels in the ladder.
706    #[must_use]
707    #[allow(dead_code)]
708    pub(crate) fn len(&self) -> usize {
709        self.levels.len()
710    }
711
712    /// Returns true if the ladder has no price levels.
713    #[must_use]
714    #[allow(dead_code)]
715    pub(crate) fn is_empty(&self) -> bool {
716        self.levels.is_empty()
717    }
718
719    /// Removes all orders and price levels from the ladder.
720    pub(crate) fn clear(&mut self) {
721        self.levels.clear();
722        self.cache.clear();
723    }
724
725    /// Adds an order to the ladder at its price level.
726    ///
727    /// Re-adding a client order ID at a different price moves the order to the new level's
728    /// FIFO tail, so each ID lives at exactly one level.
729    pub(crate) fn add(&mut self, order: OwnBookOrder) {
730        let book_price = order.to_book_price();
731
732        if self
733            .cache
734            .get(&order.client_order_id)
735            .is_some_and(|price| *price != book_price)
736            && let Err(e) = self.remove(&order.client_order_id)
737        {
738            log::error!("{e}");
739        }
740
741        self.cache.insert(order.client_order_id, book_price);
742
743        if let Some(level) = self.levels.get_mut(&book_price) {
744            level.add(order);
745        } else {
746            let level = OwnBookLevel::from_order(order);
747            self.levels.insert(book_price, level);
748        }
749    }
750
751    /// Updates an existing order in the ladder, moving it to a new price level if needed.
752    /// Removes the order if the size becomes zero.
753    ///
754    /// # Errors
755    ///
756    /// Returns an error if the order is not found.
757    pub(crate) fn update(&mut self, order: OwnBookOrder) -> Result<(), OwnBookError> {
758        let client_order_id = order.client_order_id;
759
760        let Some(price) = self.cache.get(&order.client_order_id).copied() else {
761            return Err(OwnBookError::OrderNotFoundInCache { client_order_id });
762        };
763
764        let Some(level) = self.levels.get_mut(&price) else {
765            return Err(OwnBookError::CachedLevelMissing {
766                client_order_id,
767                price,
768            });
769        };
770
771        if order.price == level.price.value {
772            level.update(order);
773            if order.size.is_zero() {
774                self.cache.shift_remove(&order.client_order_id);
775
776                if level.is_empty() {
777                    self.levels.remove(&price);
778                }
779            }
780            return Ok(());
781        }
782
783        level.delete(&client_order_id)?;
784        self.cache.shift_remove(&order.client_order_id);
785
786        if level.is_empty() {
787            self.levels.remove(&price);
788        }
789
790        if !order.size.is_zero() {
791            self.add(order);
792        }
793
794        Ok(())
795    }
796
797    /// Deletes an order from the ladder.
798    ///
799    /// # Errors
800    ///
801    /// Returns an error if the order is not found.
802    pub(crate) fn delete(&mut self, order: OwnBookOrder) -> Result<(), OwnBookError> {
803        self.remove(&order.client_order_id)
804    }
805
806    /// Removes an order by its ID from the ladder.
807    ///
808    /// # Errors
809    ///
810    /// Returns an error if the order is not found.
811    pub(crate) fn remove(&mut self, client_order_id: &ClientOrderId) -> Result<(), OwnBookError> {
812        let Some(price) = self.cache.get(client_order_id).copied() else {
813            return Err(OwnBookError::OrderNotFoundInCache {
814                client_order_id: *client_order_id,
815            });
816        };
817
818        let Some(level) = self.levels.get_mut(&price) else {
819            return Err(OwnBookError::CachedLevelMissing {
820                client_order_id: *client_order_id,
821                price,
822            });
823        };
824
825        level.delete(client_order_id)?;
826
827        if level.is_empty() {
828            self.levels.remove(&price);
829        }
830        self.cache.shift_remove(client_order_id);
831
832        Ok(())
833    }
834
835    /// Returns the total size of all orders in the ladder.
836    #[must_use]
837    #[allow(dead_code)]
838    pub(crate) fn sizes(&self) -> f64 {
839        self.levels.values().map(OwnBookLevel::size).sum()
840    }
841
842    /// Returns the total value exposure (price * size) of all orders in the ladder.
843    #[must_use]
844    #[allow(dead_code)]
845    pub(crate) fn exposures(&self) -> f64 {
846        self.levels.values().map(OwnBookLevel::exposure).sum()
847    }
848
849    /// Returns the best price level in the ladder.
850    #[must_use]
851    #[allow(dead_code)]
852    pub(crate) fn top(&self) -> Option<&OwnBookLevel> {
853        self.levels.values().next()
854    }
855}
856
857impl Debug for OwnBookLadder {
858    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
859        f.debug_struct(stringify!(OwnBookLadder))
860            .field("side", &self.side)
861            .field("levels", &self.levels)
862            .field("cache", &self.cache)
863            .finish()
864    }
865}
866
867impl Display for OwnBookLadder {
868    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
869        writeln!(f, "{}(side={})", stringify!(OwnBookLadder), self.side)?;
870        for (price, level) in &self.levels {
871            writeln!(f, "  {} -> {} orders", price, level.len())?;
872        }
873        Ok(())
874    }
875}
876
877#[derive(Clone, Debug)]
878pub struct OwnBookLevel {
879    pub price: BookPrice,
880    pub orders: IndexMap<ClientOrderId, OwnBookOrder>,
881}
882
883impl OwnBookLevel {
884    /// Creates a new [`OwnBookLevel`] instance.
885    #[must_use]
886    pub fn new(price: BookPrice) -> Self {
887        Self {
888            price,
889            orders: IndexMap::new(),
890        }
891    }
892
893    /// Creates a new [`OwnBookLevel`] from an order, using the order's price and side.
894    #[must_use]
895    pub fn from_order(order: OwnBookOrder) -> Self {
896        let mut level = Self {
897            price: order.to_book_price(),
898            orders: IndexMap::new(),
899        };
900        level.orders.insert(order.client_order_id, order);
901        level
902    }
903
904    /// Returns the number of orders at this price level.
905    #[must_use]
906    pub fn len(&self) -> usize {
907        self.orders.len()
908    }
909
910    /// Returns true if this price level has no orders.
911    #[must_use]
912    pub fn is_empty(&self) -> bool {
913        self.orders.is_empty()
914    }
915
916    /// Returns a reference to the first order at this price level in FIFO order.
917    #[must_use]
918    pub fn first(&self) -> Option<&OwnBookOrder> {
919        self.orders.get_index(0).map(|(_key, order)| order)
920    }
921
922    /// Returns an iterator over the orders at this price level in FIFO order.
923    pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
924        self.orders.values()
925    }
926
927    /// Returns all orders at this price level in FIFO insertion order.
928    #[must_use]
929    pub fn get_orders(&self) -> Vec<OwnBookOrder> {
930        self.orders.values().copied().collect()
931    }
932
933    /// Returns the total size of all orders at this price level as a float.
934    #[must_use]
935    pub fn size(&self) -> f64 {
936        self.orders.values().map(|order| order.size.as_f64()).sum()
937    }
938
939    /// Returns the total size of all orders at this price level as a decimal.
940    #[must_use]
941    pub fn size_decimal(&self) -> Decimal {
942        self.orders
943            .values()
944            .map(|order| order.size.as_decimal())
945            .sum()
946    }
947
948    /// Returns the total exposure (price * size) of all orders at this price level as a float.
949    #[must_use]
950    pub fn exposure(&self) -> f64 {
951        self.orders
952            .values()
953            .map(|order| order.price.as_f64() * order.size.as_f64())
954            .sum()
955    }
956
957    /// Adds multiple orders to this price level in FIFO order. Orders must match the level's price.
958    pub fn add_bulk(&mut self, orders: &[OwnBookOrder]) {
959        for order in orders {
960            self.add(*order);
961        }
962    }
963
964    /// Adds an order to this price level. Order must match the level's price.
965    pub fn add(&mut self, order: OwnBookOrder) {
966        debug_assert_eq!(order.price, self.price.value);
967
968        self.orders.insert(order.client_order_id, order);
969    }
970
971    /// Updates an order at this price level, inserting it if missing. Updated order
972    /// must match the level's price. Removes the order if the size becomes zero.
973    pub fn update(&mut self, order: OwnBookOrder) {
974        debug_assert_eq!(order.price, self.price.value);
975
976        if order.size.is_zero() {
977            self.orders.shift_remove(&order.client_order_id);
978        } else {
979            self.orders.insert(order.client_order_id, order);
980        }
981    }
982
983    /// Deletes an order from this price level.
984    ///
985    /// # Errors
986    ///
987    /// Returns an error if the order is not found.
988    pub fn delete(&mut self, client_order_id: &ClientOrderId) -> Result<(), OwnBookError> {
989        if self.orders.shift_remove(client_order_id).is_none() {
990            return Err(OwnBookError::OrderNotFoundAtLevel {
991                client_order_id: *client_order_id,
992                price: self.price,
993            });
994        }
995        Ok(())
996    }
997}
998
999impl PartialEq for OwnBookLevel {
1000    fn eq(&self, other: &Self) -> bool {
1001        self.price == other.price
1002    }
1003}
1004
1005impl Eq for OwnBookLevel {}
1006
1007impl PartialOrd for OwnBookLevel {
1008    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1009        Some(self.cmp(other))
1010    }
1011}
1012
1013impl Ord for OwnBookLevel {
1014    fn cmp(&self, other: &Self) -> Ordering {
1015        self.price.cmp(&other.price)
1016    }
1017}
1018
1019/// Returns whether an order belongs in an own order book.
1020///
1021/// An eligible order has a price, does not use `IOC` or `FOK` time in force, is not held by the
1022/// order emulator, and has a base-denominated quantity. Emulated orders never rest in the public
1023/// book, and a quote-quantity order becomes eligible once an update converts it to base units.
1024#[must_use]
1025pub fn should_handle_own_book_order(order: &OrderAny) -> bool {
1026    order.has_price()
1027        && !matches!(order.time_in_force(), TimeInForce::Ioc | TimeInForce::Fok)
1028        && order.emulation_trigger().is_none()
1029        && !order.is_quote_quantity()
1030}