nautilus_model/orderbook/
own.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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, HashMap, HashSet},
23    fmt::{Debug, Display},
24    hash::{Hash, Hasher},
25};
26
27use indexmap::IndexMap;
28use nautilus_core::{UnixNanos, time::nanos_since_unix_epoch};
29use rust_decimal::Decimal;
30
31use super::display::pprint_own_book;
32use crate::{
33    enums::{OrderSideSpecified, OrderStatus, OrderType, TimeInForce},
34    identifiers::{ClientOrderId, InstrumentId, TraderId, VenueOrderId},
35    orderbook::BookPrice,
36    orders::{Order, OrderAny},
37    types::{Price, Quantity},
38};
39
40/// Represents an own/user order for a book.
41///
42/// This struct models an order that may be in-flight to the trading venue or actively working,
43/// depending on the value of the `status` field.
44#[repr(C)]
45#[derive(Clone, Copy, Eq)]
46#[cfg_attr(
47    feature = "python",
48    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
49)]
50pub struct OwnBookOrder {
51    /// The trader ID.
52    pub trader_id: TraderId,
53    /// The client order ID.
54    pub client_order_id: ClientOrderId,
55    /// The venue order ID (if assigned by the venue).
56    pub venue_order_id: Option<VenueOrderId>,
57    /// The specified order side (BUY or SELL).
58    pub side: OrderSideSpecified,
59    /// The order price.
60    pub price: Price,
61    /// The order size.
62    pub size: Quantity,
63    /// The order type.
64    pub order_type: OrderType,
65    /// The order time in force.
66    pub time_in_force: TimeInForce,
67    /// The current order status (SUBMITTED/ACCEPTED/PENDING_CANCEL/PENDING_UPDATE/PARTIALLY_FILLED).
68    pub status: OrderStatus,
69    /// UNIX timestamp (nanoseconds) when the last order event occurred for this order.
70    pub ts_last: UnixNanos,
71    /// UNIX timestamp (nanoseconds) when the order was accepted (zero unless accepted).
72    pub ts_accepted: UnixNanos,
73    /// UNIX timestamp (nanoseconds) when the order was submitted (zero unless submitted).
74    pub ts_submitted: UnixNanos,
75    /// UNIX timestamp (nanoseconds) when the order was initialized.
76    pub ts_init: UnixNanos,
77}
78
79impl OwnBookOrder {
80    /// Creates a new [`OwnBookOrder`] instance.
81    #[must_use]
82    #[allow(clippy::too_many_arguments)]
83    pub fn new(
84        trader_id: TraderId,
85        client_order_id: ClientOrderId,
86        venue_order_id: Option<VenueOrderId>,
87        side: OrderSideSpecified,
88        price: Price,
89        size: Quantity,
90        order_type: OrderType,
91        time_in_force: TimeInForce,
92        status: OrderStatus,
93        ts_last: UnixNanos,
94        ts_accepted: UnixNanos,
95        ts_submitted: UnixNanos,
96        ts_init: UnixNanos,
97    ) -> Self {
98        Self {
99            trader_id,
100            client_order_id,
101            venue_order_id,
102            side,
103            price,
104            size,
105            order_type,
106            time_in_force,
107            status,
108            ts_last,
109            ts_accepted,
110            ts_submitted,
111            ts_init,
112        }
113    }
114
115    /// Returns a [`BookPrice`] from this order.
116    #[must_use]
117    pub fn to_book_price(&self) -> BookPrice {
118        BookPrice::new(self.price, self.side)
119    }
120
121    /// Returns the order exposure as an `f64`.
122    #[must_use]
123    pub fn exposure(&self) -> f64 {
124        self.price.as_f64() * self.size.as_f64()
125    }
126
127    /// Returns the signed order exposure as an `f64`.
128    #[must_use]
129    pub fn signed_size(&self) -> f64 {
130        match self.side {
131            OrderSideSpecified::Buy => self.size.as_f64(),
132            OrderSideSpecified::Sell => -(self.size.as_f64()),
133        }
134    }
135}
136
137impl Ord for OwnBookOrder {
138    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
139        // Compare solely based on ts_init.
140        self.ts_init.cmp(&other.ts_init)
141    }
142}
143
144impl PartialOrd for OwnBookOrder {
145    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
146        Some(self.cmp(other))
147    }
148}
149
150impl PartialEq for OwnBookOrder {
151    fn eq(&self, other: &Self) -> bool {
152        self.client_order_id == other.client_order_id
153            && self.status == other.status
154            && self.ts_last == other.ts_last
155    }
156}
157
158impl Hash for OwnBookOrder {
159    fn hash<H: Hasher>(&self, state: &mut H) {
160        self.client_order_id.hash(state);
161    }
162}
163
164impl Debug for OwnBookOrder {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        write!(
167            f,
168            "{}(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={})",
169            stringify!(OwnBookOrder),
170            self.trader_id,
171            self.client_order_id,
172            self.venue_order_id,
173            self.side,
174            self.price,
175            self.size,
176            self.order_type,
177            self.time_in_force,
178            self.status,
179            self.ts_last,
180            self.ts_accepted,
181            self.ts_submitted,
182            self.ts_init,
183        )
184    }
185}
186
187impl Display for OwnBookOrder {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        write!(
190            f,
191            "{},{},{:?},{},{},{},{},{},{},{},{},{},{}",
192            self.trader_id,
193            self.client_order_id,
194            self.venue_order_id,
195            self.side,
196            self.price,
197            self.size,
198            self.order_type,
199            self.time_in_force,
200            self.status,
201            self.ts_last,
202            self.ts_accepted,
203            self.ts_submitted,
204            self.ts_init,
205        )
206    }
207}
208
209#[derive(Debug)]
210#[cfg_attr(
211    feature = "python",
212    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
213)]
214pub struct OwnOrderBook {
215    /// The instrument ID for the order book.
216    pub instrument_id: InstrumentId,
217    /// The timestamp of the last event applied to the order book.
218    pub ts_last: UnixNanos,
219    /// The current count of updates applied to the order book.
220    pub update_count: u64,
221    pub(crate) bids: OwnBookLadder,
222    pub(crate) asks: OwnBookLadder,
223}
224
225impl PartialEq for OwnOrderBook {
226    fn eq(&self, other: &Self) -> bool {
227        self.instrument_id == other.instrument_id
228    }
229}
230
231impl Display for OwnOrderBook {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        write!(
234            f,
235            "{}(instrument_id={}, orders={}, update_count={})",
236            stringify!(OwnOrderBook),
237            self.instrument_id,
238            self.bids.cache.len() + self.asks.cache.len(),
239            self.update_count,
240        )
241    }
242}
243
244impl OwnOrderBook {
245    /// Creates a new [`OwnOrderBook`] instance.
246    #[must_use]
247    pub fn new(instrument_id: InstrumentId) -> Self {
248        Self {
249            instrument_id,
250            ts_last: UnixNanos::default(),
251            update_count: 0,
252            bids: OwnBookLadder::new(OrderSideSpecified::Buy),
253            asks: OwnBookLadder::new(OrderSideSpecified::Sell),
254        }
255    }
256
257    fn increment(&mut self, order: &OwnBookOrder) {
258        self.ts_last = order.ts_last;
259        self.update_count += 1;
260    }
261
262    /// Resets the order book to its initial empty state.
263    pub fn reset(&mut self) {
264        self.bids.clear();
265        self.asks.clear();
266        self.ts_last = UnixNanos::default();
267        self.update_count = 0;
268    }
269
270    /// Adds an own order to the book.
271    pub fn add(&mut self, order: OwnBookOrder) {
272        self.increment(&order);
273        match order.side {
274            OrderSideSpecified::Buy => self.bids.add(order),
275            OrderSideSpecified::Sell => self.asks.add(order),
276        }
277    }
278
279    /// Updates an existing own order in the book.
280    pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
281        self.increment(&order);
282        match order.side {
283            OrderSideSpecified::Buy => self.bids.update(order),
284            OrderSideSpecified::Sell => self.asks.update(order),
285        }
286    }
287
288    /// Deletes an own order from the book.
289    pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
290        self.increment(&order);
291        match order.side {
292            OrderSideSpecified::Buy => self.bids.delete(order),
293            OrderSideSpecified::Sell => self.asks.delete(order),
294        }
295    }
296
297    /// Clears all orders from both sides of the book.
298    pub fn clear(&mut self) {
299        self.bids.clear();
300        self.asks.clear();
301    }
302
303    /// Returns an iterator over bid price levels.
304    pub fn bids(&self) -> impl Iterator<Item = &OwnBookLevel> {
305        self.bids.levels.values()
306    }
307
308    /// Returns an iterator over ask price levels.
309    pub fn asks(&self) -> impl Iterator<Item = &OwnBookLevel> {
310        self.asks.levels.values()
311    }
312
313    /// Returns the client order IDs currently on the bid side.
314    pub fn bid_client_order_ids(&self) -> Vec<ClientOrderId> {
315        self.bids.cache.keys().cloned().collect()
316    }
317
318    /// Returns the client order IDs currently on the ask side.
319    pub fn ask_client_order_ids(&self) -> Vec<ClientOrderId> {
320        self.asks.cache.keys().cloned().collect()
321    }
322
323    /// Return whether the given client order ID is in the own book.
324    pub fn is_order_in_book(&self, client_order_id: &ClientOrderId) -> bool {
325        self.asks.cache.contains_key(client_order_id)
326            || self.bids.cache.contains_key(client_order_id)
327    }
328
329    /// Maps bid price levels to their own orders, excluding empty levels after filtering.
330    ///
331    /// Filters by `status` if provided. With `accepted_buffer_ns`, only includes orders accepted
332    /// at least that many nanoseconds before `ts_now` (defaults to now).
333    pub fn bids_as_map(
334        &self,
335        status: Option<HashSet<OrderStatus>>,
336        accepted_buffer_ns: Option<u64>,
337        ts_now: Option<u64>,
338    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
339        filter_orders(self.bids(), status.as_ref(), accepted_buffer_ns, ts_now)
340    }
341
342    /// Maps ask price levels to their own orders, excluding empty levels after filtering.
343    ///
344    /// Filters by `status` if provided. With `accepted_buffer_ns`, only includes orders accepted
345    /// at least that many nanoseconds before `ts_now` (defaults to now).
346    pub fn asks_as_map(
347        &self,
348        status: Option<HashSet<OrderStatus>>,
349        accepted_buffer_ns: Option<u64>,
350        ts_now: Option<u64>,
351    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
352        filter_orders(self.asks(), status.as_ref(), accepted_buffer_ns, ts_now)
353    }
354
355    /// Aggregates own bid quantities per price level, omitting zero-quantity levels.
356    ///
357    /// Filters by `status` if provided, including only matching orders. With `accepted_buffer_ns`,
358    /// only includes orders accepted at least that many nanoseconds before `ts_now` (defaults to now).
359    pub fn bid_quantity(
360        &self,
361        status: Option<HashSet<OrderStatus>>,
362        accepted_buffer_ns: Option<u64>,
363        ts_now: Option<u64>,
364    ) -> IndexMap<Decimal, Decimal> {
365        self.bids_as_map(status, accepted_buffer_ns, ts_now)
366            .into_iter()
367            .map(|(price, orders)| (price, sum_order_sizes(orders.iter())))
368            .filter(|(_, quantity)| *quantity > Decimal::ZERO)
369            .collect()
370    }
371
372    /// Aggregates own ask quantities per price level, omitting zero-quantity levels.
373    ///
374    /// Filters by `status` if provided, including only matching orders. With `accepted_buffer_ns`,
375    /// only includes orders accepted at least that many nanoseconds before `ts_now` (defaults to now).
376    pub fn ask_quantity(
377        &self,
378        status: Option<HashSet<OrderStatus>>,
379        accepted_buffer_ns: Option<u64>,
380        ts_now: Option<u64>,
381    ) -> IndexMap<Decimal, Decimal> {
382        self.asks_as_map(status, accepted_buffer_ns, ts_now)
383            .into_iter()
384            .map(|(price, orders)| {
385                let quantity = sum_order_sizes(orders.iter());
386                (price, quantity)
387            })
388            .filter(|(_, quantity)| *quantity > Decimal::ZERO)
389            .collect()
390    }
391
392    /// Groups own bid quantities by price into buckets, truncating to a maximum depth.
393    ///
394    /// Filters by `status` if provided. With `accepted_buffer_ns`, only includes orders accepted
395    /// at least that many nanoseconds before `ts_now` (defaults to now).
396    pub fn group_bids(
397        &self,
398        group_size: Decimal,
399        depth: Option<usize>,
400        status: Option<HashSet<OrderStatus>>,
401        accepted_buffer_ns: Option<u64>,
402        ts_now: Option<u64>,
403    ) -> IndexMap<Decimal, Decimal> {
404        let quantities = self.bid_quantity(status, accepted_buffer_ns, ts_now);
405        group_quantities(quantities, group_size, depth, true)
406    }
407
408    /// Groups own ask quantities by price into buckets, truncating to a maximum depth.
409    ///
410    /// Filters by `status` if provided. With `accepted_buffer_ns`, only includes orders accepted
411    /// at least that many nanoseconds before `ts_now` (defaults to now).
412    pub fn group_asks(
413        &self,
414        group_size: Decimal,
415        depth: Option<usize>,
416        status: Option<HashSet<OrderStatus>>,
417        accepted_buffer_ns: Option<u64>,
418        ts_now: Option<u64>,
419    ) -> IndexMap<Decimal, Decimal> {
420        let quantities = self.ask_quantity(status, accepted_buffer_ns, ts_now);
421        group_quantities(quantities, group_size, depth, false)
422    }
423
424    /// Return a formatted string representation of the order book.
425    #[must_use]
426    pub fn pprint(&self, num_levels: usize) -> String {
427        pprint_own_book(&self.bids, &self.asks, num_levels)
428    }
429
430    pub fn audit_open_orders(&mut self, open_order_ids: &HashSet<ClientOrderId>) {
431        log::debug!("Auditing {self}");
432
433        // Audit bids
434        let bids_to_remove: Vec<ClientOrderId> = self
435            .bids
436            .cache
437            .keys()
438            .filter(|&key| !open_order_ids.contains(key))
439            .cloned()
440            .collect();
441
442        // Audit asks
443        let asks_to_remove: Vec<ClientOrderId> = self
444            .asks
445            .cache
446            .keys()
447            .filter(|&key| !open_order_ids.contains(key))
448            .cloned()
449            .collect();
450
451        for client_order_id in bids_to_remove {
452            log_audit_error(&client_order_id);
453            if let Err(e) = self.bids.remove(&client_order_id) {
454                log::error!("{e}");
455            }
456        }
457
458        for client_order_id in asks_to_remove {
459            log_audit_error(&client_order_id);
460            if let Err(e) = self.asks.remove(&client_order_id) {
461                log::error!("{e}");
462            }
463        }
464    }
465}
466
467fn log_audit_error(client_order_id: &ClientOrderId) {
468    log::error!(
469        "Audit error - {} cached order already closed, deleting from own book",
470        client_order_id
471    );
472}
473
474fn filter_orders<'a>(
475    levels: impl Iterator<Item = &'a OwnBookLevel>,
476    status: Option<&HashSet<OrderStatus>>,
477    accepted_buffer_ns: Option<u64>,
478    ts_now: Option<u64>,
479) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
480    let accepted_buffer_ns = accepted_buffer_ns.unwrap_or(0);
481    let ts_now = ts_now.unwrap_or_else(nanos_since_unix_epoch);
482    levels
483        .map(|level| {
484            let orders = level
485                .orders
486                .values()
487                .filter(|order| status.is_none_or(|f| f.contains(&order.status)))
488                .filter(|order| order.ts_accepted + accepted_buffer_ns <= ts_now)
489                .cloned()
490                .collect::<Vec<OwnBookOrder>>();
491
492            (level.price.value.as_decimal(), orders)
493        })
494        .filter(|(_, orders)| !orders.is_empty())
495        .collect::<IndexMap<Decimal, Vec<OwnBookOrder>>>()
496}
497
498fn group_quantities(
499    quantities: IndexMap<Decimal, Decimal>,
500    group_size: Decimal,
501    depth: Option<usize>,
502    is_bid: bool,
503) -> IndexMap<Decimal, Decimal> {
504    let mut grouped = IndexMap::new();
505    let depth = depth.unwrap_or(usize::MAX);
506
507    for (price, size) in quantities {
508        let grouped_price = if is_bid {
509            (price / group_size).floor() * group_size
510        } else {
511            (price / group_size).ceil() * group_size
512        };
513
514        grouped
515            .entry(grouped_price)
516            .and_modify(|total| *total += size)
517            .or_insert(size);
518
519        if grouped.len() > depth {
520            if is_bid {
521                // For bids, remove the lowest price level
522                if let Some((lowest_price, _)) = grouped.iter().min_by_key(|(price, _)| *price) {
523                    let lowest_price = *lowest_price;
524                    grouped.shift_remove(&lowest_price);
525                }
526            } else {
527                // For asks, remove the highest price level
528                if let Some((highest_price, _)) = grouped.iter().max_by_key(|(price, _)| *price) {
529                    let highest_price = *highest_price;
530                    grouped.shift_remove(&highest_price);
531                }
532            }
533        }
534    }
535
536    grouped
537}
538
539fn sum_order_sizes<'a, I>(orders: I) -> Decimal
540where
541    I: Iterator<Item = &'a OwnBookOrder>,
542{
543    orders.fold(Decimal::ZERO, |total, order| {
544        total + order.size.as_decimal()
545    })
546}
547
548/// Represents a ladder of price levels for one side of an order book.
549pub(crate) struct OwnBookLadder {
550    pub side: OrderSideSpecified,
551    pub levels: BTreeMap<BookPrice, OwnBookLevel>,
552    pub cache: HashMap<ClientOrderId, BookPrice>,
553}
554
555impl OwnBookLadder {
556    /// Creates a new [`OwnBookLadder`] instance.
557    #[must_use]
558    pub fn new(side: OrderSideSpecified) -> Self {
559        Self {
560            side,
561            levels: BTreeMap::new(),
562            cache: HashMap::new(),
563        }
564    }
565
566    /// Returns the number of price levels in the ladder.
567    #[must_use]
568    #[allow(dead_code)] // Used in tests
569    pub fn len(&self) -> usize {
570        self.levels.len()
571    }
572
573    /// Returns true if the ladder has no price levels.
574    #[must_use]
575    #[allow(dead_code)] // Used in tests
576    pub fn is_empty(&self) -> bool {
577        self.levels.is_empty()
578    }
579
580    /// Removes all orders and price levels from the ladder.
581    pub fn clear(&mut self) {
582        self.levels.clear();
583        self.cache.clear();
584    }
585
586    /// Adds an order to the ladder at its price level.
587    pub fn add(&mut self, order: OwnBookOrder) {
588        let book_price = order.to_book_price();
589        self.cache.insert(order.client_order_id, book_price);
590
591        match self.levels.get_mut(&book_price) {
592            Some(level) => {
593                level.add(order);
594            }
595            None => {
596                let level = OwnBookLevel::from_order(order);
597                self.levels.insert(book_price, level);
598            }
599        }
600    }
601
602    /// Updates an existing order in the ladder, moving it to a new price level if needed.
603    pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
604        let price = self.cache.get(&order.client_order_id).copied();
605        if let Some(price) = price {
606            if let Some(level) = self.levels.get_mut(&price) {
607                if order.price == level.price.value {
608                    // Update at current price level
609                    level.update(order);
610                    return Ok(());
611                }
612
613                // Price update: delete and insert at new level
614                self.cache.remove(&order.client_order_id);
615                level.delete(&order.client_order_id)?;
616                if level.is_empty() {
617                    self.levels.remove(&price);
618                }
619            }
620        }
621
622        self.add(order);
623        Ok(())
624    }
625
626    /// Deletes an order from the ladder.
627    pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
628        self.remove(&order.client_order_id)
629    }
630
631    /// Removes an order by its ID from the ladder.
632    pub fn remove(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
633        if let Some(price) = self.cache.remove(client_order_id) {
634            if let Some(level) = self.levels.get_mut(&price) {
635                level.delete(client_order_id)?;
636                if level.is_empty() {
637                    self.levels.remove(&price);
638                }
639            }
640        }
641
642        Ok(())
643    }
644
645    /// Returns the total size of all orders in the ladder.
646    #[must_use]
647    #[allow(dead_code)] // Used in tests
648    pub fn sizes(&self) -> f64 {
649        self.levels.values().map(OwnBookLevel::size).sum()
650    }
651
652    /// Returns the total value exposure (price * size) of all orders in the ladder.
653    #[must_use]
654    #[allow(dead_code)] // Used in tests
655    pub fn exposures(&self) -> f64 {
656        self.levels.values().map(OwnBookLevel::exposure).sum()
657    }
658
659    /// Returns the best price level in the ladder.
660    #[must_use]
661    #[allow(dead_code)] // Used in tests
662    pub fn top(&self) -> Option<&OwnBookLevel> {
663        match self.levels.iter().next() {
664            Some((_, l)) => Option::Some(l),
665            None => Option::None,
666        }
667    }
668}
669
670impl Debug for OwnBookLadder {
671    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
672        f.debug_struct(stringify!(OwnBookLadder))
673            .field("side", &self.side)
674            .field("levels", &self.levels)
675            .finish()
676    }
677}
678
679impl Display for OwnBookLadder {
680    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
681        writeln!(f, "{}(side={})", stringify!(OwnBookLadder), self.side)?;
682        for (price, level) in &self.levels {
683            writeln!(f, "  {} -> {} orders", price, level.len())?;
684        }
685        Ok(())
686    }
687}
688
689#[derive(Clone, Debug)]
690pub struct OwnBookLevel {
691    pub price: BookPrice,
692    pub orders: IndexMap<ClientOrderId, OwnBookOrder>,
693}
694
695impl OwnBookLevel {
696    /// Creates a new [`OwnBookLevel`] instance.
697    #[must_use]
698    pub fn new(price: BookPrice) -> Self {
699        Self {
700            price,
701            orders: IndexMap::new(),
702        }
703    }
704
705    /// Creates a new [`OwnBookLevel`] from an order, using the order's price and side.
706    #[must_use]
707    pub fn from_order(order: OwnBookOrder) -> Self {
708        let mut level = Self {
709            price: order.to_book_price(),
710            orders: IndexMap::new(),
711        };
712        level.orders.insert(order.client_order_id, order);
713        level
714    }
715
716    /// Returns the number of orders at this price level.
717    #[must_use]
718    pub fn len(&self) -> usize {
719        self.orders.len()
720    }
721
722    /// Returns true if this price level has no orders.
723    #[must_use]
724    pub fn is_empty(&self) -> bool {
725        self.orders.is_empty()
726    }
727
728    /// Returns a reference to the first order at this price level in FIFO order.
729    #[must_use]
730    pub fn first(&self) -> Option<&OwnBookOrder> {
731        self.orders.get_index(0).map(|(_key, order)| order)
732    }
733
734    /// Returns an iterator over the orders at this price level in FIFO order.
735    pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
736        self.orders.values()
737    }
738
739    /// Returns all orders at this price level in FIFO insertion order.
740    #[must_use]
741    pub fn get_orders(&self) -> Vec<OwnBookOrder> {
742        self.orders.values().copied().collect()
743    }
744
745    /// Returns the total size of all orders at this price level as a float.
746    #[must_use]
747    pub fn size(&self) -> f64 {
748        self.orders.iter().map(|(_, o)| o.size.as_f64()).sum()
749    }
750
751    /// Returns the total size of all orders at this price level as a decimal.
752    #[must_use]
753    pub fn size_decimal(&self) -> Decimal {
754        self.orders.iter().map(|(_, o)| o.size.as_decimal()).sum()
755    }
756
757    /// Returns the total exposure (price * size) of all orders at this price level as a float.
758    #[must_use]
759    pub fn exposure(&self) -> f64 {
760        self.orders
761            .iter()
762            .map(|(_, o)| o.price.as_f64() * o.size.as_f64())
763            .sum()
764    }
765
766    /// Adds multiple orders to this price level in FIFO order. Orders must match the level's price.
767    pub fn add_bulk(&mut self, orders: Vec<OwnBookOrder>) {
768        for order in orders {
769            self.add(order);
770        }
771    }
772
773    /// Adds an order to this price level. Order must match the level's price.
774    pub fn add(&mut self, order: OwnBookOrder) {
775        debug_assert_eq!(order.price, self.price.value);
776
777        self.orders.insert(order.client_order_id, order);
778    }
779
780    /// Updates an existing order at this price level. Updated order must match the level's price.
781    /// Removes the order if size becomes zero.
782    pub fn update(&mut self, order: OwnBookOrder) {
783        debug_assert_eq!(order.price, self.price.value);
784
785        self.orders[&order.client_order_id] = order;
786    }
787
788    /// Deletes an order from this price level.
789    pub fn delete(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
790        if self.orders.shift_remove(client_order_id).is_none() {
791            // TODO: Use a generic anyhow result for now pending specific error types
792            anyhow::bail!("Order {client_order_id} not found for delete");
793        };
794        Ok(())
795    }
796}
797
798impl PartialEq for OwnBookLevel {
799    fn eq(&self, other: &Self) -> bool {
800        self.price == other.price
801    }
802}
803
804impl Eq for OwnBookLevel {}
805
806impl PartialOrd for OwnBookLevel {
807    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
808        Some(self.cmp(other))
809    }
810}
811
812impl Ord for OwnBookLevel {
813    fn cmp(&self, other: &Self) -> Ordering {
814        self.price.cmp(&other.price)
815    }
816}
817
818pub fn should_handle_own_book_order(order: &OrderAny) -> bool {
819    order.has_price()
820        && order.time_in_force() != TimeInForce::Ioc
821        && order.time_in_force() != TimeInForce::Fok
822}