nautilus_model/orderbook/
own.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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 = "posei_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 = "posei_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 - {} cached order already closed, deleting from own book",
478        client_order_id
479    );
480}
481
482fn filter_orders<'a>(
483    levels: impl Iterator<Item = &'a OwnBookLevel>,
484    status: Option<&HashSet<OrderStatus>>,
485    accepted_buffer_ns: Option<u64>,
486    ts_now: Option<u64>,
487) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
488    let accepted_buffer_ns = accepted_buffer_ns.unwrap_or(0);
489    let ts_now = ts_now.unwrap_or_else(nanos_since_unix_epoch);
490    levels
491        .map(|level| {
492            let orders = level
493                .orders
494                .values()
495                .filter(|order| status.is_none_or(|f| f.contains(&order.status)))
496                .filter(|order| order.ts_accepted + accepted_buffer_ns <= ts_now)
497                .cloned()
498                .collect::<Vec<OwnBookOrder>>();
499
500            (level.price.value.as_decimal(), orders)
501        })
502        .filter(|(_, orders)| !orders.is_empty())
503        .collect::<IndexMap<Decimal, Vec<OwnBookOrder>>>()
504}
505
506fn group_quantities(
507    quantities: IndexMap<Decimal, Decimal>,
508    group_size: Decimal,
509    depth: Option<usize>,
510    is_bid: bool,
511) -> IndexMap<Decimal, Decimal> {
512    let mut grouped = IndexMap::new();
513    let depth = depth.unwrap_or(usize::MAX);
514
515    for (price, size) in quantities {
516        let grouped_price = if is_bid {
517            (price / group_size).floor() * group_size
518        } else {
519            (price / group_size).ceil() * group_size
520        };
521
522        grouped
523            .entry(grouped_price)
524            .and_modify(|total| *total += size)
525            .or_insert(size);
526
527        if grouped.len() > depth {
528            if is_bid {
529                // For bids, remove the lowest price level
530                if let Some((lowest_price, _)) = grouped.iter().min_by_key(|(price, _)| *price) {
531                    let lowest_price = *lowest_price;
532                    grouped.shift_remove(&lowest_price);
533                }
534            } else {
535                // For asks, remove the highest price level
536                if let Some((highest_price, _)) = grouped.iter().max_by_key(|(price, _)| *price) {
537                    let highest_price = *highest_price;
538                    grouped.shift_remove(&highest_price);
539                }
540            }
541        }
542    }
543
544    grouped
545}
546
547fn sum_order_sizes<'a, I>(orders: I) -> Decimal
548where
549    I: Iterator<Item = &'a OwnBookOrder>,
550{
551    orders.fold(Decimal::ZERO, |total, order| {
552        total + order.size.as_decimal()
553    })
554}
555
556/// Represents a ladder of price levels for one side of an order book.
557pub(crate) struct OwnBookLadder {
558    pub side: OrderSideSpecified,
559    pub levels: BTreeMap<BookPrice, OwnBookLevel>,
560    pub cache: HashMap<ClientOrderId, BookPrice>,
561}
562
563impl OwnBookLadder {
564    /// Creates a new [`OwnBookLadder`] instance.
565    #[must_use]
566    pub fn new(side: OrderSideSpecified) -> Self {
567        Self {
568            side,
569            levels: BTreeMap::new(),
570            cache: HashMap::new(),
571        }
572    }
573
574    /// Returns the number of price levels in the ladder.
575    #[must_use]
576    #[allow(dead_code)] // Used in tests
577    pub fn len(&self) -> usize {
578        self.levels.len()
579    }
580
581    /// Returns true if the ladder has no price levels.
582    #[must_use]
583    #[allow(dead_code)] // Used in tests
584    pub fn is_empty(&self) -> bool {
585        self.levels.is_empty()
586    }
587
588    /// Removes all orders and price levels from the ladder.
589    pub fn clear(&mut self) {
590        self.levels.clear();
591        self.cache.clear();
592    }
593
594    /// Adds an order to the ladder at its price level.
595    pub fn add(&mut self, order: OwnBookOrder) {
596        let book_price = order.to_book_price();
597        self.cache.insert(order.client_order_id, book_price);
598
599        match self.levels.get_mut(&book_price) {
600            Some(level) => {
601                level.add(order);
602            }
603            None => {
604                let level = OwnBookLevel::from_order(order);
605                self.levels.insert(book_price, level);
606            }
607        }
608    }
609
610    /// Updates an existing order in the ladder, moving it to a new price level if needed.
611    ///
612    /// # Errors
613    ///
614    /// Returns an error if the order is not found.
615    pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
616        let price = self.cache.get(&order.client_order_id).copied();
617        if let Some(price) = price {
618            if let Some(level) = self.levels.get_mut(&price) {
619                if order.price == level.price.value {
620                    // Update at current price level
621                    level.update(order);
622                    return Ok(());
623                }
624
625                // Price update: delete and insert at new level
626                self.cache.remove(&order.client_order_id);
627                level.delete(&order.client_order_id)?;
628                if level.is_empty() {
629                    self.levels.remove(&price);
630                }
631            }
632        }
633
634        self.add(order);
635        Ok(())
636    }
637
638    /// Deletes an order from the ladder.
639    ///
640    /// # Errors
641    ///
642    /// Returns an error if the order is not found.
643    pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
644        self.remove(&order.client_order_id)
645    }
646
647    /// Removes an order by its ID from the ladder.
648    ///
649    /// # Errors
650    ///
651    /// Returns an error if the order is not found.
652    pub fn remove(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
653        if let Some(price) = self.cache.remove(client_order_id) {
654            if let Some(level) = self.levels.get_mut(&price) {
655                level.delete(client_order_id)?;
656                if level.is_empty() {
657                    self.levels.remove(&price);
658                }
659            }
660        }
661
662        Ok(())
663    }
664
665    /// Returns the total size of all orders in the ladder.
666    #[must_use]
667    #[allow(dead_code)] // Used in tests
668    pub fn sizes(&self) -> f64 {
669        self.levels.values().map(OwnBookLevel::size).sum()
670    }
671
672    /// Returns the total value exposure (price * size) of all orders in the ladder.
673    #[must_use]
674    #[allow(dead_code)] // Used in tests
675    pub fn exposures(&self) -> f64 {
676        self.levels.values().map(OwnBookLevel::exposure).sum()
677    }
678
679    /// Returns the best price level in the ladder.
680    #[must_use]
681    #[allow(dead_code)] // Used in tests
682    pub fn top(&self) -> Option<&OwnBookLevel> {
683        match self.levels.iter().next() {
684            Some((_, l)) => Option::Some(l),
685            None => Option::None,
686        }
687    }
688}
689
690impl Debug for OwnBookLadder {
691    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
692        f.debug_struct(stringify!(OwnBookLadder))
693            .field("side", &self.side)
694            .field("levels", &self.levels)
695            .finish()
696    }
697}
698
699impl Display for OwnBookLadder {
700    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
701        writeln!(f, "{}(side={})", stringify!(OwnBookLadder), self.side)?;
702        for (price, level) in &self.levels {
703            writeln!(f, "  {} -> {} orders", price, level.len())?;
704        }
705        Ok(())
706    }
707}
708
709#[derive(Clone, Debug)]
710pub struct OwnBookLevel {
711    pub price: BookPrice,
712    pub orders: IndexMap<ClientOrderId, OwnBookOrder>,
713}
714
715impl OwnBookLevel {
716    /// Creates a new [`OwnBookLevel`] instance.
717    #[must_use]
718    pub fn new(price: BookPrice) -> Self {
719        Self {
720            price,
721            orders: IndexMap::new(),
722        }
723    }
724
725    /// Creates a new [`OwnBookLevel`] from an order, using the order's price and side.
726    #[must_use]
727    pub fn from_order(order: OwnBookOrder) -> Self {
728        let mut level = Self {
729            price: order.to_book_price(),
730            orders: IndexMap::new(),
731        };
732        level.orders.insert(order.client_order_id, order);
733        level
734    }
735
736    /// Returns the number of orders at this price level.
737    #[must_use]
738    pub fn len(&self) -> usize {
739        self.orders.len()
740    }
741
742    /// Returns true if this price level has no orders.
743    #[must_use]
744    pub fn is_empty(&self) -> bool {
745        self.orders.is_empty()
746    }
747
748    /// Returns a reference to the first order at this price level in FIFO order.
749    #[must_use]
750    pub fn first(&self) -> Option<&OwnBookOrder> {
751        self.orders.get_index(0).map(|(_key, order)| order)
752    }
753
754    /// Returns an iterator over the orders at this price level in FIFO order.
755    pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
756        self.orders.values()
757    }
758
759    /// Returns all orders at this price level in FIFO insertion order.
760    #[must_use]
761    pub fn get_orders(&self) -> Vec<OwnBookOrder> {
762        self.orders.values().copied().collect()
763    }
764
765    /// Returns the total size of all orders at this price level as a float.
766    #[must_use]
767    pub fn size(&self) -> f64 {
768        self.orders.iter().map(|(_, o)| o.size.as_f64()).sum()
769    }
770
771    /// Returns the total size of all orders at this price level as a decimal.
772    #[must_use]
773    pub fn size_decimal(&self) -> Decimal {
774        self.orders.iter().map(|(_, o)| o.size.as_decimal()).sum()
775    }
776
777    /// Returns the total exposure (price * size) of all orders at this price level as a float.
778    #[must_use]
779    pub fn exposure(&self) -> f64 {
780        self.orders
781            .iter()
782            .map(|(_, o)| o.price.as_f64() * o.size.as_f64())
783            .sum()
784    }
785
786    /// Adds multiple orders to this price level in FIFO order. Orders must match the level's price.
787    pub fn add_bulk(&mut self, orders: Vec<OwnBookOrder>) {
788        for order in orders {
789            self.add(order);
790        }
791    }
792
793    /// Adds an order to this price level. Order must match the level's price.
794    pub fn add(&mut self, order: OwnBookOrder) {
795        debug_assert_eq!(order.price, self.price.value);
796
797        self.orders.insert(order.client_order_id, order);
798    }
799
800    /// Updates an existing order at this price level. Updated order must match the level's price.
801    /// Removes the order if size becomes zero.
802    pub fn update(&mut self, order: OwnBookOrder) {
803        debug_assert_eq!(order.price, self.price.value);
804
805        self.orders[&order.client_order_id] = order;
806    }
807
808    /// Deletes an order from this price level.
809    ///
810    /// # Errors
811    ///
812    /// Returns an error if the order is not found.
813    pub fn delete(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
814        if self.orders.shift_remove(client_order_id).is_none() {
815            // TODO: Use a generic anyhow result for now pending specific error types
816            anyhow::bail!("Order {client_order_id} not found for delete");
817        };
818        Ok(())
819    }
820}
821
822impl PartialEq for OwnBookLevel {
823    fn eq(&self, other: &Self) -> bool {
824        self.price == other.price
825    }
826}
827
828impl Eq for OwnBookLevel {}
829
830impl PartialOrd for OwnBookLevel {
831    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
832        Some(self.cmp(other))
833    }
834}
835
836impl Ord for OwnBookLevel {
837    fn cmp(&self, other: &Self) -> Ordering {
838        self.price.cmp(&other.price)
839    }
840}
841
842pub fn should_handle_own_book_order(order: &OrderAny) -> bool {
843    order.has_price()
844        && order.time_in_force() != TimeInForce::Ioc
845        && order.time_in_force() != TimeInForce::Fok
846}