Skip to main content

nautilus_model/orderbook/
own.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! An `OwnBookOrder` for use with tracking own/user orders in L3 order books.
17//! It organizes orders into bid and ask ladders, maintains timestamps for state changes,
18//! and provides various methods for adding, updating, deleting, and querying orders.
19
20use std::{
21    cmp::Ordering,
22    collections::BTreeMap,
23    fmt::{Debug, Display},
24    hash::{Hash, Hasher},
25};
26
27use ahash::AHashSet;
28use indexmap::IndexMap;
29use nautilus_core::{UnixNanos, time::nanos_since_unix_epoch};
30use rust_decimal::Decimal;
31
32use super::{BookViewError, display::pprint_own_book};
33use crate::{
34    enums::{OrderSideSpecified, OrderStatus, OrderType, TimeInForce},
35    identifiers::{ClientOrderId, InstrumentId, TraderId, VenueOrderId},
36    orderbook::BookPrice,
37    orders::{Order, OrderAny},
38    types::{Price, Quantity},
39};
40
41/// Represents an own/user order for a book.
42///
43/// This struct models an order that may be in-flight to the trading venue or actively working,
44/// depending on the value of the `status` field.
45#[repr(C)]
46#[derive(Clone, Copy, Eq)]
47#[cfg_attr(
48    feature = "python",
49    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
50)]
51#[cfg_attr(
52    feature = "python",
53    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
54)]
55pub struct OwnBookOrder {
56    /// The trader ID.
57    pub trader_id: TraderId,
58    /// The client order ID.
59    pub client_order_id: ClientOrderId,
60    /// The venue order ID (if assigned by the venue).
61    pub venue_order_id: Option<VenueOrderId>,
62    /// The specified order side (BUY or SELL).
63    pub side: OrderSideSpecified,
64    /// The order price.
65    pub price: Price,
66    /// The order size.
67    pub size: Quantity,
68    /// The order type.
69    pub order_type: OrderType,
70    /// The order time in force.
71    pub time_in_force: TimeInForce,
72    /// The current order status (`SUBMITTED/ACCEPTED/PENDING_CANCEL/PENDING_UPDATE/PARTIALLY_FILLED`).
73    pub status: OrderStatus,
74    /// UNIX timestamp (nanoseconds) when the last order event occurred for this order.
75    pub ts_last: UnixNanos,
76    /// UNIX timestamp (nanoseconds) when the order was accepted (zero unless accepted).
77    pub ts_accepted: UnixNanos,
78    /// UNIX timestamp (nanoseconds) when the order was submitted (zero unless submitted).
79    pub ts_submitted: UnixNanos,
80    /// UNIX timestamp (nanoseconds) when the order was initialized.
81    pub ts_init: UnixNanos,
82}
83
84impl OwnBookOrder {
85    /// Creates a new [`OwnBookOrder`] instance.
86    #[must_use]
87    #[expect(clippy::too_many_arguments)]
88    pub fn new(
89        trader_id: TraderId,
90        client_order_id: ClientOrderId,
91        venue_order_id: Option<VenueOrderId>,
92        side: OrderSideSpecified,
93        price: Price,
94        size: Quantity,
95        order_type: OrderType,
96        time_in_force: TimeInForce,
97        status: OrderStatus,
98        ts_last: UnixNanos,
99        ts_accepted: UnixNanos,
100        ts_submitted: UnixNanos,
101        ts_init: UnixNanos,
102    ) -> Self {
103        Self {
104            trader_id,
105            client_order_id,
106            venue_order_id,
107            side,
108            price,
109            size,
110            order_type,
111            time_in_force,
112            status,
113            ts_last,
114            ts_accepted,
115            ts_submitted,
116            ts_init,
117        }
118    }
119
120    /// Returns a [`BookPrice`] from this order.
121    #[must_use]
122    pub fn to_book_price(&self) -> BookPrice {
123        BookPrice::new(self.price, self.side)
124    }
125
126    /// Returns the order exposure as an `f64`.
127    #[must_use]
128    pub fn exposure(&self) -> f64 {
129        self.price.as_f64() * self.size.as_f64()
130    }
131
132    /// Returns the signed order exposure as an `f64`.
133    #[must_use]
134    pub fn signed_size(&self) -> f64 {
135        match self.side {
136            OrderSideSpecified::Buy => self.size.as_f64(),
137            OrderSideSpecified::Sell => -(self.size.as_f64()),
138        }
139    }
140}
141
142impl Ord for OwnBookOrder {
143    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
144        self.ts_init
145            .cmp(&other.ts_init)
146            .then_with(|| self.client_order_id.cmp(&other.client_order_id))
147    }
148}
149
150impl PartialOrd for OwnBookOrder {
151    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
152        Some(self.cmp(other))
153    }
154}
155
156impl PartialEq for OwnBookOrder {
157    fn eq(&self, other: &Self) -> bool {
158        self.client_order_id == other.client_order_id
159    }
160}
161
162impl Hash for OwnBookOrder {
163    fn hash<H: Hasher>(&self, state: &mut H) {
164        self.client_order_id.hash(state);
165    }
166}
167
168impl Debug for OwnBookOrder {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        write!(
171            f,
172            "{}(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={})",
173            stringify!(OwnBookOrder),
174            self.trader_id,
175            self.client_order_id,
176            self.venue_order_id,
177            self.side,
178            self.price,
179            self.size,
180            self.order_type,
181            self.time_in_force,
182            self.status,
183            self.ts_last,
184            self.ts_accepted,
185            self.ts_submitted,
186            self.ts_init,
187        )
188    }
189}
190
191impl Display for OwnBookOrder {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        write!(
194            f,
195            "{},{},{:?},{},{},{},{},{},{},{},{},{},{}",
196            self.trader_id,
197            self.client_order_id,
198            self.venue_order_id,
199            self.side,
200            self.price,
201            self.size,
202            self.order_type,
203            self.time_in_force,
204            self.status,
205            self.ts_last,
206            self.ts_accepted,
207            self.ts_submitted,
208            self.ts_init,
209        )
210    }
211}
212
213#[derive(Clone, Debug)]
214#[cfg_attr(
215    feature = "python",
216    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
217)]
218#[cfg_attr(
219    feature = "python",
220    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
221)]
222pub struct OwnOrderBook {
223    /// The instrument ID for the order book.
224    pub instrument_id: InstrumentId,
225    /// The timestamp of the last event applied to the order book.
226    pub ts_last: UnixNanos,
227    /// The current count of updates applied to the order book.
228    pub update_count: u64,
229    pub(crate) bids: OwnBookLadder,
230    pub(crate) asks: OwnBookLadder,
231}
232
233impl PartialEq for OwnOrderBook {
234    fn eq(&self, other: &Self) -> bool {
235        self.instrument_id == other.instrument_id
236    }
237}
238
239impl Display for OwnOrderBook {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        write!(
242            f,
243            "{}(instrument_id={}, orders={}, update_count={})",
244            stringify!(OwnOrderBook),
245            self.instrument_id,
246            self.bids.cache.len() + self.asks.cache.len(),
247            self.update_count,
248        )
249    }
250}
251
252impl OwnOrderBook {
253    /// Creates a new [`OwnOrderBook`] instance.
254    #[must_use]
255    pub fn new(instrument_id: InstrumentId) -> Self {
256        Self {
257            instrument_id,
258            ts_last: UnixNanos::default(),
259            update_count: 0,
260            bids: OwnBookLadder::new(OrderSideSpecified::Buy),
261            asks: OwnBookLadder::new(OrderSideSpecified::Sell),
262        }
263    }
264
265    fn increment(&mut self, order: &OwnBookOrder) {
266        self.ts_last = order.ts_last;
267        self.update_count += 1;
268    }
269
270    /// Resets the order book to its initial empty state.
271    pub fn reset(&mut self) {
272        self.bids.clear();
273        self.asks.clear();
274        self.ts_last = UnixNanos::default();
275        self.update_count = 0;
276    }
277
278    /// Adds an own order to the book.
279    pub fn add(&mut self, order: OwnBookOrder) {
280        self.increment(&order);
281        match order.side {
282            OrderSideSpecified::Buy => self.bids.add(order),
283            OrderSideSpecified::Sell => self.asks.add(order),
284        }
285    }
286
287    /// Updates an existing own order in the book.
288    ///
289    /// # Errors
290    ///
291    /// Returns an error if the order is not found.
292    pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
293        let result = match order.side {
294            OrderSideSpecified::Buy => self.bids.update(order),
295            OrderSideSpecified::Sell => self.asks.update(order),
296        };
297
298        if result.is_ok() {
299            self.increment(&order);
300        }
301
302        result
303    }
304
305    /// Deletes an own order from the book.
306    ///
307    /// # Errors
308    ///
309    /// Returns an error if the order is not found.
310    pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
311        let result = match order.side {
312            OrderSideSpecified::Buy => self.bids.delete(order),
313            OrderSideSpecified::Sell => self.asks.delete(order),
314        };
315
316        if result.is_ok() {
317            self.increment(&order);
318        }
319
320        result
321    }
322
323    /// Clears all orders from both sides of the book.
324    pub fn clear(&mut self) {
325        self.bids.clear();
326        self.asks.clear();
327    }
328
329    /// Returns an iterator over bid price levels.
330    pub fn bids(&self) -> impl Iterator<Item = &OwnBookLevel> {
331        self.bids.levels.values()
332    }
333
334    /// Returns an iterator over ask price levels.
335    pub fn asks(&self) -> impl Iterator<Item = &OwnBookLevel> {
336        self.asks.levels.values()
337    }
338
339    /// Returns the client order IDs currently on the bid side.
340    #[must_use]
341    pub fn bid_client_order_ids(&self) -> Vec<ClientOrderId> {
342        self.bids.cache.keys().copied().collect()
343    }
344
345    /// Returns the client order IDs currently on the ask side.
346    #[must_use]
347    pub fn ask_client_order_ids(&self) -> Vec<ClientOrderId> {
348        self.asks.cache.keys().copied().collect()
349    }
350
351    /// Return whether the given client order ID is in the own book.
352    #[must_use]
353    pub fn is_order_in_book(&self, client_order_id: &ClientOrderId) -> bool {
354        self.asks.cache.contains_key(client_order_id)
355            || self.bids.cache.contains_key(client_order_id)
356    }
357
358    /// Maps bid price levels to their own orders, excluding empty levels after filtering.
359    ///
360    /// Filters by `status` if provided. With `accepted_buffer_ns`, only includes orders accepted
361    /// at least that many nanoseconds before `ts_now` (defaults to now).
362    #[must_use]
363    pub fn bids_as_map(
364        &self,
365        status: Option<&AHashSet<OrderStatus>>,
366        accepted_buffer_ns: Option<u64>,
367        ts_now: Option<u64>,
368    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
369        filter_orders(self.bids(), status, accepted_buffer_ns, ts_now)
370    }
371
372    /// Maps ask price levels to their own orders, excluding empty levels after filtering.
373    ///
374    /// Filters by `status` if provided. With `accepted_buffer_ns`, only includes orders accepted
375    /// at least that many nanoseconds before `ts_now` (defaults to now).
376    #[must_use]
377    pub fn asks_as_map(
378        &self,
379        status: Option<&AHashSet<OrderStatus>>,
380        accepted_buffer_ns: Option<u64>,
381        ts_now: Option<u64>,
382    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
383        filter_orders(self.asks(), status, accepted_buffer_ns, ts_now)
384    }
385
386    /// Aggregates own bid quantities per price level, omitting zero-quantity levels.
387    ///
388    /// Filters by `status` if provided, including only matching orders. With `accepted_buffer_ns`,
389    /// only includes orders accepted at least that many nanoseconds before `ts_now` (defaults to now).
390    ///
391    /// If `group_size` is provided, groups quantities into price buckets.
392    /// If `depth` is provided, limits the number of price levels returned.
393    #[must_use]
394    pub fn bid_quantity(
395        &self,
396        status: Option<&AHashSet<OrderStatus>>,
397        depth: Option<usize>,
398        group_size: Option<Decimal>,
399        accepted_buffer_ns: Option<u64>,
400        ts_now: Option<u64>,
401    ) -> IndexMap<Decimal, Decimal> {
402        let quantities = self
403            .bids_as_map(status, accepted_buffer_ns, ts_now)
404            .into_iter()
405            .map(|(price, orders)| (price, sum_order_sizes(orders.iter())))
406            .filter(|(_, quantity)| *quantity > Decimal::ZERO)
407            .collect::<IndexMap<Decimal, Decimal>>();
408
409        if let Some(group_size) = group_size {
410            group_quantities(quantities, group_size, depth, true)
411        } else if let Some(depth) = depth {
412            quantities.into_iter().take(depth).collect()
413        } else {
414            quantities
415        }
416    }
417
418    /// Aggregates own ask quantities per price level, omitting zero-quantity levels.
419    ///
420    /// Filters by `status` if provided, including only matching orders. With `accepted_buffer_ns`,
421    /// only includes orders accepted at least that many nanoseconds before `ts_now` (defaults to now).
422    ///
423    /// If `group_size` is provided, groups quantities into price buckets.
424    /// If `depth` is provided, limits the number of price levels returned.
425    #[must_use]
426    pub fn ask_quantity(
427        &self,
428        status: Option<&AHashSet<OrderStatus>>,
429        depth: Option<usize>,
430        group_size: Option<Decimal>,
431        accepted_buffer_ns: Option<u64>,
432        ts_now: Option<u64>,
433    ) -> IndexMap<Decimal, Decimal> {
434        let quantities = self
435            .asks_as_map(status, accepted_buffer_ns, ts_now)
436            .into_iter()
437            .map(|(price, orders)| {
438                let quantity = sum_order_sizes(orders.iter());
439                (price, quantity)
440            })
441            .filter(|(_, quantity)| *quantity > Decimal::ZERO)
442            .collect::<IndexMap<Decimal, Decimal>>();
443
444        if let Some(group_size) = group_size {
445            group_quantities(quantities, group_size, depth, false)
446        } else if let Some(depth) = depth {
447            quantities.into_iter().take(depth).collect()
448        } else {
449            quantities
450        }
451    }
452
453    /// Returns a new own book containing this books orders plus parity-transformed opposite orders.
454    ///
455    /// Opposite asks are transformed into bids with price `1 - price`.
456    /// Opposite bids are transformed into asks with price `1 - price`.
457    ///
458    /// # Errors
459    ///
460    /// Returns [`BookViewError::OppositeInstrumentMatch`] if `self` and `opposite` have the
461    /// same instrument ID.
462    pub fn combined_with_opposite(&self, opposite: &Self) -> Result<Self, BookViewError> {
463        if self.instrument_id == opposite.instrument_id {
464            return Err(BookViewError::OppositeInstrumentMatch(
465                self.instrument_id,
466                opposite.instrument_id,
467            ));
468        }
469
470        let mut combined = self.clone();
471
472        for level in opposite.asks() {
473            for order in level.iter() {
474                combined.add(transform_opposite_order(*order, OrderSideSpecified::Buy));
475            }
476        }
477
478        for level in opposite.bids() {
479            for order in level.iter() {
480                combined.add(transform_opposite_order(*order, OrderSideSpecified::Sell));
481            }
482        }
483
484        Ok(combined)
485    }
486
487    /// Return a formatted string representation of the order book.
488    #[must_use]
489    pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
490        pprint_own_book(self, num_levels, group_size)
491    }
492
493    pub fn audit_open_orders(&mut self, open_order_ids: &AHashSet<ClientOrderId>) {
494        log::debug!("Auditing {self}");
495
496        // Audit bids
497        let bids_to_remove: Vec<ClientOrderId> = self
498            .bids
499            .cache
500            .keys()
501            .filter(|&key| !open_order_ids.contains(key))
502            .copied()
503            .collect();
504
505        // Audit asks
506        let asks_to_remove: Vec<ClientOrderId> = self
507            .asks
508            .cache
509            .keys()
510            .filter(|&key| !open_order_ids.contains(key))
511            .copied()
512            .collect();
513
514        for client_order_id in bids_to_remove {
515            log_audit_error(&client_order_id);
516            if let Err(e) = self.bids.remove(&client_order_id) {
517                log::error!("{e}");
518            }
519        }
520
521        for client_order_id in asks_to_remove {
522            log_audit_error(&client_order_id);
523            if let Err(e) = self.asks.remove(&client_order_id) {
524                log::error!("{e}");
525            }
526        }
527    }
528}
529
530fn log_audit_error(client_order_id: &ClientOrderId) {
531    log::error!(
532        "Audit error - {client_order_id} cached order already closed, deleting from own book"
533    );
534}
535
536fn transform_opposite_order(order: OwnBookOrder, side: OrderSideSpecified) -> OwnBookOrder {
537    let parity_price = Price::from_decimal(Decimal::ONE - order.price.as_decimal())
538        .expect("Invalid parity transformed price for OwnOrderBook::combined_with_opposite");
539
540    OwnBookOrder::new(
541        order.trader_id,
542        order.client_order_id,
543        order.venue_order_id,
544        side,
545        parity_price,
546        order.size,
547        order.order_type,
548        order.time_in_force,
549        order.status,
550        order.ts_last,
551        order.ts_accepted,
552        order.ts_submitted,
553        order.ts_init,
554    )
555}
556
557/// Filters orders by status and accepted timestamp.
558///
559/// `accepted_buffer_ns` acts as a grace period after `ts_accepted`. Orders whose
560/// `ts_accepted` is still zero (e.g. SUBMITTED/PENDING state before an ACCEPTED
561/// event) will pass the buffer check once `ts_now` exceeds the buffer, even though
562/// they have not been venue-acknowledged yet. Callers that want to hide inflight
563/// orders must additionally filter by `OrderStatus` (for example, include only
564/// `ACCEPTED` / `PARTIALLY_FILLED`).
565fn filter_orders<'a>(
566    levels: impl Iterator<Item = &'a OwnBookLevel>,
567    status: Option<&AHashSet<OrderStatus>>,
568    accepted_buffer_ns: Option<u64>,
569    ts_now: Option<u64>,
570) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
571    let accepted_buffer_ns = accepted_buffer_ns.unwrap_or(0);
572    let ts_now = ts_now.unwrap_or_else(nanos_since_unix_epoch);
573    levels
574        .map(|level| {
575            let orders = level
576                .orders
577                .values()
578                .filter(|order| status.is_none_or(|f| f.contains(&order.status)))
579                .filter(|order| order.ts_accepted + accepted_buffer_ns <= ts_now)
580                .copied()
581                .collect::<Vec<OwnBookOrder>>();
582
583            (level.price.value.as_decimal(), orders)
584        })
585        .filter(|(_, orders)| !orders.is_empty())
586        .collect::<IndexMap<Decimal, Vec<OwnBookOrder>>>()
587}
588
589fn group_quantities(
590    quantities: IndexMap<Decimal, Decimal>,
591    group_size: Decimal,
592    depth: Option<usize>,
593    is_bid: bool,
594) -> IndexMap<Decimal, Decimal> {
595    if group_size <= Decimal::ZERO {
596        log::error!("Invalid group_size: {group_size}, must be positive; returning empty map");
597        return IndexMap::new();
598    }
599
600    let mut grouped = IndexMap::new();
601    let depth = depth.unwrap_or(usize::MAX);
602
603    for (price, size) in quantities {
604        let grouped_price = if is_bid {
605            (price / group_size).floor() * group_size
606        } else {
607            (price / group_size).ceil() * group_size
608        };
609
610        grouped
611            .entry(grouped_price)
612            .and_modify(|total| *total += size)
613            .or_insert(size);
614
615        if grouped.len() > depth {
616            if is_bid {
617                // For bids, remove the lowest price level
618                if let Some((lowest_price, _)) = grouped.iter().min_by_key(|(price, _)| *price) {
619                    let lowest_price = *lowest_price;
620                    grouped.shift_remove(&lowest_price);
621                }
622            } else {
623                // For asks, remove the highest price level
624                if let Some((highest_price, _)) = grouped.iter().max_by_key(|(price, _)| *price) {
625                    let highest_price = *highest_price;
626                    grouped.shift_remove(&highest_price);
627                }
628            }
629        }
630    }
631
632    grouped
633}
634
635fn sum_order_sizes<'a, I>(orders: I) -> Decimal
636where
637    I: Iterator<Item = &'a OwnBookOrder>,
638{
639    orders.fold(Decimal::ZERO, |total, order| {
640        total + order.size.as_decimal()
641    })
642}
643
644/// Represents a ladder of price levels for one side of an order book.
645#[derive(Clone)]
646pub(crate) struct OwnBookLadder {
647    pub side: OrderSideSpecified,
648    pub levels: BTreeMap<BookPrice, OwnBookLevel>,
649    pub cache: IndexMap<ClientOrderId, BookPrice>,
650}
651
652impl OwnBookLadder {
653    /// Creates a new [`OwnBookLadder`] instance.
654    #[must_use]
655    pub fn new(side: OrderSideSpecified) -> Self {
656        Self {
657            side,
658            levels: BTreeMap::new(),
659            cache: IndexMap::new(),
660        }
661    }
662
663    /// Returns the number of price levels in the ladder.
664    #[must_use]
665    #[allow(dead_code)]
666    pub fn len(&self) -> usize {
667        self.levels.len()
668    }
669
670    /// Returns true if the ladder has no price levels.
671    #[must_use]
672    #[allow(dead_code)]
673    pub fn is_empty(&self) -> bool {
674        self.levels.is_empty()
675    }
676
677    /// Removes all orders and price levels from the ladder.
678    pub fn clear(&mut self) {
679        self.levels.clear();
680        self.cache.clear();
681    }
682
683    /// Adds an order to the ladder at its price level.
684    pub fn add(&mut self, order: OwnBookOrder) {
685        let book_price = order.to_book_price();
686        self.cache.insert(order.client_order_id, book_price);
687
688        if let Some(level) = self.levels.get_mut(&book_price) {
689            level.add(order);
690        } else {
691            let level = OwnBookLevel::from_order(order);
692            self.levels.insert(book_price, level);
693        }
694    }
695
696    /// Updates an existing order in the ladder, moving it to a new price level if needed.
697    ///
698    /// # Errors
699    ///
700    /// Returns an error if the order is not found.
701    pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
702        let Some(price) = self.cache.get(&order.client_order_id).copied() else {
703            log::error!(
704                "Own book update failed - order {client_order_id} not in cache",
705                client_order_id = order.client_order_id
706            );
707            anyhow::bail!(
708                "Order {} not found in own book (cache)",
709                order.client_order_id
710            );
711        };
712
713        let Some(level) = self.levels.get_mut(&price) else {
714            log::error!(
715                "Own book update failed - order {client_order_id} cached level {price:?} missing",
716                client_order_id = order.client_order_id
717            );
718            anyhow::bail!(
719                "Order {} not found in own book (level)",
720                order.client_order_id
721            );
722        };
723
724        if order.price == level.price.value {
725            level.update(order);
726            if order.size.is_zero() {
727                self.cache.shift_remove(&order.client_order_id);
728
729                if level.is_empty() {
730                    self.levels.remove(&price);
731                }
732            }
733            return Ok(());
734        }
735
736        level.delete(&order.client_order_id)?;
737        self.cache.shift_remove(&order.client_order_id);
738
739        if level.is_empty() {
740            self.levels.remove(&price);
741        }
742
743        self.add(order);
744        Ok(())
745    }
746
747    /// Deletes an order from the ladder.
748    ///
749    /// # Errors
750    ///
751    /// Returns an error if the order is not found.
752    pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
753        self.remove(&order.client_order_id)
754    }
755
756    /// Removes an order by its ID from the ladder.
757    ///
758    /// # Errors
759    ///
760    /// Returns an error if the order is not found.
761    pub fn remove(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
762        let Some(price) = self.cache.get(client_order_id).copied() else {
763            log::error!("Own book remove failed - order {client_order_id} not in cache");
764            anyhow::bail!("Order {client_order_id} not found in own book (cache)");
765        };
766
767        let Some(level) = self.levels.get_mut(&price) else {
768            log::error!(
769                "Own book remove failed - order {client_order_id} cached level {price:?} missing"
770            );
771            anyhow::bail!("Order {client_order_id} not found in own book (level)");
772        };
773
774        level.delete(client_order_id)?;
775
776        if level.is_empty() {
777            self.levels.remove(&price);
778        }
779        self.cache.shift_remove(client_order_id);
780
781        Ok(())
782    }
783
784    /// Returns the total size of all orders in the ladder.
785    #[must_use]
786    #[allow(dead_code)]
787    pub fn sizes(&self) -> f64 {
788        self.levels.values().map(OwnBookLevel::size).sum()
789    }
790
791    /// Returns the total value exposure (price * size) of all orders in the ladder.
792    #[must_use]
793    #[allow(dead_code)]
794    pub fn exposures(&self) -> f64 {
795        self.levels.values().map(OwnBookLevel::exposure).sum()
796    }
797
798    /// Returns the best price level in the ladder.
799    #[must_use]
800    #[allow(dead_code)]
801    pub fn top(&self) -> Option<&OwnBookLevel> {
802        match self.levels.iter().next() {
803            Some((_, l)) => Option::Some(l),
804            None => Option::None,
805        }
806    }
807}
808
809impl Debug for OwnBookLadder {
810    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
811        f.debug_struct(stringify!(OwnBookLadder))
812            .field("side", &self.side)
813            .field("levels", &self.levels)
814            .field("cache", &self.cache)
815            .finish()
816    }
817}
818
819impl Display for OwnBookLadder {
820    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
821        writeln!(f, "{}(side={})", stringify!(OwnBookLadder), self.side)?;
822        for (price, level) in &self.levels {
823            writeln!(f, "  {} -> {} orders", price, level.len())?;
824        }
825        Ok(())
826    }
827}
828
829#[derive(Clone, Debug)]
830pub struct OwnBookLevel {
831    pub price: BookPrice,
832    pub orders: IndexMap<ClientOrderId, OwnBookOrder>,
833}
834
835impl OwnBookLevel {
836    /// Creates a new [`OwnBookLevel`] instance.
837    #[must_use]
838    pub fn new(price: BookPrice) -> Self {
839        Self {
840            price,
841            orders: IndexMap::new(),
842        }
843    }
844
845    /// Creates a new [`OwnBookLevel`] from an order, using the order's price and side.
846    #[must_use]
847    pub fn from_order(order: OwnBookOrder) -> Self {
848        let mut level = Self {
849            price: order.to_book_price(),
850            orders: IndexMap::new(),
851        };
852        level.orders.insert(order.client_order_id, order);
853        level
854    }
855
856    /// Returns the number of orders at this price level.
857    #[must_use]
858    pub fn len(&self) -> usize {
859        self.orders.len()
860    }
861
862    /// Returns true if this price level has no orders.
863    #[must_use]
864    pub fn is_empty(&self) -> bool {
865        self.orders.is_empty()
866    }
867
868    /// Returns a reference to the first order at this price level in FIFO order.
869    #[must_use]
870    pub fn first(&self) -> Option<&OwnBookOrder> {
871        self.orders.get_index(0).map(|(_key, order)| order)
872    }
873
874    /// Returns an iterator over the orders at this price level in FIFO order.
875    pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
876        self.orders.values()
877    }
878
879    /// Returns all orders at this price level in FIFO insertion order.
880    #[must_use]
881    pub fn get_orders(&self) -> Vec<OwnBookOrder> {
882        self.orders.values().copied().collect()
883    }
884
885    /// Returns the total size of all orders at this price level as a float.
886    #[must_use]
887    pub fn size(&self) -> f64 {
888        self.orders.iter().map(|(_, o)| o.size.as_f64()).sum()
889    }
890
891    /// Returns the total size of all orders at this price level as a decimal.
892    #[must_use]
893    pub fn size_decimal(&self) -> Decimal {
894        self.orders.iter().map(|(_, o)| o.size.as_decimal()).sum()
895    }
896
897    /// Returns the total exposure (price * size) of all orders at this price level as a float.
898    #[must_use]
899    pub fn exposure(&self) -> f64 {
900        self.orders
901            .iter()
902            .map(|(_, o)| o.price.as_f64() * o.size.as_f64())
903            .sum()
904    }
905
906    /// Adds multiple orders to this price level in FIFO order. Orders must match the level's price.
907    pub fn add_bulk(&mut self, orders: &[OwnBookOrder]) {
908        for order in orders {
909            self.add(*order);
910        }
911    }
912
913    /// Adds an order to this price level. Order must match the level's price.
914    pub fn add(&mut self, order: OwnBookOrder) {
915        debug_assert_eq!(order.price, self.price.value);
916
917        self.orders.insert(order.client_order_id, order);
918    }
919
920    /// Updates an existing order at this price level. Updated order must match the level's price.
921    /// Removes the order if size becomes zero.
922    pub fn update(&mut self, order: OwnBookOrder) {
923        debug_assert_eq!(order.price, self.price.value);
924
925        if order.size.is_zero() {
926            self.orders.shift_remove(&order.client_order_id);
927        } else {
928            self.orders[&order.client_order_id] = order;
929        }
930    }
931
932    /// Deletes an order from this price level.
933    ///
934    /// # Errors
935    ///
936    /// Returns an error if the order is not found.
937    pub fn delete(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
938        if self.orders.shift_remove(client_order_id).is_none() {
939            // TODO: Use a generic anyhow result for now pending specific error types
940            anyhow::bail!("Order {client_order_id} not found for delete");
941        }
942        Ok(())
943    }
944}
945
946impl PartialEq for OwnBookLevel {
947    fn eq(&self, other: &Self) -> bool {
948        self.price == other.price
949    }
950}
951
952impl Eq for OwnBookLevel {}
953
954impl PartialOrd for OwnBookLevel {
955    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
956        Some(self.cmp(other))
957    }
958}
959
960impl Ord for OwnBookLevel {
961    fn cmp(&self, other: &Self) -> Ordering {
962        self.price.cmp(&other.price)
963    }
964}
965
966#[must_use]
967pub fn should_handle_own_book_order(order: &OrderAny) -> bool {
968    order.has_price()
969        && order.time_in_force() != TimeInForce::Ioc
970        && order.time_in_force() != TimeInForce::Fok
971}