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;
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 (`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: 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 the book.
277    pub fn add(&mut self, order: OwnBookOrder) {
278        self.increment(&order);
279        match order.side {
280            OrderSide::Buy => self.bids.add(order),
281            OrderSide::Sell => self.asks.add(order),
282        }
283    }
284
285    /// Updates an existing own order in the book.
286    ///
287    /// # Errors
288    ///
289    /// Returns an error if the order is not found.
290    pub fn update(&mut self, order: OwnBookOrder) -> Result<(), OwnBookError> {
291        let result = match order.side {
292            OrderSide::Buy => self.bids.update(order),
293            OrderSide::Sell => self.asks.update(order),
294        };
295
296        if result.is_ok() {
297            self.increment(&order);
298        }
299
300        result
301    }
302
303    /// Deletes an own order from the book.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if the order is not found.
308    pub fn delete(&mut self, order: OwnBookOrder) -> Result<(), OwnBookError> {
309        let result = match order.side {
310            OrderSide::Buy => self.bids.delete(order),
311            OrderSide::Sell => self.asks.delete(order),
312        };
313
314        if result.is_ok() {
315            self.increment(&order);
316        }
317
318        result
319    }
320
321    /// Clears all orders from both sides of the book.
322    pub fn clear(&mut self) {
323        self.bids.clear();
324        self.asks.clear();
325    }
326
327    /// Returns an iterator over bid price levels.
328    pub fn bids(&self) -> impl Iterator<Item = &OwnBookLevel> {
329        self.bids.levels.values()
330    }
331
332    /// Returns an iterator over ask price levels.
333    pub fn asks(&self) -> impl Iterator<Item = &OwnBookLevel> {
334        self.asks.levels.values()
335    }
336
337    /// Returns the client order IDs currently on the bid side.
338    #[must_use]
339    pub fn bid_client_order_ids(&self) -> Vec<ClientOrderId> {
340        self.bids.cache.keys().copied().collect()
341    }
342
343    /// Returns the client order IDs currently on the ask side.
344    #[must_use]
345    pub fn ask_client_order_ids(&self) -> Vec<ClientOrderId> {
346        self.asks.cache.keys().copied().collect()
347    }
348
349    /// Return whether the given client order ID is in the own book.
350    #[must_use]
351    pub fn is_order_in_book(&self, client_order_id: &ClientOrderId) -> bool {
352        self.asks.cache.contains_key(client_order_id)
353            || self.bids.cache.contains_key(client_order_id)
354    }
355
356    /// Maps bid price levels to their own orders, excluding empty levels after filtering.
357    ///
358    /// Filters by `status` if provided. When `ts_now` is provided, only includes orders whose
359    /// acceptance time plus `accepted_buffer_ns` is at or before `ts_now`. When `ts_now` is
360    /// `None`, acceptance-time filtering is disabled.
361    ///
362    /// # Panics
363    ///
364    /// Panics if `accepted_buffer_ns` is positive and `ts_now` is `None`.
365    #[must_use]
366    pub fn bids_as_map(
367        &self,
368        status: Option<&AHashSet<OrderStatus>>,
369        accepted_buffer_ns: Option<u64>,
370        ts_now: Option<u64>,
371    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
372        filter_orders(self.bids(), status, accepted_buffer_ns, ts_now)
373    }
374
375    /// Maps ask price levels to their own orders, excluding empty levels after filtering.
376    ///
377    /// Filters by `status` if provided. When `ts_now` is provided, only includes orders whose
378    /// acceptance time plus `accepted_buffer_ns` is at or before `ts_now`. When `ts_now` is
379    /// `None`, acceptance-time filtering is disabled.
380    ///
381    /// # Panics
382    ///
383    /// Panics if `accepted_buffer_ns` is positive and `ts_now` is `None`.
384    #[must_use]
385    pub fn asks_as_map(
386        &self,
387        status: Option<&AHashSet<OrderStatus>>,
388        accepted_buffer_ns: Option<u64>,
389        ts_now: Option<u64>,
390    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
391        filter_orders(self.asks(), status, accepted_buffer_ns, ts_now)
392    }
393
394    /// Aggregates own bid quantities per price level, omitting zero-quantity levels.
395    ///
396    /// Filters by `status` if provided, including only matching orders. When `ts_now` is provided,
397    /// only includes orders whose acceptance time plus `accepted_buffer_ns` is at or before
398    /// `ts_now`. When `ts_now` is `None`, acceptance-time filtering is disabled.
399    ///
400    /// If `group_size` is provided, groups quantities into price buckets.
401    /// If `depth` is provided, limits the number of price levels returned.
402    ///
403    /// # Panics
404    ///
405    /// Panics if `accepted_buffer_ns` is positive and `ts_now` is `None`.
406    #[must_use]
407    pub fn bid_quantity(
408        &self,
409        status: Option<&AHashSet<OrderStatus>>,
410        depth: Option<usize>,
411        group_size: Option<Decimal>,
412        accepted_buffer_ns: Option<u64>,
413        ts_now: Option<u64>,
414    ) -> IndexMap<Decimal, Decimal> {
415        let quantities = self
416            .bids_as_map(status, accepted_buffer_ns, ts_now)
417            .into_iter()
418            .map(|(price, orders)| (price, sum_order_sizes(orders.iter())))
419            .filter(|(_, quantity)| *quantity > Decimal::ZERO)
420            .collect::<IndexMap<Decimal, Decimal>>();
421
422        if let Some(group_size) = group_size {
423            group_quantities(quantities, group_size, depth, true)
424        } else if let Some(depth) = depth {
425            quantities.into_iter().take(depth).collect()
426        } else {
427            quantities
428        }
429    }
430
431    /// Aggregates own ask quantities per price level, omitting zero-quantity levels.
432    ///
433    /// Filters by `status` if provided, including only matching orders. When `ts_now` is provided,
434    /// only includes orders whose acceptance time plus `accepted_buffer_ns` is at or before
435    /// `ts_now`. When `ts_now` is `None`, acceptance-time filtering is disabled.
436    ///
437    /// If `group_size` is provided, groups quantities into price buckets.
438    /// If `depth` is provided, limits the number of price levels returned.
439    ///
440    /// # Panics
441    ///
442    /// Panics if `accepted_buffer_ns` is positive and `ts_now` is `None`.
443    #[must_use]
444    pub fn ask_quantity(
445        &self,
446        status: Option<&AHashSet<OrderStatus>>,
447        depth: Option<usize>,
448        group_size: Option<Decimal>,
449        accepted_buffer_ns: Option<u64>,
450        ts_now: Option<u64>,
451    ) -> IndexMap<Decimal, Decimal> {
452        let quantities = self
453            .asks_as_map(status, accepted_buffer_ns, ts_now)
454            .into_iter()
455            .map(|(price, orders)| {
456                let quantity = sum_order_sizes(orders.iter());
457                (price, quantity)
458            })
459            .filter(|(_, quantity)| *quantity > Decimal::ZERO)
460            .collect::<IndexMap<Decimal, Decimal>>();
461
462        if let Some(group_size) = group_size {
463            group_quantities(quantities, group_size, depth, false)
464        } else if let Some(depth) = depth {
465            quantities.into_iter().take(depth).collect()
466        } else {
467            quantities
468        }
469    }
470
471    /// Returns a new own book containing this books orders plus parity-transformed opposite orders.
472    ///
473    /// Opposite asks are transformed into bids with price `1 - price`.
474    /// Opposite bids are transformed into asks with price `1 - price`.
475    ///
476    /// # Errors
477    ///
478    /// Returns [`BookViewError::OppositeInstrumentMatch`] if `self` and `opposite` have the
479    /// 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_error(&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_error(&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_error(client_order_id: &ClientOrderId) {
549    log::error!(
550        "Audit error - {client_order_id} absent from valid order IDs, deleting from own book"
551    );
552}
553
554fn transform_opposite_order(order: OwnBookOrder, side: OrderSide) -> OwnBookOrder {
555    let parity_price = Price::from_decimal(Decimal::ONE - order.price.as_decimal())
556        .expect("Invalid parity transformed price for OwnOrderBook::combined_with_opposite");
557
558    OwnBookOrder::new(
559        order.trader_id,
560        order.client_order_id,
561        order.venue_order_id,
562        side,
563        parity_price,
564        order.size,
565        order.order_type,
566        order.time_in_force,
567        order.status,
568        order.ts_last,
569        order.ts_accepted,
570        order.ts_submitted,
571        order.ts_init,
572    )
573}
574
575/// Validates the acceptance-time filter arguments.
576///
577/// # Errors
578///
579/// Returns an error if `accepted_buffer_ns` is positive and `ts_now` is `None`.
580pub(crate) fn validate_accepted_buffer(
581    accepted_buffer_ns: Option<u64>,
582    ts_now: Option<u64>,
583) -> Result<(), &'static str> {
584    if accepted_buffer_ns.is_some_and(|buffer| buffer > 0) && ts_now.is_none() {
585        Err("ts_now must be provided when accepted_buffer_ns > 0")
586    } else {
587        Ok(())
588    }
589}
590
591/// Filters orders by status and accepted timestamp.
592///
593/// `accepted_buffer_ns` acts as a grace period after `ts_accepted`. Orders whose
594/// `ts_accepted` is still zero (e.g. SUBMITTED/PENDING state before an ACCEPTED
595/// event) will pass the buffer check once `ts_now` exceeds the buffer, even though
596/// they have not been venue-acknowledged yet. Callers that want to hide inflight
597/// orders must additionally filter by `OrderStatus` (for example, include only
598/// `ACCEPTED` / `PARTIALLY_FILLED`).
599///
600/// # Panics
601///
602/// Panics if `accepted_buffer_ns` is positive and `ts_now` is `None`.
603fn filter_orders<'a>(
604    levels: impl Iterator<Item = &'a OwnBookLevel>,
605    status: Option<&AHashSet<OrderStatus>>,
606    accepted_buffer_ns: Option<u64>,
607    ts_now: Option<u64>,
608) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
609    validate_accepted_buffer(accepted_buffer_ns, ts_now).unwrap_or_else(|e| panic!("{e}"));
610    let accepted_buffer_ns = accepted_buffer_ns.unwrap_or(0);
611
612    levels
613        .map(|level| {
614            let orders = level
615                .orders
616                .values()
617                .filter(|order| status.is_none_or(|f| f.contains(&order.status)))
618                .filter(|order| {
619                    ts_now.is_none_or(|ts_now| {
620                        order
621                            .ts_accepted
622                            .checked_add(accepted_buffer_ns)
623                            .is_some_and(|eligible_at| eligible_at.as_u64() <= ts_now)
624                    })
625                })
626                .copied()
627                .collect::<Vec<OwnBookOrder>>();
628
629            (level.price.value.as_decimal(), orders)
630        })
631        .filter(|(_, orders)| !orders.is_empty())
632        .collect::<IndexMap<Decimal, Vec<OwnBookOrder>>>()
633}
634
635fn group_quantities(
636    quantities: IndexMap<Decimal, Decimal>,
637    group_size: Decimal,
638    depth: Option<usize>,
639    is_bid: bool,
640) -> IndexMap<Decimal, Decimal> {
641    if group_size <= Decimal::ZERO {
642        log::warn!("Invalid group_size: {group_size}, must be positive; returning empty map");
643        return IndexMap::new();
644    }
645
646    let mut grouped = IndexMap::new();
647    let depth = depth.unwrap_or(usize::MAX);
648
649    for (price, size) in quantities {
650        let grouped_price = if is_bid {
651            (price / group_size).floor() * group_size
652        } else {
653            (price / group_size).ceil() * group_size
654        };
655
656        grouped
657            .entry(grouped_price)
658            .and_modify(|total| *total += size)
659            .or_insert(size);
660
661        if grouped.len() > depth {
662            if is_bid {
663                // For bids, remove the lowest price level
664                if let Some((lowest_price, _)) = grouped.iter().min_by_key(|(price, _)| *price) {
665                    let lowest_price = *lowest_price;
666                    grouped.shift_remove(&lowest_price);
667                }
668            } else {
669                // For asks, remove the highest price level
670                if let Some((highest_price, _)) = grouped.iter().max_by_key(|(price, _)| *price) {
671                    let highest_price = *highest_price;
672                    grouped.shift_remove(&highest_price);
673                }
674            }
675        }
676    }
677
678    grouped
679}
680
681fn sum_order_sizes<'a, I>(orders: I) -> Decimal
682where
683    I: Iterator<Item = &'a OwnBookOrder>,
684{
685    orders.map(|order| order.size.as_decimal()).sum()
686}
687
688/// Represents a ladder of price levels for one side of an order book.
689#[derive(Clone)]
690pub(crate) struct OwnBookLadder {
691    pub side: OrderSide,
692    pub levels: BTreeMap<BookPrice, OwnBookLevel>,
693    pub cache: IndexMap<ClientOrderId, BookPrice>,
694}
695
696impl OwnBookLadder {
697    /// Creates a new [`OwnBookLadder`] instance.
698    #[must_use]
699    pub(crate) fn new(side: OrderSide) -> Self {
700        Self {
701            side,
702            levels: BTreeMap::new(),
703            cache: IndexMap::new(),
704        }
705    }
706
707    /// Returns the number of price levels in the ladder.
708    #[must_use]
709    #[allow(dead_code)]
710    pub(crate) fn len(&self) -> usize {
711        self.levels.len()
712    }
713
714    /// Returns true if the ladder has no price levels.
715    #[must_use]
716    #[allow(dead_code)]
717    pub(crate) fn is_empty(&self) -> bool {
718        self.levels.is_empty()
719    }
720
721    /// Removes all orders and price levels from the ladder.
722    pub(crate) fn clear(&mut self) {
723        self.levels.clear();
724        self.cache.clear();
725    }
726
727    /// Adds an order to the ladder at its price level.
728    pub(crate) fn add(&mut self, order: OwnBookOrder) {
729        let book_price = order.to_book_price();
730        self.cache.insert(order.client_order_id, book_price);
731
732        if let Some(level) = self.levels.get_mut(&book_price) {
733            level.add(order);
734        } else {
735            let level = OwnBookLevel::from_order(order);
736            self.levels.insert(book_price, level);
737        }
738    }
739
740    /// Updates an existing order in the ladder, moving it to a new price level if needed.
741    ///
742    /// # Errors
743    ///
744    /// Returns an error if the order is not found.
745    pub(crate) fn update(&mut self, order: OwnBookOrder) -> Result<(), OwnBookError> {
746        let client_order_id = order.client_order_id;
747
748        let Some(price) = self.cache.get(&order.client_order_id).copied() else {
749            return Err(OwnBookError::OrderNotFoundInCache { client_order_id });
750        };
751
752        let Some(level) = self.levels.get_mut(&price) else {
753            return Err(OwnBookError::CachedLevelMissing {
754                client_order_id,
755                price,
756            });
757        };
758
759        if order.price == level.price.value {
760            level.update(order);
761            if order.size.is_zero() {
762                self.cache.shift_remove(&order.client_order_id);
763
764                if level.is_empty() {
765                    self.levels.remove(&price);
766                }
767            }
768            return Ok(());
769        }
770
771        level.delete(&client_order_id)?;
772        self.cache.shift_remove(&order.client_order_id);
773
774        if level.is_empty() {
775            self.levels.remove(&price);
776        }
777
778        self.add(order);
779        Ok(())
780    }
781
782    /// Deletes an order from the ladder.
783    ///
784    /// # Errors
785    ///
786    /// Returns an error if the order is not found.
787    pub(crate) fn delete(&mut self, order: OwnBookOrder) -> Result<(), OwnBookError> {
788        self.remove(&order.client_order_id)
789    }
790
791    /// Removes an order by its ID from the ladder.
792    ///
793    /// # Errors
794    ///
795    /// Returns an error if the order is not found.
796    pub(crate) fn remove(&mut self, client_order_id: &ClientOrderId) -> Result<(), OwnBookError> {
797        let Some(price) = self.cache.get(client_order_id).copied() else {
798            return Err(OwnBookError::OrderNotFoundInCache {
799                client_order_id: *client_order_id,
800            });
801        };
802
803        let Some(level) = self.levels.get_mut(&price) else {
804            return Err(OwnBookError::CachedLevelMissing {
805                client_order_id: *client_order_id,
806                price,
807            });
808        };
809
810        level.delete(client_order_id)?;
811
812        if level.is_empty() {
813            self.levels.remove(&price);
814        }
815        self.cache.shift_remove(client_order_id);
816
817        Ok(())
818    }
819
820    /// Returns the total size of all orders in the ladder.
821    #[must_use]
822    #[allow(dead_code)]
823    pub(crate) fn sizes(&self) -> f64 {
824        self.levels.values().map(OwnBookLevel::size).sum()
825    }
826
827    /// Returns the total value exposure (price * size) of all orders in the ladder.
828    #[must_use]
829    #[allow(dead_code)]
830    pub(crate) fn exposures(&self) -> f64 {
831        self.levels.values().map(OwnBookLevel::exposure).sum()
832    }
833
834    /// Returns the best price level in the ladder.
835    #[must_use]
836    #[allow(dead_code)]
837    pub(crate) fn top(&self) -> Option<&OwnBookLevel> {
838        self.levels.values().next()
839    }
840}
841
842impl Debug for OwnBookLadder {
843    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
844        f.debug_struct(stringify!(OwnBookLadder))
845            .field("side", &self.side)
846            .field("levels", &self.levels)
847            .field("cache", &self.cache)
848            .finish()
849    }
850}
851
852impl Display for OwnBookLadder {
853    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
854        writeln!(f, "{}(side={})", stringify!(OwnBookLadder), self.side)?;
855        for (price, level) in &self.levels {
856            writeln!(f, "  {} -> {} orders", price, level.len())?;
857        }
858        Ok(())
859    }
860}
861
862#[derive(Clone, Debug)]
863pub struct OwnBookLevel {
864    pub price: BookPrice,
865    pub orders: IndexMap<ClientOrderId, OwnBookOrder>,
866}
867
868impl OwnBookLevel {
869    /// Creates a new [`OwnBookLevel`] instance.
870    #[must_use]
871    pub fn new(price: BookPrice) -> Self {
872        Self {
873            price,
874            orders: IndexMap::new(),
875        }
876    }
877
878    /// Creates a new [`OwnBookLevel`] from an order, using the order's price and side.
879    #[must_use]
880    pub fn from_order(order: OwnBookOrder) -> Self {
881        let mut level = Self {
882            price: order.to_book_price(),
883            orders: IndexMap::new(),
884        };
885        level.orders.insert(order.client_order_id, order);
886        level
887    }
888
889    /// Returns the number of orders at this price level.
890    #[must_use]
891    pub fn len(&self) -> usize {
892        self.orders.len()
893    }
894
895    /// Returns true if this price level has no orders.
896    #[must_use]
897    pub fn is_empty(&self) -> bool {
898        self.orders.is_empty()
899    }
900
901    /// Returns a reference to the first order at this price level in FIFO order.
902    #[must_use]
903    pub fn first(&self) -> Option<&OwnBookOrder> {
904        self.orders.get_index(0).map(|(_key, order)| order)
905    }
906
907    /// Returns an iterator over the orders at this price level in FIFO order.
908    pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
909        self.orders.values()
910    }
911
912    /// Returns all orders at this price level in FIFO insertion order.
913    #[must_use]
914    pub fn get_orders(&self) -> Vec<OwnBookOrder> {
915        self.orders.values().copied().collect()
916    }
917
918    /// Returns the total size of all orders at this price level as a float.
919    #[must_use]
920    pub fn size(&self) -> f64 {
921        self.orders.values().map(|order| order.size.as_f64()).sum()
922    }
923
924    /// Returns the total size of all orders at this price level as a decimal.
925    #[must_use]
926    pub fn size_decimal(&self) -> Decimal {
927        self.orders
928            .values()
929            .map(|order| order.size.as_decimal())
930            .sum()
931    }
932
933    /// Returns the total exposure (price * size) of all orders at this price level as a float.
934    #[must_use]
935    pub fn exposure(&self) -> f64 {
936        self.orders
937            .values()
938            .map(|order| order.price.as_f64() * order.size.as_f64())
939            .sum()
940    }
941
942    /// Adds multiple orders to this price level in FIFO order. Orders must match the level's price.
943    pub fn add_bulk(&mut self, orders: &[OwnBookOrder]) {
944        for order in orders {
945            self.add(*order);
946        }
947    }
948
949    /// Adds an order to this price level. Order must match the level's price.
950    pub fn add(&mut self, order: OwnBookOrder) {
951        debug_assert_eq!(order.price, self.price.value);
952
953        self.orders.insert(order.client_order_id, order);
954    }
955
956    /// Updates an order at this price level, inserting it if missing. Updated order
957    /// must match the level's price. Removes the order if the size becomes zero.
958    pub fn update(&mut self, order: OwnBookOrder) {
959        debug_assert_eq!(order.price, self.price.value);
960
961        if order.size.is_zero() {
962            self.orders.shift_remove(&order.client_order_id);
963        } else {
964            self.orders.insert(order.client_order_id, order);
965        }
966    }
967
968    /// Deletes an order from this price level.
969    ///
970    /// # Errors
971    ///
972    /// Returns an error if the order is not found.
973    pub fn delete(&mut self, client_order_id: &ClientOrderId) -> Result<(), OwnBookError> {
974        if self.orders.shift_remove(client_order_id).is_none() {
975            return Err(OwnBookError::OrderNotFoundAtLevel {
976                client_order_id: *client_order_id,
977                price: self.price,
978            });
979        }
980        Ok(())
981    }
982}
983
984impl PartialEq for OwnBookLevel {
985    fn eq(&self, other: &Self) -> bool {
986        self.price == other.price
987    }
988}
989
990impl Eq for OwnBookLevel {}
991
992impl PartialOrd for OwnBookLevel {
993    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
994        Some(self.cmp(other))
995    }
996}
997
998impl Ord for OwnBookLevel {
999    fn cmp(&self, other: &Self) -> Ordering {
1000        self.price.cmp(&other.price)
1001    }
1002}
1003
1004#[must_use]
1005pub fn should_handle_own_book_order(order: &OrderAny) -> bool {
1006    order.has_price() && !matches!(order.time_in_force(), TimeInForce::Ioc | TimeInForce::Fok)
1007}