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    ///
281    /// # Errors
282    ///
283    /// Returns an error if the order is not found.
284    pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
285        self.increment(&order);
286        match order.side {
287            OrderSideSpecified::Buy => self.bids.update(order),
288            OrderSideSpecified::Sell => self.asks.update(order),
289        }
290    }
291
292    /// Deletes an own order from the book.
293    ///
294    /// # Errors
295    ///
296    /// Returns an error if the order is not found.
297    pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
298        self.increment(&order);
299        match order.side {
300            OrderSideSpecified::Buy => self.bids.delete(order),
301            OrderSideSpecified::Sell => self.asks.delete(order),
302        }
303    }
304
305    /// Clears all orders from both sides of the book.
306    pub fn clear(&mut self) {
307        self.bids.clear();
308        self.asks.clear();
309    }
310
311    /// Returns an iterator over bid price levels.
312    pub fn bids(&self) -> impl Iterator<Item = &OwnBookLevel> {
313        self.bids.levels.values()
314    }
315
316    /// Returns an iterator over ask price levels.
317    pub fn asks(&self) -> impl Iterator<Item = &OwnBookLevel> {
318        self.asks.levels.values()
319    }
320
321    /// Returns the client order IDs currently on the bid side.
322    pub fn bid_client_order_ids(&self) -> Vec<ClientOrderId> {
323        self.bids.cache.keys().cloned().collect()
324    }
325
326    /// Returns the client order IDs currently on the ask side.
327    pub fn ask_client_order_ids(&self) -> Vec<ClientOrderId> {
328        self.asks.cache.keys().cloned().collect()
329    }
330
331    /// Return whether the given client order ID is in the own book.
332    pub fn is_order_in_book(&self, client_order_id: &ClientOrderId) -> bool {
333        self.asks.cache.contains_key(client_order_id)
334            || self.bids.cache.contains_key(client_order_id)
335    }
336
337    /// Maps bid price levels to their own orders, excluding empty levels after filtering.
338    ///
339    /// Filters by `status` if provided. With `accepted_buffer_ns`, only includes orders accepted
340    /// at least that many nanoseconds before `ts_now` (defaults to now).
341    pub fn bids_as_map(
342        &self,
343        status: Option<HashSet<OrderStatus>>,
344        accepted_buffer_ns: Option<u64>,
345        ts_now: Option<u64>,
346    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
347        filter_orders(self.bids(), status.as_ref(), accepted_buffer_ns, ts_now)
348    }
349
350    /// Maps ask price levels to their own orders, excluding empty levels after filtering.
351    ///
352    /// Filters by `status` if provided. With `accepted_buffer_ns`, only includes orders accepted
353    /// at least that many nanoseconds before `ts_now` (defaults to now).
354    pub fn asks_as_map(
355        &self,
356        status: Option<HashSet<OrderStatus>>,
357        accepted_buffer_ns: Option<u64>,
358        ts_now: Option<u64>,
359    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
360        filter_orders(self.asks(), status.as_ref(), accepted_buffer_ns, ts_now)
361    }
362
363    /// Aggregates own bid quantities per price level, omitting zero-quantity levels.
364    ///
365    /// Filters by `status` if provided, including only matching orders. With `accepted_buffer_ns`,
366    /// only includes orders accepted at least that many nanoseconds before `ts_now` (defaults to now).
367    ///
368    /// If `group_size` is provided, groups quantities into price buckets.
369    /// If `depth` is provided, limits the number of price levels returned.
370    pub fn bid_quantity(
371        &self,
372        status: Option<HashSet<OrderStatus>>,
373        depth: Option<usize>,
374        group_size: Option<Decimal>,
375        accepted_buffer_ns: Option<u64>,
376        ts_now: Option<u64>,
377    ) -> IndexMap<Decimal, Decimal> {
378        let quantities = self
379            .bids_as_map(status, accepted_buffer_ns, ts_now)
380            .into_iter()
381            .map(|(price, orders)| (price, sum_order_sizes(orders.iter())))
382            .filter(|(_, quantity)| *quantity > Decimal::ZERO)
383            .collect::<IndexMap<Decimal, Decimal>>();
384
385        if let Some(group_size) = group_size {
386            group_quantities(quantities, group_size, depth, true)
387        } else if let Some(depth) = depth {
388            quantities.into_iter().take(depth).collect()
389        } else {
390            quantities
391        }
392    }
393
394    /// Aggregates own ask quantities per price level, omitting zero-quantity levels.
395    ///
396    /// Filters by `status` if provided, including only matching orders. With `accepted_buffer_ns`,
397    /// only includes orders accepted at least that many nanoseconds before `ts_now` (defaults to now).
398    ///
399    /// If `group_size` is provided, groups quantities into price buckets.
400    /// If `depth` is provided, limits the number of price levels returned.
401    pub fn ask_quantity(
402        &self,
403        status: Option<HashSet<OrderStatus>>,
404        depth: Option<usize>,
405        group_size: Option<Decimal>,
406        accepted_buffer_ns: Option<u64>,
407        ts_now: Option<u64>,
408    ) -> IndexMap<Decimal, Decimal> {
409        let quantities = self
410            .asks_as_map(status, accepted_buffer_ns, ts_now)
411            .into_iter()
412            .map(|(price, orders)| {
413                let quantity = sum_order_sizes(orders.iter());
414                (price, quantity)
415            })
416            .filter(|(_, quantity)| *quantity > Decimal::ZERO)
417            .collect::<IndexMap<Decimal, Decimal>>();
418
419        if let Some(group_size) = group_size {
420            group_quantities(quantities, group_size, depth, false)
421        } else if let Some(depth) = depth {
422            quantities.into_iter().take(depth).collect()
423        } else {
424            quantities
425        }
426    }
427
428    /// Return a formatted string representation of the order book.
429    #[must_use]
430    pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
431        pprint_own_book(self, num_levels, group_size)
432    }
433
434    pub fn audit_open_orders(&mut self, open_order_ids: &HashSet<ClientOrderId>) {
435        log::debug!("Auditing {self}");
436
437        // Audit bids
438        let bids_to_remove: Vec<ClientOrderId> = self
439            .bids
440            .cache
441            .keys()
442            .filter(|&key| !open_order_ids.contains(key))
443            .cloned()
444            .collect();
445
446        // Audit asks
447        let asks_to_remove: Vec<ClientOrderId> = self
448            .asks
449            .cache
450            .keys()
451            .filter(|&key| !open_order_ids.contains(key))
452            .cloned()
453            .collect();
454
455        for client_order_id in bids_to_remove {
456            log_audit_error(&client_order_id);
457            if let Err(e) = self.bids.remove(&client_order_id) {
458                log::error!("{e}");
459            }
460        }
461
462        for client_order_id in asks_to_remove {
463            log_audit_error(&client_order_id);
464            if let Err(e) = self.asks.remove(&client_order_id) {
465                log::error!("{e}");
466            }
467        }
468    }
469}
470
471fn log_audit_error(client_order_id: &ClientOrderId) {
472    log::error!(
473        "Audit error - {client_order_id} cached order already closed, deleting from own book"
474    );
475}
476
477fn filter_orders<'a>(
478    levels: impl Iterator<Item = &'a OwnBookLevel>,
479    status: Option<&HashSet<OrderStatus>>,
480    accepted_buffer_ns: Option<u64>,
481    ts_now: Option<u64>,
482) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
483    let accepted_buffer_ns = accepted_buffer_ns.unwrap_or(0);
484    let ts_now = ts_now.unwrap_or_else(nanos_since_unix_epoch);
485    levels
486        .map(|level| {
487            let orders = level
488                .orders
489                .values()
490                .filter(|order| status.is_none_or(|f| f.contains(&order.status)))
491                .filter(|order| order.ts_accepted + accepted_buffer_ns <= ts_now)
492                .cloned()
493                .collect::<Vec<OwnBookOrder>>();
494
495            (level.price.value.as_decimal(), orders)
496        })
497        .filter(|(_, orders)| !orders.is_empty())
498        .collect::<IndexMap<Decimal, Vec<OwnBookOrder>>>()
499}
500
501fn group_quantities(
502    quantities: IndexMap<Decimal, Decimal>,
503    group_size: Decimal,
504    depth: Option<usize>,
505    is_bid: bool,
506) -> IndexMap<Decimal, Decimal> {
507    let mut grouped = IndexMap::new();
508    let depth = depth.unwrap_or(usize::MAX);
509
510    for (price, size) in quantities {
511        let grouped_price = if is_bid {
512            (price / group_size).floor() * group_size
513        } else {
514            (price / group_size).ceil() * group_size
515        };
516
517        grouped
518            .entry(grouped_price)
519            .and_modify(|total| *total += size)
520            .or_insert(size);
521
522        if grouped.len() > depth {
523            if is_bid {
524                // For bids, remove the lowest price level
525                if let Some((lowest_price, _)) = grouped.iter().min_by_key(|(price, _)| *price) {
526                    let lowest_price = *lowest_price;
527                    grouped.shift_remove(&lowest_price);
528                }
529            } else {
530                // For asks, remove the highest price level
531                if let Some((highest_price, _)) = grouped.iter().max_by_key(|(price, _)| *price) {
532                    let highest_price = *highest_price;
533                    grouped.shift_remove(&highest_price);
534                }
535            }
536        }
537    }
538
539    grouped
540}
541
542fn sum_order_sizes<'a, I>(orders: I) -> Decimal
543where
544    I: Iterator<Item = &'a OwnBookOrder>,
545{
546    orders.fold(Decimal::ZERO, |total, order| {
547        total + order.size.as_decimal()
548    })
549}
550
551/// Represents a ladder of price levels for one side of an order book.
552pub(crate) struct OwnBookLadder {
553    pub side: OrderSideSpecified,
554    pub levels: BTreeMap<BookPrice, OwnBookLevel>,
555    pub cache: HashMap<ClientOrderId, BookPrice>,
556}
557
558impl OwnBookLadder {
559    /// Creates a new [`OwnBookLadder`] instance.
560    #[must_use]
561    pub fn new(side: OrderSideSpecified) -> Self {
562        Self {
563            side,
564            levels: BTreeMap::new(),
565            cache: HashMap::new(),
566        }
567    }
568
569    /// Returns the number of price levels in the ladder.
570    #[must_use]
571    #[allow(dead_code)] // Used in tests
572    pub fn len(&self) -> usize {
573        self.levels.len()
574    }
575
576    /// Returns true if the ladder has no price levels.
577    #[must_use]
578    #[allow(dead_code)] // Used in tests
579    pub fn is_empty(&self) -> bool {
580        self.levels.is_empty()
581    }
582
583    /// Removes all orders and price levels from the ladder.
584    pub fn clear(&mut self) {
585        self.levels.clear();
586        self.cache.clear();
587    }
588
589    /// Adds an order to the ladder at its price level.
590    pub fn add(&mut self, order: OwnBookOrder) {
591        let book_price = order.to_book_price();
592        self.cache.insert(order.client_order_id, book_price);
593
594        match self.levels.get_mut(&book_price) {
595            Some(level) => {
596                level.add(order);
597            }
598            None => {
599                let level = OwnBookLevel::from_order(order);
600                self.levels.insert(book_price, level);
601            }
602        }
603    }
604
605    /// Updates an existing order in the ladder, moving it to a new price level if needed.
606    ///
607    /// # Errors
608    ///
609    /// Returns an error if the order is not found.
610    pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
611        let price = self.cache.get(&order.client_order_id).copied();
612        if let Some(price) = price
613            && let Some(level) = self.levels.get_mut(&price)
614        {
615            if order.price == level.price.value {
616                // Update at current price level
617                level.update(order);
618                return Ok(());
619            }
620
621            // Price update: delete and insert at new level
622            self.cache.remove(&order.client_order_id);
623            level.delete(&order.client_order_id)?;
624            if level.is_empty() {
625                self.levels.remove(&price);
626            }
627        }
628
629        self.add(order);
630        Ok(())
631    }
632
633    /// Deletes an order from the ladder.
634    ///
635    /// # Errors
636    ///
637    /// Returns an error if the order is not found.
638    pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
639        self.remove(&order.client_order_id)
640    }
641
642    /// Removes an order by its ID from the ladder.
643    ///
644    /// # Errors
645    ///
646    /// Returns an error if the order is not found.
647    pub fn remove(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
648        if let Some(price) = self.cache.remove(client_order_id)
649            && let Some(level) = self.levels.get_mut(&price)
650        {
651            level.delete(client_order_id)?;
652            if level.is_empty() {
653                self.levels.remove(&price);
654            }
655        }
656
657        Ok(())
658    }
659
660    /// Returns the total size of all orders in the ladder.
661    #[must_use]
662    #[allow(dead_code)] // Used in tests
663    pub fn sizes(&self) -> f64 {
664        self.levels.values().map(OwnBookLevel::size).sum()
665    }
666
667    /// Returns the total value exposure (price * size) of all orders in the ladder.
668    #[must_use]
669    #[allow(dead_code)] // Used in tests
670    pub fn exposures(&self) -> f64 {
671        self.levels.values().map(OwnBookLevel::exposure).sum()
672    }
673
674    /// Returns the best price level in the ladder.
675    #[must_use]
676    #[allow(dead_code)] // Used in tests
677    pub fn top(&self) -> Option<&OwnBookLevel> {
678        match self.levels.iter().next() {
679            Some((_, l)) => Option::Some(l),
680            None => Option::None,
681        }
682    }
683}
684
685impl Debug for OwnBookLadder {
686    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
687        f.debug_struct(stringify!(OwnBookLadder))
688            .field("side", &self.side)
689            .field("levels", &self.levels)
690            .finish()
691    }
692}
693
694impl Display for OwnBookLadder {
695    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
696        writeln!(f, "{}(side={})", stringify!(OwnBookLadder), self.side)?;
697        for (price, level) in &self.levels {
698            writeln!(f, "  {} -> {} orders", price, level.len())?;
699        }
700        Ok(())
701    }
702}
703
704#[derive(Clone, Debug)]
705pub struct OwnBookLevel {
706    pub price: BookPrice,
707    pub orders: IndexMap<ClientOrderId, OwnBookOrder>,
708}
709
710impl OwnBookLevel {
711    /// Creates a new [`OwnBookLevel`] instance.
712    #[must_use]
713    pub fn new(price: BookPrice) -> Self {
714        Self {
715            price,
716            orders: IndexMap::new(),
717        }
718    }
719
720    /// Creates a new [`OwnBookLevel`] from an order, using the order's price and side.
721    #[must_use]
722    pub fn from_order(order: OwnBookOrder) -> Self {
723        let mut level = Self {
724            price: order.to_book_price(),
725            orders: IndexMap::new(),
726        };
727        level.orders.insert(order.client_order_id, order);
728        level
729    }
730
731    /// Returns the number of orders at this price level.
732    #[must_use]
733    pub fn len(&self) -> usize {
734        self.orders.len()
735    }
736
737    /// Returns true if this price level has no orders.
738    #[must_use]
739    pub fn is_empty(&self) -> bool {
740        self.orders.is_empty()
741    }
742
743    /// Returns a reference to the first order at this price level in FIFO order.
744    #[must_use]
745    pub fn first(&self) -> Option<&OwnBookOrder> {
746        self.orders.get_index(0).map(|(_key, order)| order)
747    }
748
749    /// Returns an iterator over the orders at this price level in FIFO order.
750    pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
751        self.orders.values()
752    }
753
754    /// Returns all orders at this price level in FIFO insertion order.
755    #[must_use]
756    pub fn get_orders(&self) -> Vec<OwnBookOrder> {
757        self.orders.values().copied().collect()
758    }
759
760    /// Returns the total size of all orders at this price level as a float.
761    #[must_use]
762    pub fn size(&self) -> f64 {
763        self.orders.iter().map(|(_, o)| o.size.as_f64()).sum()
764    }
765
766    /// Returns the total size of all orders at this price level as a decimal.
767    #[must_use]
768    pub fn size_decimal(&self) -> Decimal {
769        self.orders.iter().map(|(_, o)| o.size.as_decimal()).sum()
770    }
771
772    /// Returns the total exposure (price * size) of all orders at this price level as a float.
773    #[must_use]
774    pub fn exposure(&self) -> f64 {
775        self.orders
776            .iter()
777            .map(|(_, o)| o.price.as_f64() * o.size.as_f64())
778            .sum()
779    }
780
781    /// Adds multiple orders to this price level in FIFO order. Orders must match the level's price.
782    pub fn add_bulk(&mut self, orders: Vec<OwnBookOrder>) {
783        for order in orders {
784            self.add(order);
785        }
786    }
787
788    /// Adds an order to this price level. Order must match the level's price.
789    pub fn add(&mut self, order: OwnBookOrder) {
790        debug_assert_eq!(order.price, self.price.value);
791
792        self.orders.insert(order.client_order_id, order);
793    }
794
795    /// Updates an existing order at this price level. Updated order must match the level's price.
796    /// Removes the order if size becomes zero.
797    pub fn update(&mut self, order: OwnBookOrder) {
798        debug_assert_eq!(order.price, self.price.value);
799
800        self.orders[&order.client_order_id] = order;
801    }
802
803    /// Deletes an order from this price level.
804    ///
805    /// # Errors
806    ///
807    /// Returns an error if the order is not found.
808    pub fn delete(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
809        if self.orders.shift_remove(client_order_id).is_none() {
810            // TODO: Use a generic anyhow result for now pending specific error types
811            anyhow::bail!("Order {client_order_id} not found for delete");
812        };
813        Ok(())
814    }
815}
816
817impl PartialEq for OwnBookLevel {
818    fn eq(&self, other: &Self) -> bool {
819        self.price == other.price
820    }
821}
822
823impl Eq for OwnBookLevel {}
824
825impl PartialOrd for OwnBookLevel {
826    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
827        Some(self.cmp(other))
828    }
829}
830
831impl Ord for OwnBookLevel {
832    fn cmp(&self, other: &Self) -> Ordering {
833        self.price.cmp(&other.price)
834    }
835}
836
837pub fn should_handle_own_book_order(order: &OrderAny) -> bool {
838    order.has_price()
839        && order.time_in_force() != TimeInForce::Ioc
840        && order.time_in_force() != TimeInForce::Fok
841}