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 self.increment(&order);
286 match order.side {
287 OrderSideSpecified::Buy => self.bids.update(order),
288 OrderSideSpecified::Sell => self.asks.update(order),
289 }
290 }
291
292 pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
298 self.increment(&order);
299 match order.side {
300 OrderSideSpecified::Buy => self.bids.delete(order),
301 OrderSideSpecified::Sell => self.asks.delete(order),
302 }
303 }
304
305 pub fn clear(&mut self) {
307 self.bids.clear();
308 self.asks.clear();
309 }
310
311 pub fn bids(&self) -> impl Iterator<Item = &OwnBookLevel> {
313 self.bids.levels.values()
314 }
315
316 pub fn asks(&self) -> impl Iterator<Item = &OwnBookLevel> {
318 self.asks.levels.values()
319 }
320
321 pub fn bid_client_order_ids(&self) -> Vec<ClientOrderId> {
323 self.bids.cache.keys().cloned().collect()
324 }
325
326 pub fn ask_client_order_ids(&self) -> Vec<ClientOrderId> {
328 self.asks.cache.keys().cloned().collect()
329 }
330
331 pub fn is_order_in_book(&self, client_order_id: &ClientOrderId) -> bool {
333 self.asks.cache.contains_key(client_order_id)
334 || self.bids.cache.contains_key(client_order_id)
335 }
336
337 pub fn bids_as_map(
342 &self,
343 status: Option<HashSet<OrderStatus>>,
344 accepted_buffer_ns: Option<u64>,
345 ts_now: Option<u64>,
346 ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
347 filter_orders(self.bids(), status.as_ref(), accepted_buffer_ns, ts_now)
348 }
349
350 pub fn asks_as_map(
355 &self,
356 status: Option<HashSet<OrderStatus>>,
357 accepted_buffer_ns: Option<u64>,
358 ts_now: Option<u64>,
359 ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
360 filter_orders(self.asks(), status.as_ref(), accepted_buffer_ns, ts_now)
361 }
362
363 pub fn bid_quantity(
368 &self,
369 status: Option<HashSet<OrderStatus>>,
370 accepted_buffer_ns: Option<u64>,
371 ts_now: Option<u64>,
372 ) -> IndexMap<Decimal, Decimal> {
373 self.bids_as_map(status, accepted_buffer_ns, ts_now)
374 .into_iter()
375 .map(|(price, orders)| (price, sum_order_sizes(orders.iter())))
376 .filter(|(_, quantity)| *quantity > Decimal::ZERO)
377 .collect()
378 }
379
380 pub fn ask_quantity(
385 &self,
386 status: Option<HashSet<OrderStatus>>,
387 accepted_buffer_ns: Option<u64>,
388 ts_now: Option<u64>,
389 ) -> IndexMap<Decimal, Decimal> {
390 self.asks_as_map(status, accepted_buffer_ns, ts_now)
391 .into_iter()
392 .map(|(price, orders)| {
393 let quantity = sum_order_sizes(orders.iter());
394 (price, quantity)
395 })
396 .filter(|(_, quantity)| *quantity > Decimal::ZERO)
397 .collect()
398 }
399
400 pub fn group_bids(
405 &self,
406 group_size: Decimal,
407 depth: Option<usize>,
408 status: Option<HashSet<OrderStatus>>,
409 accepted_buffer_ns: Option<u64>,
410 ts_now: Option<u64>,
411 ) -> IndexMap<Decimal, Decimal> {
412 let quantities = self.bid_quantity(status, accepted_buffer_ns, ts_now);
413 group_quantities(quantities, group_size, depth, true)
414 }
415
416 pub fn group_asks(
421 &self,
422 group_size: Decimal,
423 depth: Option<usize>,
424 status: Option<HashSet<OrderStatus>>,
425 accepted_buffer_ns: Option<u64>,
426 ts_now: Option<u64>,
427 ) -> IndexMap<Decimal, Decimal> {
428 let quantities = self.ask_quantity(status, accepted_buffer_ns, ts_now);
429 group_quantities(quantities, group_size, depth, false)
430 }
431
432 #[must_use]
434 pub fn pprint(&self, num_levels: usize) -> String {
435 pprint_own_book(&self.bids, &self.asks, num_levels)
436 }
437
438 pub fn audit_open_orders(&mut self, open_order_ids: &HashSet<ClientOrderId>) {
439 log::debug!("Auditing {self}");
440
441 let bids_to_remove: Vec<ClientOrderId> = self
443 .bids
444 .cache
445 .keys()
446 .filter(|&key| !open_order_ids.contains(key))
447 .cloned()
448 .collect();
449
450 let asks_to_remove: Vec<ClientOrderId> = self
452 .asks
453 .cache
454 .keys()
455 .filter(|&key| !open_order_ids.contains(key))
456 .cloned()
457 .collect();
458
459 for client_order_id in bids_to_remove {
460 log_audit_error(&client_order_id);
461 if let Err(e) = self.bids.remove(&client_order_id) {
462 log::error!("{e}");
463 }
464 }
465
466 for client_order_id in asks_to_remove {
467 log_audit_error(&client_order_id);
468 if let Err(e) = self.asks.remove(&client_order_id) {
469 log::error!("{e}");
470 }
471 }
472 }
473}
474
475fn log_audit_error(client_order_id: &ClientOrderId) {
476 log::error!(
477 "Audit error - {client_order_id} cached order already closed, deleting from own book"
478 );
479}
480
481fn filter_orders<'a>(
482 levels: impl Iterator<Item = &'a OwnBookLevel>,
483 status: Option<&HashSet<OrderStatus>>,
484 accepted_buffer_ns: Option<u64>,
485 ts_now: Option<u64>,
486) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
487 let accepted_buffer_ns = accepted_buffer_ns.unwrap_or(0);
488 let ts_now = ts_now.unwrap_or_else(nanos_since_unix_epoch);
489 levels
490 .map(|level| {
491 let orders = level
492 .orders
493 .values()
494 .filter(|order| status.is_none_or(|f| f.contains(&order.status)))
495 .filter(|order| order.ts_accepted + accepted_buffer_ns <= ts_now)
496 .cloned()
497 .collect::<Vec<OwnBookOrder>>();
498
499 (level.price.value.as_decimal(), orders)
500 })
501 .filter(|(_, orders)| !orders.is_empty())
502 .collect::<IndexMap<Decimal, Vec<OwnBookOrder>>>()
503}
504
505fn group_quantities(
506 quantities: IndexMap<Decimal, Decimal>,
507 group_size: Decimal,
508 depth: Option<usize>,
509 is_bid: bool,
510) -> IndexMap<Decimal, Decimal> {
511 let mut grouped = IndexMap::new();
512 let depth = depth.unwrap_or(usize::MAX);
513
514 for (price, size) in quantities {
515 let grouped_price = if is_bid {
516 (price / group_size).floor() * group_size
517 } else {
518 (price / group_size).ceil() * group_size
519 };
520
521 grouped
522 .entry(grouped_price)
523 .and_modify(|total| *total += size)
524 .or_insert(size);
525
526 if grouped.len() > depth {
527 if is_bid {
528 if let Some((lowest_price, _)) = grouped.iter().min_by_key(|(price, _)| *price) {
530 let lowest_price = *lowest_price;
531 grouped.shift_remove(&lowest_price);
532 }
533 } else {
534 if let Some((highest_price, _)) = grouped.iter().max_by_key(|(price, _)| *price) {
536 let highest_price = *highest_price;
537 grouped.shift_remove(&highest_price);
538 }
539 }
540 }
541 }
542
543 grouped
544}
545
546fn sum_order_sizes<'a, I>(orders: I) -> Decimal
547where
548 I: Iterator<Item = &'a OwnBookOrder>,
549{
550 orders.fold(Decimal::ZERO, |total, order| {
551 total + order.size.as_decimal()
552 })
553}
554
555pub(crate) struct OwnBookLadder {
557 pub side: OrderSideSpecified,
558 pub levels: BTreeMap<BookPrice, OwnBookLevel>,
559 pub cache: HashMap<ClientOrderId, BookPrice>,
560}
561
562impl OwnBookLadder {
563 #[must_use]
565 pub fn new(side: OrderSideSpecified) -> Self {
566 Self {
567 side,
568 levels: BTreeMap::new(),
569 cache: HashMap::new(),
570 }
571 }
572
573 #[must_use]
575 #[allow(dead_code)] pub fn len(&self) -> usize {
577 self.levels.len()
578 }
579
580 #[must_use]
582 #[allow(dead_code)] pub fn is_empty(&self) -> bool {
584 self.levels.is_empty()
585 }
586
587 pub fn clear(&mut self) {
589 self.levels.clear();
590 self.cache.clear();
591 }
592
593 pub fn add(&mut self, order: OwnBookOrder) {
595 let book_price = order.to_book_price();
596 self.cache.insert(order.client_order_id, book_price);
597
598 match self.levels.get_mut(&book_price) {
599 Some(level) => {
600 level.add(order);
601 }
602 None => {
603 let level = OwnBookLevel::from_order(order);
604 self.levels.insert(book_price, level);
605 }
606 }
607 }
608
609 pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
615 let price = self.cache.get(&order.client_order_id).copied();
616 if let Some(price) = price {
617 if let Some(level) = self.levels.get_mut(&price) {
618 if order.price == level.price.value {
619 level.update(order);
621 return Ok(());
622 }
623
624 self.cache.remove(&order.client_order_id);
626 level.delete(&order.client_order_id)?;
627 if level.is_empty() {
628 self.levels.remove(&price);
629 }
630 }
631 }
632
633 self.add(order);
634 Ok(())
635 }
636
637 pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
643 self.remove(&order.client_order_id)
644 }
645
646 pub fn remove(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
652 if let Some(price) = self.cache.remove(client_order_id) {
653 if let Some(level) = self.levels.get_mut(&price) {
654 level.delete(client_order_id)?;
655 if level.is_empty() {
656 self.levels.remove(&price);
657 }
658 }
659 }
660
661 Ok(())
662 }
663
664 #[must_use]
666 #[allow(dead_code)] pub fn sizes(&self) -> f64 {
668 self.levels.values().map(OwnBookLevel::size).sum()
669 }
670
671 #[must_use]
673 #[allow(dead_code)] pub fn exposures(&self) -> f64 {
675 self.levels.values().map(OwnBookLevel::exposure).sum()
676 }
677
678 #[must_use]
680 #[allow(dead_code)] pub fn top(&self) -> Option<&OwnBookLevel> {
682 match self.levels.iter().next() {
683 Some((_, l)) => Option::Some(l),
684 None => Option::None,
685 }
686 }
687}
688
689impl Debug for OwnBookLadder {
690 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
691 f.debug_struct(stringify!(OwnBookLadder))
692 .field("side", &self.side)
693 .field("levels", &self.levels)
694 .finish()
695 }
696}
697
698impl Display for OwnBookLadder {
699 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
700 writeln!(f, "{}(side={})", stringify!(OwnBookLadder), self.side)?;
701 for (price, level) in &self.levels {
702 writeln!(f, " {} -> {} orders", price, level.len())?;
703 }
704 Ok(())
705 }
706}
707
708#[derive(Clone, Debug)]
709pub struct OwnBookLevel {
710 pub price: BookPrice,
711 pub orders: IndexMap<ClientOrderId, OwnBookOrder>,
712}
713
714impl OwnBookLevel {
715 #[must_use]
717 pub fn new(price: BookPrice) -> Self {
718 Self {
719 price,
720 orders: IndexMap::new(),
721 }
722 }
723
724 #[must_use]
726 pub fn from_order(order: OwnBookOrder) -> Self {
727 let mut level = Self {
728 price: order.to_book_price(),
729 orders: IndexMap::new(),
730 };
731 level.orders.insert(order.client_order_id, order);
732 level
733 }
734
735 #[must_use]
737 pub fn len(&self) -> usize {
738 self.orders.len()
739 }
740
741 #[must_use]
743 pub fn is_empty(&self) -> bool {
744 self.orders.is_empty()
745 }
746
747 #[must_use]
749 pub fn first(&self) -> Option<&OwnBookOrder> {
750 self.orders.get_index(0).map(|(_key, order)| order)
751 }
752
753 pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
755 self.orders.values()
756 }
757
758 #[must_use]
760 pub fn get_orders(&self) -> Vec<OwnBookOrder> {
761 self.orders.values().copied().collect()
762 }
763
764 #[must_use]
766 pub fn size(&self) -> f64 {
767 self.orders.iter().map(|(_, o)| o.size.as_f64()).sum()
768 }
769
770 #[must_use]
772 pub fn size_decimal(&self) -> Decimal {
773 self.orders.iter().map(|(_, o)| o.size.as_decimal()).sum()
774 }
775
776 #[must_use]
778 pub fn exposure(&self) -> f64 {
779 self.orders
780 .iter()
781 .map(|(_, o)| o.price.as_f64() * o.size.as_f64())
782 .sum()
783 }
784
785 pub fn add_bulk(&mut self, orders: Vec<OwnBookOrder>) {
787 for order in orders {
788 self.add(order);
789 }
790 }
791
792 pub fn add(&mut self, order: OwnBookOrder) {
794 debug_assert_eq!(order.price, self.price.value);
795
796 self.orders.insert(order.client_order_id, order);
797 }
798
799 pub fn update(&mut self, order: OwnBookOrder) {
802 debug_assert_eq!(order.price, self.price.value);
803
804 self.orders[&order.client_order_id] = order;
805 }
806
807 pub fn delete(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
813 if self.orders.shift_remove(client_order_id).is_none() {
814 anyhow::bail!("Order {client_order_id} not found for delete");
816 };
817 Ok(())
818 }
819}
820
821impl PartialEq for OwnBookLevel {
822 fn eq(&self, other: &Self) -> bool {
823 self.price == other.price
824 }
825}
826
827impl Eq for OwnBookLevel {}
828
829impl PartialOrd for OwnBookLevel {
830 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
831 Some(self.cmp(other))
832 }
833}
834
835impl Ord for OwnBookLevel {
836 fn cmp(&self, other: &Self) -> Ordering {
837 self.price.cmp(&other.price)
838 }
839}
840
841pub fn should_handle_own_book_order(order: &OrderAny) -> bool {
842 order.has_price()
843 && order.time_in_force() != TimeInForce::Ioc
844 && order.time_in_force() != TimeInForce::Fok
845}