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 = "posei_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 = "posei_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 - {} cached order already closed, deleting from own book",
478 client_order_id
479 );
480}
481
482fn filter_orders<'a>(
483 levels: impl Iterator<Item = &'a OwnBookLevel>,
484 status: Option<&HashSet<OrderStatus>>,
485 accepted_buffer_ns: Option<u64>,
486 ts_now: Option<u64>,
487) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
488 let accepted_buffer_ns = accepted_buffer_ns.unwrap_or(0);
489 let ts_now = ts_now.unwrap_or_else(nanos_since_unix_epoch);
490 levels
491 .map(|level| {
492 let orders = level
493 .orders
494 .values()
495 .filter(|order| status.is_none_or(|f| f.contains(&order.status)))
496 .filter(|order| order.ts_accepted + accepted_buffer_ns <= ts_now)
497 .cloned()
498 .collect::<Vec<OwnBookOrder>>();
499
500 (level.price.value.as_decimal(), orders)
501 })
502 .filter(|(_, orders)| !orders.is_empty())
503 .collect::<IndexMap<Decimal, Vec<OwnBookOrder>>>()
504}
505
506fn group_quantities(
507 quantities: IndexMap<Decimal, Decimal>,
508 group_size: Decimal,
509 depth: Option<usize>,
510 is_bid: bool,
511) -> IndexMap<Decimal, Decimal> {
512 let mut grouped = IndexMap::new();
513 let depth = depth.unwrap_or(usize::MAX);
514
515 for (price, size) in quantities {
516 let grouped_price = if is_bid {
517 (price / group_size).floor() * group_size
518 } else {
519 (price / group_size).ceil() * group_size
520 };
521
522 grouped
523 .entry(grouped_price)
524 .and_modify(|total| *total += size)
525 .or_insert(size);
526
527 if grouped.len() > depth {
528 if is_bid {
529 if let Some((lowest_price, _)) = grouped.iter().min_by_key(|(price, _)| *price) {
531 let lowest_price = *lowest_price;
532 grouped.shift_remove(&lowest_price);
533 }
534 } else {
535 if let Some((highest_price, _)) = grouped.iter().max_by_key(|(price, _)| *price) {
537 let highest_price = *highest_price;
538 grouped.shift_remove(&highest_price);
539 }
540 }
541 }
542 }
543
544 grouped
545}
546
547fn sum_order_sizes<'a, I>(orders: I) -> Decimal
548where
549 I: Iterator<Item = &'a OwnBookOrder>,
550{
551 orders.fold(Decimal::ZERO, |total, order| {
552 total + order.size.as_decimal()
553 })
554}
555
556pub(crate) struct OwnBookLadder {
558 pub side: OrderSideSpecified,
559 pub levels: BTreeMap<BookPrice, OwnBookLevel>,
560 pub cache: HashMap<ClientOrderId, BookPrice>,
561}
562
563impl OwnBookLadder {
564 #[must_use]
566 pub fn new(side: OrderSideSpecified) -> Self {
567 Self {
568 side,
569 levels: BTreeMap::new(),
570 cache: HashMap::new(),
571 }
572 }
573
574 #[must_use]
576 #[allow(dead_code)] pub fn len(&self) -> usize {
578 self.levels.len()
579 }
580
581 #[must_use]
583 #[allow(dead_code)] pub fn is_empty(&self) -> bool {
585 self.levels.is_empty()
586 }
587
588 pub fn clear(&mut self) {
590 self.levels.clear();
591 self.cache.clear();
592 }
593
594 pub fn add(&mut self, order: OwnBookOrder) {
596 let book_price = order.to_book_price();
597 self.cache.insert(order.client_order_id, book_price);
598
599 match self.levels.get_mut(&book_price) {
600 Some(level) => {
601 level.add(order);
602 }
603 None => {
604 let level = OwnBookLevel::from_order(order);
605 self.levels.insert(book_price, level);
606 }
607 }
608 }
609
610 pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
616 let price = self.cache.get(&order.client_order_id).copied();
617 if let Some(price) = price {
618 if let Some(level) = self.levels.get_mut(&price) {
619 if order.price == level.price.value {
620 level.update(order);
622 return Ok(());
623 }
624
625 self.cache.remove(&order.client_order_id);
627 level.delete(&order.client_order_id)?;
628 if level.is_empty() {
629 self.levels.remove(&price);
630 }
631 }
632 }
633
634 self.add(order);
635 Ok(())
636 }
637
638 pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
644 self.remove(&order.client_order_id)
645 }
646
647 pub fn remove(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
653 if let Some(price) = self.cache.remove(client_order_id) {
654 if let Some(level) = self.levels.get_mut(&price) {
655 level.delete(client_order_id)?;
656 if level.is_empty() {
657 self.levels.remove(&price);
658 }
659 }
660 }
661
662 Ok(())
663 }
664
665 #[must_use]
667 #[allow(dead_code)] pub fn sizes(&self) -> f64 {
669 self.levels.values().map(OwnBookLevel::size).sum()
670 }
671
672 #[must_use]
674 #[allow(dead_code)] pub fn exposures(&self) -> f64 {
676 self.levels.values().map(OwnBookLevel::exposure).sum()
677 }
678
679 #[must_use]
681 #[allow(dead_code)] pub fn top(&self) -> Option<&OwnBookLevel> {
683 match self.levels.iter().next() {
684 Some((_, l)) => Option::Some(l),
685 None => Option::None,
686 }
687 }
688}
689
690impl Debug for OwnBookLadder {
691 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
692 f.debug_struct(stringify!(OwnBookLadder))
693 .field("side", &self.side)
694 .field("levels", &self.levels)
695 .finish()
696 }
697}
698
699impl Display for OwnBookLadder {
700 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
701 writeln!(f, "{}(side={})", stringify!(OwnBookLadder), self.side)?;
702 for (price, level) in &self.levels {
703 writeln!(f, " {} -> {} orders", price, level.len())?;
704 }
705 Ok(())
706 }
707}
708
709#[derive(Clone, Debug)]
710pub struct OwnBookLevel {
711 pub price: BookPrice,
712 pub orders: IndexMap<ClientOrderId, OwnBookOrder>,
713}
714
715impl OwnBookLevel {
716 #[must_use]
718 pub fn new(price: BookPrice) -> Self {
719 Self {
720 price,
721 orders: IndexMap::new(),
722 }
723 }
724
725 #[must_use]
727 pub fn from_order(order: OwnBookOrder) -> Self {
728 let mut level = Self {
729 price: order.to_book_price(),
730 orders: IndexMap::new(),
731 };
732 level.orders.insert(order.client_order_id, order);
733 level
734 }
735
736 #[must_use]
738 pub fn len(&self) -> usize {
739 self.orders.len()
740 }
741
742 #[must_use]
744 pub fn is_empty(&self) -> bool {
745 self.orders.is_empty()
746 }
747
748 #[must_use]
750 pub fn first(&self) -> Option<&OwnBookOrder> {
751 self.orders.get_index(0).map(|(_key, order)| order)
752 }
753
754 pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
756 self.orders.values()
757 }
758
759 #[must_use]
761 pub fn get_orders(&self) -> Vec<OwnBookOrder> {
762 self.orders.values().copied().collect()
763 }
764
765 #[must_use]
767 pub fn size(&self) -> f64 {
768 self.orders.iter().map(|(_, o)| o.size.as_f64()).sum()
769 }
770
771 #[must_use]
773 pub fn size_decimal(&self) -> Decimal {
774 self.orders.iter().map(|(_, o)| o.size.as_decimal()).sum()
775 }
776
777 #[must_use]
779 pub fn exposure(&self) -> f64 {
780 self.orders
781 .iter()
782 .map(|(_, o)| o.price.as_f64() * o.size.as_f64())
783 .sum()
784 }
785
786 pub fn add_bulk(&mut self, orders: Vec<OwnBookOrder>) {
788 for order in orders {
789 self.add(order);
790 }
791 }
792
793 pub fn add(&mut self, order: OwnBookOrder) {
795 debug_assert_eq!(order.price, self.price.value);
796
797 self.orders.insert(order.client_order_id, order);
798 }
799
800 pub fn update(&mut self, order: OwnBookOrder) {
803 debug_assert_eq!(order.price, self.price.value);
804
805 self.orders[&order.client_order_id] = order;
806 }
807
808 pub fn delete(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
814 if self.orders.shift_remove(client_order_id).is_none() {
815 anyhow::bail!("Order {client_order_id} not found for delete");
817 };
818 Ok(())
819 }
820}
821
822impl PartialEq for OwnBookLevel {
823 fn eq(&self, other: &Self) -> bool {
824 self.price == other.price
825 }
826}
827
828impl Eq for OwnBookLevel {}
829
830impl PartialOrd for OwnBookLevel {
831 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
832 Some(self.cmp(other))
833 }
834}
835
836impl Ord for OwnBookLevel {
837 fn cmp(&self, other: &Self) -> Ordering {
838 self.price.cmp(&other.price)
839 }
840}
841
842pub fn should_handle_own_book_order(order: &OrderAny) -> bool {
843 order.has_price()
844 && order.time_in_force() != TimeInForce::Ioc
845 && order.time_in_force() != TimeInForce::Fok
846}