1use 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#[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 pub trader_id: TraderId,
53 pub client_order_id: ClientOrderId,
55 pub venue_order_id: Option<VenueOrderId>,
57 pub side: OrderSideSpecified,
59 pub price: Price,
61 pub size: Quantity,
63 pub order_type: OrderType,
65 pub time_in_force: TimeInForce,
67 pub status: OrderStatus,
69 pub ts_last: UnixNanos,
71 pub ts_accepted: UnixNanos,
73 pub ts_submitted: UnixNanos,
75 pub ts_init: UnixNanos,
77}
78
79impl OwnBookOrder {
80 #[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 #[must_use]
117 pub fn to_book_price(&self) -> BookPrice {
118 BookPrice::new(self.price, self.side)
119 }
120
121 #[must_use]
123 pub fn exposure(&self) -> f64 {
124 self.price.as_f64() * self.size.as_f64()
125 }
126
127 #[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 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 pub instrument_id: InstrumentId,
217 pub ts_last: UnixNanos,
219 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 #[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 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 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 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 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 pub fn clear(&mut self) {
317 self.bids.clear();
318 self.asks.clear();
319 }
320
321 pub fn bids(&self) -> impl Iterator<Item = &OwnBookLevel> {
323 self.bids.levels.values()
324 }
325
326 pub fn asks(&self) -> impl Iterator<Item = &OwnBookLevel> {
328 self.asks.levels.values()
329 }
330
331 pub fn bid_client_order_ids(&self) -> Vec<ClientOrderId> {
333 self.bids.cache.keys().cloned().collect()
334 }
335
336 pub fn ask_client_order_ids(&self) -> Vec<ClientOrderId> {
338 self.asks.cache.keys().cloned().collect()
339 }
340
341 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 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 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 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 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 #[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 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 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
487fn 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 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 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
574pub(crate) struct OwnBookLadder {
576 pub side: OrderSideSpecified,
577 pub levels: BTreeMap<BookPrice, OwnBookLevel>,
578 pub cache: HashMap<ClientOrderId, BookPrice>,
579}
580
581impl OwnBookLadder {
582 #[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 #[must_use]
594 #[allow(dead_code, reason = "Used in tests")]
595 pub fn len(&self) -> usize {
596 self.levels.len()
597 }
598
599 #[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 pub fn clear(&mut self) {
608 self.levels.clear();
609 self.cache.clear();
610 }
611
612 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 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 pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
678 self.remove(&order.client_order_id)
679 }
680
681 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 #[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 #[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 #[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 #[must_use]
766 pub fn new(price: BookPrice) -> Self {
767 Self {
768 price,
769 orders: IndexMap::new(),
770 }
771 }
772
773 #[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 #[must_use]
786 pub fn len(&self) -> usize {
787 self.orders.len()
788 }
789
790 #[must_use]
792 pub fn is_empty(&self) -> bool {
793 self.orders.is_empty()
794 }
795
796 #[must_use]
798 pub fn first(&self) -> Option<&OwnBookOrder> {
799 self.orders.get_index(0).map(|(_key, order)| order)
800 }
801
802 pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
804 self.orders.values()
805 }
806
807 #[must_use]
809 pub fn get_orders(&self) -> Vec<OwnBookOrder> {
810 self.orders.values().copied().collect()
811 }
812
813 #[must_use]
815 pub fn size(&self) -> f64 {
816 self.orders.iter().map(|(_, o)| o.size.as_f64()).sum()
817 }
818
819 #[must_use]
821 pub fn size_decimal(&self) -> Decimal {
822 self.orders.iter().map(|(_, o)| o.size.as_decimal()).sum()
823 }
824
825 #[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 pub fn add_bulk(&mut self, orders: Vec<OwnBookOrder>) {
836 for order in orders {
837 self.add(order);
838 }
839 }
840
841 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 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 pub fn delete(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
862 if self.orders.shift_remove(client_order_id).is_none() {
863 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}