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<()> {
281 self.increment(&order);
282 match order.side {
283 OrderSideSpecified::Buy => self.bids.update(order),
284 OrderSideSpecified::Sell => self.asks.update(order),
285 }
286 }
287
288 pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
290 self.increment(&order);
291 match order.side {
292 OrderSideSpecified::Buy => self.bids.delete(order),
293 OrderSideSpecified::Sell => self.asks.delete(order),
294 }
295 }
296
297 pub fn clear(&mut self) {
299 self.bids.clear();
300 self.asks.clear();
301 }
302
303 pub fn bids(&self) -> impl Iterator<Item = &OwnBookLevel> {
305 self.bids.levels.values()
306 }
307
308 pub fn asks(&self) -> impl Iterator<Item = &OwnBookLevel> {
310 self.asks.levels.values()
311 }
312
313 pub fn bid_client_order_ids(&self) -> Vec<ClientOrderId> {
315 self.bids.cache.keys().cloned().collect()
316 }
317
318 pub fn ask_client_order_ids(&self) -> Vec<ClientOrderId> {
320 self.asks.cache.keys().cloned().collect()
321 }
322
323 pub fn is_order_in_book(&self, client_order_id: &ClientOrderId) -> bool {
325 self.asks.cache.contains_key(client_order_id)
326 || self.bids.cache.contains_key(client_order_id)
327 }
328
329 pub fn bids_as_map(
334 &self,
335 status: Option<HashSet<OrderStatus>>,
336 accepted_buffer_ns: Option<u64>,
337 ts_now: Option<u64>,
338 ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
339 filter_orders(self.bids(), status.as_ref(), accepted_buffer_ns, ts_now)
340 }
341
342 pub fn asks_as_map(
347 &self,
348 status: Option<HashSet<OrderStatus>>,
349 accepted_buffer_ns: Option<u64>,
350 ts_now: Option<u64>,
351 ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
352 filter_orders(self.asks(), status.as_ref(), accepted_buffer_ns, ts_now)
353 }
354
355 pub fn bid_quantity(
360 &self,
361 status: Option<HashSet<OrderStatus>>,
362 accepted_buffer_ns: Option<u64>,
363 ts_now: Option<u64>,
364 ) -> IndexMap<Decimal, Decimal> {
365 self.bids_as_map(status, accepted_buffer_ns, ts_now)
366 .into_iter()
367 .map(|(price, orders)| (price, sum_order_sizes(orders.iter())))
368 .filter(|(_, quantity)| *quantity > Decimal::ZERO)
369 .collect()
370 }
371
372 pub fn ask_quantity(
377 &self,
378 status: Option<HashSet<OrderStatus>>,
379 accepted_buffer_ns: Option<u64>,
380 ts_now: Option<u64>,
381 ) -> IndexMap<Decimal, Decimal> {
382 self.asks_as_map(status, accepted_buffer_ns, ts_now)
383 .into_iter()
384 .map(|(price, orders)| {
385 let quantity = sum_order_sizes(orders.iter());
386 (price, quantity)
387 })
388 .filter(|(_, quantity)| *quantity > Decimal::ZERO)
389 .collect()
390 }
391
392 pub fn group_bids(
397 &self,
398 group_size: Decimal,
399 depth: Option<usize>,
400 status: Option<HashSet<OrderStatus>>,
401 accepted_buffer_ns: Option<u64>,
402 ts_now: Option<u64>,
403 ) -> IndexMap<Decimal, Decimal> {
404 let quantities = self.bid_quantity(status, accepted_buffer_ns, ts_now);
405 group_quantities(quantities, group_size, depth, true)
406 }
407
408 pub fn group_asks(
413 &self,
414 group_size: Decimal,
415 depth: Option<usize>,
416 status: Option<HashSet<OrderStatus>>,
417 accepted_buffer_ns: Option<u64>,
418 ts_now: Option<u64>,
419 ) -> IndexMap<Decimal, Decimal> {
420 let quantities = self.ask_quantity(status, accepted_buffer_ns, ts_now);
421 group_quantities(quantities, group_size, depth, false)
422 }
423
424 #[must_use]
426 pub fn pprint(&self, num_levels: usize) -> String {
427 pprint_own_book(&self.bids, &self.asks, num_levels)
428 }
429
430 pub fn audit_open_orders(&mut self, open_order_ids: &HashSet<ClientOrderId>) {
431 log::debug!("Auditing {self}");
432
433 let bids_to_remove: Vec<ClientOrderId> = self
435 .bids
436 .cache
437 .keys()
438 .filter(|&key| !open_order_ids.contains(key))
439 .cloned()
440 .collect();
441
442 let asks_to_remove: Vec<ClientOrderId> = self
444 .asks
445 .cache
446 .keys()
447 .filter(|&key| !open_order_ids.contains(key))
448 .cloned()
449 .collect();
450
451 for client_order_id in bids_to_remove {
452 log_audit_error(&client_order_id);
453 if let Err(e) = self.bids.remove(&client_order_id) {
454 log::error!("{e}");
455 }
456 }
457
458 for client_order_id in asks_to_remove {
459 log_audit_error(&client_order_id);
460 if let Err(e) = self.asks.remove(&client_order_id) {
461 log::error!("{e}");
462 }
463 }
464 }
465}
466
467fn log_audit_error(client_order_id: &ClientOrderId) {
468 log::error!(
469 "Audit error - {} cached order already closed, deleting from own book",
470 client_order_id
471 );
472}
473
474fn filter_orders<'a>(
475 levels: impl Iterator<Item = &'a OwnBookLevel>,
476 status: Option<&HashSet<OrderStatus>>,
477 accepted_buffer_ns: Option<u64>,
478 ts_now: Option<u64>,
479) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
480 let accepted_buffer_ns = accepted_buffer_ns.unwrap_or(0);
481 let ts_now = ts_now.unwrap_or_else(nanos_since_unix_epoch);
482 levels
483 .map(|level| {
484 let orders = level
485 .orders
486 .values()
487 .filter(|order| status.is_none_or(|f| f.contains(&order.status)))
488 .filter(|order| order.ts_accepted + accepted_buffer_ns <= ts_now)
489 .cloned()
490 .collect::<Vec<OwnBookOrder>>();
491
492 (level.price.value.as_decimal(), orders)
493 })
494 .filter(|(_, orders)| !orders.is_empty())
495 .collect::<IndexMap<Decimal, Vec<OwnBookOrder>>>()
496}
497
498fn group_quantities(
499 quantities: IndexMap<Decimal, Decimal>,
500 group_size: Decimal,
501 depth: Option<usize>,
502 is_bid: bool,
503) -> IndexMap<Decimal, Decimal> {
504 let mut grouped = IndexMap::new();
505 let depth = depth.unwrap_or(usize::MAX);
506
507 for (price, size) in quantities {
508 let grouped_price = if is_bid {
509 (price / group_size).floor() * group_size
510 } else {
511 (price / group_size).ceil() * group_size
512 };
513
514 grouped
515 .entry(grouped_price)
516 .and_modify(|total| *total += size)
517 .or_insert(size);
518
519 if grouped.len() > depth {
520 if is_bid {
521 if let Some((lowest_price, _)) = grouped.iter().min_by_key(|(price, _)| *price) {
523 let lowest_price = *lowest_price;
524 grouped.shift_remove(&lowest_price);
525 }
526 } else {
527 if let Some((highest_price, _)) = grouped.iter().max_by_key(|(price, _)| *price) {
529 let highest_price = *highest_price;
530 grouped.shift_remove(&highest_price);
531 }
532 }
533 }
534 }
535
536 grouped
537}
538
539fn sum_order_sizes<'a, I>(orders: I) -> Decimal
540where
541 I: Iterator<Item = &'a OwnBookOrder>,
542{
543 orders.fold(Decimal::ZERO, |total, order| {
544 total + order.size.as_decimal()
545 })
546}
547
548pub(crate) struct OwnBookLadder {
550 pub side: OrderSideSpecified,
551 pub levels: BTreeMap<BookPrice, OwnBookLevel>,
552 pub cache: HashMap<ClientOrderId, BookPrice>,
553}
554
555impl OwnBookLadder {
556 #[must_use]
558 pub fn new(side: OrderSideSpecified) -> Self {
559 Self {
560 side,
561 levels: BTreeMap::new(),
562 cache: HashMap::new(),
563 }
564 }
565
566 #[must_use]
568 #[allow(dead_code)] pub fn len(&self) -> usize {
570 self.levels.len()
571 }
572
573 #[must_use]
575 #[allow(dead_code)] pub fn is_empty(&self) -> bool {
577 self.levels.is_empty()
578 }
579
580 pub fn clear(&mut self) {
582 self.levels.clear();
583 self.cache.clear();
584 }
585
586 pub fn add(&mut self, order: OwnBookOrder) {
588 let book_price = order.to_book_price();
589 self.cache.insert(order.client_order_id, book_price);
590
591 match self.levels.get_mut(&book_price) {
592 Some(level) => {
593 level.add(order);
594 }
595 None => {
596 let level = OwnBookLevel::from_order(order);
597 self.levels.insert(book_price, level);
598 }
599 }
600 }
601
602 pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
604 let price = self.cache.get(&order.client_order_id).copied();
605 if let Some(price) = price {
606 if let Some(level) = self.levels.get_mut(&price) {
607 if order.price == level.price.value {
608 level.update(order);
610 return Ok(());
611 }
612
613 self.cache.remove(&order.client_order_id);
615 level.delete(&order.client_order_id)?;
616 if level.is_empty() {
617 self.levels.remove(&price);
618 }
619 }
620 }
621
622 self.add(order);
623 Ok(())
624 }
625
626 pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
628 self.remove(&order.client_order_id)
629 }
630
631 pub fn remove(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
633 if let Some(price) = self.cache.remove(client_order_id) {
634 if let Some(level) = self.levels.get_mut(&price) {
635 level.delete(client_order_id)?;
636 if level.is_empty() {
637 self.levels.remove(&price);
638 }
639 }
640 }
641
642 Ok(())
643 }
644
645 #[must_use]
647 #[allow(dead_code)] pub fn sizes(&self) -> f64 {
649 self.levels.values().map(OwnBookLevel::size).sum()
650 }
651
652 #[must_use]
654 #[allow(dead_code)] pub fn exposures(&self) -> f64 {
656 self.levels.values().map(OwnBookLevel::exposure).sum()
657 }
658
659 #[must_use]
661 #[allow(dead_code)] pub fn top(&self) -> Option<&OwnBookLevel> {
663 match self.levels.iter().next() {
664 Some((_, l)) => Option::Some(l),
665 None => Option::None,
666 }
667 }
668}
669
670impl Debug for OwnBookLadder {
671 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
672 f.debug_struct(stringify!(OwnBookLadder))
673 .field("side", &self.side)
674 .field("levels", &self.levels)
675 .finish()
676 }
677}
678
679impl Display for OwnBookLadder {
680 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
681 writeln!(f, "{}(side={})", stringify!(OwnBookLadder), self.side)?;
682 for (price, level) in &self.levels {
683 writeln!(f, " {} -> {} orders", price, level.len())?;
684 }
685 Ok(())
686 }
687}
688
689#[derive(Clone, Debug)]
690pub struct OwnBookLevel {
691 pub price: BookPrice,
692 pub orders: IndexMap<ClientOrderId, OwnBookOrder>,
693}
694
695impl OwnBookLevel {
696 #[must_use]
698 pub fn new(price: BookPrice) -> Self {
699 Self {
700 price,
701 orders: IndexMap::new(),
702 }
703 }
704
705 #[must_use]
707 pub fn from_order(order: OwnBookOrder) -> Self {
708 let mut level = Self {
709 price: order.to_book_price(),
710 orders: IndexMap::new(),
711 };
712 level.orders.insert(order.client_order_id, order);
713 level
714 }
715
716 #[must_use]
718 pub fn len(&self) -> usize {
719 self.orders.len()
720 }
721
722 #[must_use]
724 pub fn is_empty(&self) -> bool {
725 self.orders.is_empty()
726 }
727
728 #[must_use]
730 pub fn first(&self) -> Option<&OwnBookOrder> {
731 self.orders.get_index(0).map(|(_key, order)| order)
732 }
733
734 pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
736 self.orders.values()
737 }
738
739 #[must_use]
741 pub fn get_orders(&self) -> Vec<OwnBookOrder> {
742 self.orders.values().copied().collect()
743 }
744
745 #[must_use]
747 pub fn size(&self) -> f64 {
748 self.orders.iter().map(|(_, o)| o.size.as_f64()).sum()
749 }
750
751 #[must_use]
753 pub fn size_decimal(&self) -> Decimal {
754 self.orders.iter().map(|(_, o)| o.size.as_decimal()).sum()
755 }
756
757 #[must_use]
759 pub fn exposure(&self) -> f64 {
760 self.orders
761 .iter()
762 .map(|(_, o)| o.price.as_f64() * o.size.as_f64())
763 .sum()
764 }
765
766 pub fn add_bulk(&mut self, orders: Vec<OwnBookOrder>) {
768 for order in orders {
769 self.add(order);
770 }
771 }
772
773 pub fn add(&mut self, order: OwnBookOrder) {
775 debug_assert_eq!(order.price, self.price.value);
776
777 self.orders.insert(order.client_order_id, order);
778 }
779
780 pub fn update(&mut self, order: OwnBookOrder) {
783 debug_assert_eq!(order.price, self.price.value);
784
785 self.orders[&order.client_order_id] = order;
786 }
787
788 pub fn delete(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
790 if self.orders.shift_remove(client_order_id).is_none() {
791 anyhow::bail!("Order {client_order_id} not found for delete");
793 };
794 Ok(())
795 }
796}
797
798impl PartialEq for OwnBookLevel {
799 fn eq(&self, other: &Self) -> bool {
800 self.price == other.price
801 }
802}
803
804impl Eq for OwnBookLevel {}
805
806impl PartialOrd for OwnBookLevel {
807 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
808 Some(self.cmp(other))
809 }
810}
811
812impl Ord for OwnBookLevel {
813 fn cmp(&self, other: &Self) -> Ordering {
814 self.price.cmp(&other.price)
815 }
816}
817
818pub fn should_handle_own_book_order(order: &OrderAny) -> bool {
819 order.has_price()
820 && order.time_in_force() != TimeInForce::Ioc
821 && order.time_in_force() != TimeInForce::Fok
822}