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(
371 &self,
372 status: Option<HashSet<OrderStatus>>,
373 depth: Option<usize>,
374 group_size: Option<Decimal>,
375 accepted_buffer_ns: Option<u64>,
376 ts_now: Option<u64>,
377 ) -> IndexMap<Decimal, Decimal> {
378 let quantities = self
379 .bids_as_map(status, accepted_buffer_ns, ts_now)
380 .into_iter()
381 .map(|(price, orders)| (price, sum_order_sizes(orders.iter())))
382 .filter(|(_, quantity)| *quantity > Decimal::ZERO)
383 .collect::<IndexMap<Decimal, Decimal>>();
384
385 if let Some(group_size) = group_size {
386 group_quantities(quantities, group_size, depth, true)
387 } else if let Some(depth) = depth {
388 quantities.into_iter().take(depth).collect()
389 } else {
390 quantities
391 }
392 }
393
394 pub fn ask_quantity(
402 &self,
403 status: Option<HashSet<OrderStatus>>,
404 depth: Option<usize>,
405 group_size: Option<Decimal>,
406 accepted_buffer_ns: Option<u64>,
407 ts_now: Option<u64>,
408 ) -> IndexMap<Decimal, Decimal> {
409 let quantities = self
410 .asks_as_map(status, accepted_buffer_ns, ts_now)
411 .into_iter()
412 .map(|(price, orders)| {
413 let quantity = sum_order_sizes(orders.iter());
414 (price, quantity)
415 })
416 .filter(|(_, quantity)| *quantity > Decimal::ZERO)
417 .collect::<IndexMap<Decimal, Decimal>>();
418
419 if let Some(group_size) = group_size {
420 group_quantities(quantities, group_size, depth, false)
421 } else if let Some(depth) = depth {
422 quantities.into_iter().take(depth).collect()
423 } else {
424 quantities
425 }
426 }
427
428 #[must_use]
430 pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
431 pprint_own_book(self, num_levels, group_size)
432 }
433
434 pub fn audit_open_orders(&mut self, open_order_ids: &HashSet<ClientOrderId>) {
435 log::debug!("Auditing {self}");
436
437 let bids_to_remove: Vec<ClientOrderId> = self
439 .bids
440 .cache
441 .keys()
442 .filter(|&key| !open_order_ids.contains(key))
443 .cloned()
444 .collect();
445
446 let asks_to_remove: Vec<ClientOrderId> = self
448 .asks
449 .cache
450 .keys()
451 .filter(|&key| !open_order_ids.contains(key))
452 .cloned()
453 .collect();
454
455 for client_order_id in bids_to_remove {
456 log_audit_error(&client_order_id);
457 if let Err(e) = self.bids.remove(&client_order_id) {
458 log::error!("{e}");
459 }
460 }
461
462 for client_order_id in asks_to_remove {
463 log_audit_error(&client_order_id);
464 if let Err(e) = self.asks.remove(&client_order_id) {
465 log::error!("{e}");
466 }
467 }
468 }
469}
470
471fn log_audit_error(client_order_id: &ClientOrderId) {
472 log::error!(
473 "Audit error - {client_order_id} cached order already closed, deleting from own book"
474 );
475}
476
477fn filter_orders<'a>(
478 levels: impl Iterator<Item = &'a OwnBookLevel>,
479 status: Option<&HashSet<OrderStatus>>,
480 accepted_buffer_ns: Option<u64>,
481 ts_now: Option<u64>,
482) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
483 let accepted_buffer_ns = accepted_buffer_ns.unwrap_or(0);
484 let ts_now = ts_now.unwrap_or_else(nanos_since_unix_epoch);
485 levels
486 .map(|level| {
487 let orders = level
488 .orders
489 .values()
490 .filter(|order| status.is_none_or(|f| f.contains(&order.status)))
491 .filter(|order| order.ts_accepted + accepted_buffer_ns <= ts_now)
492 .cloned()
493 .collect::<Vec<OwnBookOrder>>();
494
495 (level.price.value.as_decimal(), orders)
496 })
497 .filter(|(_, orders)| !orders.is_empty())
498 .collect::<IndexMap<Decimal, Vec<OwnBookOrder>>>()
499}
500
501fn group_quantities(
502 quantities: IndexMap<Decimal, Decimal>,
503 group_size: Decimal,
504 depth: Option<usize>,
505 is_bid: bool,
506) -> IndexMap<Decimal, Decimal> {
507 let mut grouped = IndexMap::new();
508 let depth = depth.unwrap_or(usize::MAX);
509
510 for (price, size) in quantities {
511 let grouped_price = if is_bid {
512 (price / group_size).floor() * group_size
513 } else {
514 (price / group_size).ceil() * group_size
515 };
516
517 grouped
518 .entry(grouped_price)
519 .and_modify(|total| *total += size)
520 .or_insert(size);
521
522 if grouped.len() > depth {
523 if is_bid {
524 if let Some((lowest_price, _)) = grouped.iter().min_by_key(|(price, _)| *price) {
526 let lowest_price = *lowest_price;
527 grouped.shift_remove(&lowest_price);
528 }
529 } else {
530 if let Some((highest_price, _)) = grouped.iter().max_by_key(|(price, _)| *price) {
532 let highest_price = *highest_price;
533 grouped.shift_remove(&highest_price);
534 }
535 }
536 }
537 }
538
539 grouped
540}
541
542fn sum_order_sizes<'a, I>(orders: I) -> Decimal
543where
544 I: Iterator<Item = &'a OwnBookOrder>,
545{
546 orders.fold(Decimal::ZERO, |total, order| {
547 total + order.size.as_decimal()
548 })
549}
550
551pub(crate) struct OwnBookLadder {
553 pub side: OrderSideSpecified,
554 pub levels: BTreeMap<BookPrice, OwnBookLevel>,
555 pub cache: HashMap<ClientOrderId, BookPrice>,
556}
557
558impl OwnBookLadder {
559 #[must_use]
561 pub fn new(side: OrderSideSpecified) -> Self {
562 Self {
563 side,
564 levels: BTreeMap::new(),
565 cache: HashMap::new(),
566 }
567 }
568
569 #[must_use]
571 #[allow(dead_code)] pub fn len(&self) -> usize {
573 self.levels.len()
574 }
575
576 #[must_use]
578 #[allow(dead_code)] pub fn is_empty(&self) -> bool {
580 self.levels.is_empty()
581 }
582
583 pub fn clear(&mut self) {
585 self.levels.clear();
586 self.cache.clear();
587 }
588
589 pub fn add(&mut self, order: OwnBookOrder) {
591 let book_price = order.to_book_price();
592 self.cache.insert(order.client_order_id, book_price);
593
594 match self.levels.get_mut(&book_price) {
595 Some(level) => {
596 level.add(order);
597 }
598 None => {
599 let level = OwnBookLevel::from_order(order);
600 self.levels.insert(book_price, level);
601 }
602 }
603 }
604
605 pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
611 let price = self.cache.get(&order.client_order_id).copied();
612 if let Some(price) = price
613 && let Some(level) = self.levels.get_mut(&price)
614 {
615 if order.price == level.price.value {
616 level.update(order);
618 return Ok(());
619 }
620
621 self.cache.remove(&order.client_order_id);
623 level.delete(&order.client_order_id)?;
624 if level.is_empty() {
625 self.levels.remove(&price);
626 }
627 }
628
629 self.add(order);
630 Ok(())
631 }
632
633 pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
639 self.remove(&order.client_order_id)
640 }
641
642 pub fn remove(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
648 if let Some(price) = self.cache.remove(client_order_id)
649 && let Some(level) = self.levels.get_mut(&price)
650 {
651 level.delete(client_order_id)?;
652 if level.is_empty() {
653 self.levels.remove(&price);
654 }
655 }
656
657 Ok(())
658 }
659
660 #[must_use]
662 #[allow(dead_code)] pub fn sizes(&self) -> f64 {
664 self.levels.values().map(OwnBookLevel::size).sum()
665 }
666
667 #[must_use]
669 #[allow(dead_code)] pub fn exposures(&self) -> f64 {
671 self.levels.values().map(OwnBookLevel::exposure).sum()
672 }
673
674 #[must_use]
676 #[allow(dead_code)] pub fn top(&self) -> Option<&OwnBookLevel> {
678 match self.levels.iter().next() {
679 Some((_, l)) => Option::Some(l),
680 None => Option::None,
681 }
682 }
683}
684
685impl Debug for OwnBookLadder {
686 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
687 f.debug_struct(stringify!(OwnBookLadder))
688 .field("side", &self.side)
689 .field("levels", &self.levels)
690 .finish()
691 }
692}
693
694impl Display for OwnBookLadder {
695 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
696 writeln!(f, "{}(side={})", stringify!(OwnBookLadder), self.side)?;
697 for (price, level) in &self.levels {
698 writeln!(f, " {} -> {} orders", price, level.len())?;
699 }
700 Ok(())
701 }
702}
703
704#[derive(Clone, Debug)]
705pub struct OwnBookLevel {
706 pub price: BookPrice,
707 pub orders: IndexMap<ClientOrderId, OwnBookOrder>,
708}
709
710impl OwnBookLevel {
711 #[must_use]
713 pub fn new(price: BookPrice) -> Self {
714 Self {
715 price,
716 orders: IndexMap::new(),
717 }
718 }
719
720 #[must_use]
722 pub fn from_order(order: OwnBookOrder) -> Self {
723 let mut level = Self {
724 price: order.to_book_price(),
725 orders: IndexMap::new(),
726 };
727 level.orders.insert(order.client_order_id, order);
728 level
729 }
730
731 #[must_use]
733 pub fn len(&self) -> usize {
734 self.orders.len()
735 }
736
737 #[must_use]
739 pub fn is_empty(&self) -> bool {
740 self.orders.is_empty()
741 }
742
743 #[must_use]
745 pub fn first(&self) -> Option<&OwnBookOrder> {
746 self.orders.get_index(0).map(|(_key, order)| order)
747 }
748
749 pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
751 self.orders.values()
752 }
753
754 #[must_use]
756 pub fn get_orders(&self) -> Vec<OwnBookOrder> {
757 self.orders.values().copied().collect()
758 }
759
760 #[must_use]
762 pub fn size(&self) -> f64 {
763 self.orders.iter().map(|(_, o)| o.size.as_f64()).sum()
764 }
765
766 #[must_use]
768 pub fn size_decimal(&self) -> Decimal {
769 self.orders.iter().map(|(_, o)| o.size.as_decimal()).sum()
770 }
771
772 #[must_use]
774 pub fn exposure(&self) -> f64 {
775 self.orders
776 .iter()
777 .map(|(_, o)| o.price.as_f64() * o.size.as_f64())
778 .sum()
779 }
780
781 pub fn add_bulk(&mut self, orders: Vec<OwnBookOrder>) {
783 for order in orders {
784 self.add(order);
785 }
786 }
787
788 pub fn add(&mut self, order: OwnBookOrder) {
790 debug_assert_eq!(order.price, self.price.value);
791
792 self.orders.insert(order.client_order_id, order);
793 }
794
795 pub fn update(&mut self, order: OwnBookOrder) {
798 debug_assert_eq!(order.price, self.price.value);
799
800 self.orders[&order.client_order_id] = order;
801 }
802
803 pub fn delete(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
809 if self.orders.shift_remove(client_order_id).is_none() {
810 anyhow::bail!("Order {client_order_id} not found for delete");
812 };
813 Ok(())
814 }
815}
816
817impl PartialEq for OwnBookLevel {
818 fn eq(&self, other: &Self) -> bool {
819 self.price == other.price
820 }
821}
822
823impl Eq for OwnBookLevel {}
824
825impl PartialOrd for OwnBookLevel {
826 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
827 Some(self.cmp(other))
828 }
829}
830
831impl Ord for OwnBookLevel {
832 fn cmp(&self, other: &Self) -> Ordering {
833 self.price.cmp(&other.price)
834 }
835}
836
837pub fn should_handle_own_book_order(order: &OrderAny) -> bool {
838 order.has_price()
839 && order.time_in_force() != TimeInForce::Ioc
840 && order.time_in_force() != TimeInForce::Fok
841}