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