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