1use 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#[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 pub trader_id: TraderId,
54 pub client_order_id: ClientOrderId,
56 pub venue_order_id: Option<VenueOrderId>,
58 pub side: OrderSideSpecified,
60 pub price: Price,
62 pub size: Quantity,
64 pub order_type: OrderType,
66 pub time_in_force: TimeInForce,
68 pub status: OrderStatus,
70 pub ts_last: UnixNanos,
72 pub ts_accepted: UnixNanos,
74 pub ts_submitted: UnixNanos,
76 pub ts_init: UnixNanos,
78}
79
80impl OwnBookOrder {
81 #[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 #[must_use]
118 pub fn to_book_price(&self) -> BookPrice {
119 BookPrice::new(self.price, self.side)
120 }
121
122 #[must_use]
124 pub fn exposure(&self) -> f64 {
125 self.price.as_f64() * self.size.as_f64()
126 }
127
128 #[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 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 pub instrument_id: InstrumentId,
218 pub ts_last: UnixNanos,
220 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 #[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 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 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 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 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 pub fn clear(&mut self) {
318 self.bids.clear();
319 self.asks.clear();
320 }
321
322 pub fn bids(&self) -> impl Iterator<Item = &OwnBookLevel> {
324 self.bids.levels.values()
325 }
326
327 pub fn asks(&self) -> impl Iterator<Item = &OwnBookLevel> {
329 self.asks.levels.values()
330 }
331
332 pub fn bid_client_order_ids(&self) -> Vec<ClientOrderId> {
334 self.bids.cache.keys().copied().collect()
335 }
336
337 pub fn ask_client_order_ids(&self) -> Vec<ClientOrderId> {
339 self.asks.cache.keys().copied().collect()
340 }
341
342 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 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 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 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 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 #[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 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 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
488fn 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 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 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
575pub(crate) struct OwnBookLadder {
577 pub side: OrderSideSpecified,
578 pub levels: BTreeMap<BookPrice, OwnBookLevel>,
579 pub cache: AHashMap<ClientOrderId, BookPrice>,
580}
581
582impl OwnBookLadder {
583 #[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 #[must_use]
595 #[allow(dead_code)]
596 pub fn len(&self) -> usize {
597 self.levels.len()
598 }
599
600 #[must_use]
602 #[allow(dead_code)]
603 pub fn is_empty(&self) -> bool {
604 self.levels.is_empty()
605 }
606
607 pub fn clear(&mut self) {
609 self.levels.clear();
610 self.cache.clear();
611 }
612
613 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 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 pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
679 self.remove(&order.client_order_id)
680 }
681
682 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 #[must_use]
712 #[allow(dead_code)]
713 pub fn sizes(&self) -> f64 {
714 self.levels.values().map(OwnBookLevel::size).sum()
715 }
716
717 #[must_use]
719 #[allow(dead_code)]
720 pub fn exposures(&self) -> f64 {
721 self.levels.values().map(OwnBookLevel::exposure).sum()
722 }
723
724 #[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 #[must_use]
763 pub fn new(price: BookPrice) -> Self {
764 Self {
765 price,
766 orders: IndexMap::new(),
767 }
768 }
769
770 #[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 #[must_use]
783 pub fn len(&self) -> usize {
784 self.orders.len()
785 }
786
787 #[must_use]
789 pub fn is_empty(&self) -> bool {
790 self.orders.is_empty()
791 }
792
793 #[must_use]
795 pub fn first(&self) -> Option<&OwnBookOrder> {
796 self.orders.get_index(0).map(|(_key, order)| order)
797 }
798
799 pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
801 self.orders.values()
802 }
803
804 #[must_use]
806 pub fn get_orders(&self) -> Vec<OwnBookOrder> {
807 self.orders.values().copied().collect()
808 }
809
810 #[must_use]
812 pub fn size(&self) -> f64 {
813 self.orders.iter().map(|(_, o)| o.size.as_f64()).sum()
814 }
815
816 #[must_use]
818 pub fn size_decimal(&self) -> Decimal {
819 self.orders.iter().map(|(_, o)| o.size.as_decimal()).sum()
820 }
821
822 #[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 pub fn add_bulk(&mut self, orders: Vec<OwnBookOrder>) {
833 for order in orders {
834 self.add(order);
835 }
836 }
837
838 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 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 pub fn delete(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
859 if self.orders.shift_remove(client_order_id).is_none() {
860 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}