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