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