1use std::{
17 cell::RefCell,
18 cmp::min,
19 fmt::Debug,
20 ops::{Add, Sub},
21 rc::Rc,
22};
23
24use chrono::TimeDelta;
25use indexmap::{IndexMap, IndexSet};
26use nautilus_common::{
27 cache::Cache,
28 clock::Clock,
29 messages::execution::{
30 BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, ModifyOrder,
31 },
32 msgbus::{self, MessagingSwitchboard},
33};
34use nautilus_core::{UUID4, UnixNanos, correctness::CorrectnessResult};
35use nautilus_model::{
36 data::{
37 Bar, BarType, InstrumentClose, OrderBookDelta, OrderBookDeltas, OrderBookDepth10,
38 QuoteTick, TradeTick,
39 order::{BookOrder, OrderId},
40 },
41 enums::{
42 AccountType, AggregationSource, AggressorSide, BookAction, BookType, ContingencyType,
43 InstrumentCloseType, LiquiditySide, MarketStatus, MarketStatusAction, OmsType, OptionKind,
44 OrderSide, OrderSideSpecified, OrderStatus, OrderType, PositionSide, PriceType, RecordFlag,
45 TimeInForce, TriggerType,
46 },
47 events::{
48 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderEventAny, OrderExpired,
49 OrderFilled, OrderModifyRejected, OrderRejected, OrderSubmitted, OrderTriggered,
50 OrderUpdated,
51 },
52 identifiers::{
53 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId, Venue,
54 VenueOrderId,
55 },
56 instruments::{Instrument, InstrumentAny},
57 orderbook::OrderBook,
58 orders::{MarketOrder, Order, OrderAny, OrderCore},
59 position::Position,
60 types::{
61 Currency, Money, Price, Quantity, fixed::FIXED_PRECISION, price::PriceRaw,
62 quantity::QuantityRaw,
63 },
64};
65use rust_decimal::Decimal;
66use ustr::Ustr;
67
68use crate::{
69 matching_core::{MatchAction, OrderMatchingCore, RestingOrder},
70 matching_engine::{config::OrderMatchingEngineConfig, ids_generator::IdsGenerator},
71 models::{
72 fee::{FeeModel, FeeModelHandle},
73 fill::{FillModel, FillModelHandle},
74 },
75 protection::protection_price_calculate,
76 trailing::trailing_stop_calculate,
77};
78
79pub struct OrderMatchingEngine {
81 pub venue: Venue,
83 pub instrument: InstrumentAny,
85 pub raw_id: u32,
87 pub book_type: BookType,
89 pub oms_type: OmsType,
91 pub account_type: AccountType,
93 pub market_status: MarketStatus,
95 pub config: OrderMatchingEngineConfig,
97 core: OrderMatchingCore,
98 clock: Rc<RefCell<dyn Clock>>,
99 cache: Rc<RefCell<Cache>>,
100 book: OrderBook,
101 fill_model: FillModelHandle,
102 fee_model: FeeModelHandle,
103 event_handler: Option<Rc<dyn Fn(OrderEventAny)>>,
104 target_bid: Option<Price>,
105 target_ask: Option<Price>,
106 target_last: Option<Price>,
107 last_bar_bid: Option<Bar>,
108 last_bar_ask: Option<Bar>,
109 fill_at_market: bool,
110 execution_bar_types: IndexMap<InstrumentId, BarType>,
111 execution_bar_deltas: IndexMap<BarType, TimeDelta>,
112 account_ids: IndexMap<TraderId, AccountId>,
113 cached_filled_qty: IndexMap<ClientOrderId, Quantity>,
114 post_match_order_ids: IndexSet<ClientOrderId>,
115 ids_generator: IdsGenerator,
116 last_trade_size: Option<Quantity>,
117 trade_consumption: QuantityRaw,
118 bid_consumption: IndexMap<PriceRaw, (QuantityRaw, QuantityRaw)>,
119 ask_consumption: IndexMap<PriceRaw, (QuantityRaw, QuantityRaw)>,
120 queue_pending: IndexMap<ClientOrderId, PriceRaw>,
121 queue_ahead_orders: IndexMap<ClientOrderId, IndexMap<OrderId, QuantityRaw>>,
122 queue_ahead_total: IndexMap<ClientOrderId, (PriceRaw, QuantityRaw)>,
123 queue_excess: IndexMap<ClientOrderId, QuantityRaw>,
124 prev_bid_price_raw: PriceRaw,
125 prev_bid_size_raw: QuantityRaw,
126 prev_ask_price_raw: PriceRaw,
127 prev_ask_size_raw: QuantityRaw,
128 tob_initialized: bool,
129 last_quote_bid: Option<Price>,
130 last_quote_ask: Option<Price>,
131 precision_mismatch_streak: u32,
132 instrument_close: Option<InstrumentClose>,
133 pending_resolution: bool,
134 settlement_price: Option<Price>,
135 expiration_processed: bool,
136}
137
138impl Debug for OrderMatchingEngine {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct(stringify!(OrderMatchingEngine))
141 .field("venue", &self.venue)
142 .field("instrument", &self.instrument.id())
143 .finish()
144 }
145}
146
147impl OrderMatchingEngine {
148 #[expect(clippy::too_many_arguments)]
150 pub fn new(
151 instrument: InstrumentAny,
152 raw_id: u32,
153 fill_model: FillModelHandle,
154 fee_model: FeeModelHandle,
155 book_type: BookType,
156 oms_type: OmsType,
157 account_type: AccountType,
158 clock: Rc<RefCell<dyn Clock>>,
159 cache: Rc<RefCell<Cache>>,
160 config: OrderMatchingEngineConfig,
161 ) -> Self {
162 let book = OrderBook::new(instrument.id(), book_type);
163 let mut core = OrderMatchingCore::new(instrument.id(), instrument.price_increment());
164 core.set_fill_limit_inside_spread(Self::fill_limit_inside_spread_or_false(&fill_model));
165 let ids_generator = IdsGenerator::new(
166 instrument.id().venue,
167 oms_type,
168 raw_id,
169 config.use_random_ids,
170 config.use_position_ids,
171 cache.clone(),
172 );
173
174 Self {
175 venue: instrument.id().venue,
176 instrument,
177 raw_id,
178 fill_model,
179 fee_model,
180 event_handler: None,
181 book_type,
182 oms_type,
183 account_type,
184 clock,
185 cache,
186 book,
187 market_status: MarketStatus::Open,
188 config,
189 core,
190 target_bid: None,
191 target_ask: None,
192 target_last: None,
193 last_bar_bid: None,
194 last_bar_ask: None,
195 fill_at_market: true,
196 execution_bar_types: IndexMap::new(),
197 execution_bar_deltas: IndexMap::new(),
198 account_ids: IndexMap::new(),
199 cached_filled_qty: IndexMap::new(),
200 post_match_order_ids: IndexSet::new(),
201 ids_generator,
202 last_trade_size: None,
203 trade_consumption: 0,
204 bid_consumption: IndexMap::new(),
205 ask_consumption: IndexMap::new(),
206 queue_pending: IndexMap::new(),
207 queue_ahead_orders: IndexMap::new(),
208 queue_ahead_total: IndexMap::new(),
209 queue_excess: IndexMap::new(),
210 prev_bid_price_raw: 0,
211 prev_bid_size_raw: 0,
212 prev_ask_price_raw: 0,
213 prev_ask_size_raw: 0,
214 tob_initialized: false,
215 last_quote_bid: None,
216 last_quote_ask: None,
217 precision_mismatch_streak: 0,
218 instrument_close: None,
219 pending_resolution: false,
220 settlement_price: None,
221 expiration_processed: false,
222 }
223 }
224
225 pub fn set_event_handler(&mut self, handler: Rc<dyn Fn(OrderEventAny)>) {
232 self.event_handler = Some(handler);
233 }
234
235 fn dispatch_order_event(&self, event: OrderEventAny) {
236 if let Some(handler) = &self.event_handler {
237 handler(event);
238 } else {
239 let endpoint = MessagingSwitchboard::exec_engine_process();
240 msgbus::send_order_event(endpoint, event);
241 }
242 }
243
244 pub fn reset(&mut self) {
250 self.book.reset();
251 self.execution_bar_types.clear();
252 self.execution_bar_deltas.clear();
253 self.account_ids.clear();
254 self.cached_filled_qty.clear();
255 self.post_match_order_ids.clear();
256 self.core.reset();
257 self.target_bid = None;
258 self.target_ask = None;
259 self.target_last = None;
260 self.last_trade_size = None;
261 self.trade_consumption = 0;
262 self.bid_consumption.clear();
263 self.ask_consumption.clear();
264 self.queue_pending.clear();
265 self.queue_ahead_orders.clear();
266 self.queue_ahead_total.clear();
267 self.queue_excess.clear();
268 self.prev_bid_price_raw = 0;
269 self.prev_bid_size_raw = 0;
270 self.prev_ask_price_raw = 0;
271 self.prev_ask_size_raw = 0;
272 self.tob_initialized = false;
273 self.last_quote_bid = None;
274 self.last_quote_ask = None;
275 self.last_bar_bid = None;
276 self.last_bar_ask = None;
277 self.precision_mismatch_streak = 0;
278 self.instrument_close = None;
279 self.pending_resolution = false;
280 self.settlement_price = None;
281 self.expiration_processed = false;
282 self.fill_at_market = true;
283 self.ids_generator.reset();
284
285 log::info!("Reset {}", self.instrument.id());
286 }
287
288 fn apply_liquidity_consumption(
289 &mut self,
290 fills: Vec<(Price, Quantity)>,
291 order_side: OrderSide,
292 leaves_qty: Quantity,
293 book_prices: Option<&[Price]>,
294 ) -> Vec<(Price, Quantity)> {
295 if !self.config.liquidity_consumption {
296 return fills;
297 }
298
299 let consumption = match order_side {
300 OrderSide::Buy => &mut self.ask_consumption,
301 OrderSide::Sell => &mut self.bid_consumption,
302 _ => return fills,
303 };
304
305 let mut adjusted_fills = Vec::with_capacity(fills.len());
306 let mut remaining_qty = leaves_qty.raw;
307
308 for (fill_idx, (price, qty)) in fills.into_iter().enumerate() {
309 if remaining_qty == 0 {
310 break;
311 }
312
313 let book_price = book_prices
316 .and_then(|bp| bp.get(fill_idx).copied())
317 .unwrap_or(price);
318
319 let book_price_raw = book_price.raw;
320 let level_size = self
321 .book
322 .get_quantity_at_level(book_price, order_side, qty.precision);
323
324 let (original_size, consumed) = consumption
325 .entry(book_price_raw)
326 .or_insert((level_size.raw, 0));
327
328 if *original_size != level_size.raw {
330 *original_size = level_size.raw;
331 *consumed = 0;
332 }
333
334 let available = original_size.saturating_sub(*consumed);
335 if available == 0 {
336 continue;
337 }
338
339 let adjusted_qty_raw = min(min(qty.raw, available), remaining_qty);
340 if adjusted_qty_raw == 0 {
341 continue;
342 }
343
344 *consumed += adjusted_qty_raw;
345 remaining_qty -= adjusted_qty_raw;
346
347 let adjusted_qty = Quantity::from_raw(adjusted_qty_raw, qty.precision);
348 adjusted_fills.push((price, adjusted_qty));
349 }
350
351 adjusted_fills
352 }
353
354 fn seed_trade_consumption(
355 &mut self,
356 trade_price_raw: PriceRaw,
357 trade_size_raw: QuantityRaw,
358 trade_ts_event: UnixNanos,
359 aggressor_side: AggressorSide,
360 ) {
361 if trade_size_raw == 0 {
362 return;
363 }
364
365 if self.book.ts_last > trade_ts_event {
368 return;
369 }
370
371 let consumption = match aggressor_side {
372 AggressorSide::Buyer => &mut self.ask_consumption,
373 AggressorSide::Seller => &mut self.bid_consumption,
374 AggressorSide::NoAggressor => return,
375 };
376
377 let levels: Vec<_> = match aggressor_side {
378 AggressorSide::Buyer => self
379 .book
380 .asks(None)
381 .take_while(|l| l.price.value.raw <= trade_price_raw)
382 .collect(),
383 AggressorSide::Seller => self
384 .book
385 .bids(None)
386 .take_while(|l| l.price.value.raw >= trade_price_raw)
387 .collect(),
388 _ => unreachable!(),
389 };
390
391 let mut remaining = trade_size_raw;
392 for level in &levels {
393 if remaining == 0 {
394 break;
395 }
396 let level_size = level.size_raw();
397 let entry = consumption
398 .entry(level.price.value.raw)
399 .or_insert((level_size, 0));
400
401 if entry.0 != level_size {
403 entry.0 = level_size;
404 entry.1 = 0;
405 }
406
407 let available = level_size.saturating_sub(entry.1);
408 let consume = min(remaining, available);
409 entry.1 += consume;
410 remaining -= consume;
411 }
412 }
413
414 pub fn set_fill_model(&mut self, fill_model: FillModelHandle) {
416 self.core
417 .set_fill_limit_inside_spread(Self::fill_limit_inside_spread_or_false(&fill_model));
418 self.fill_model = fill_model;
419 }
420
421 fn fill_limit_inside_spread_or_false(fill_model: &FillModelHandle) -> bool {
422 fill_model.fill_limit_inside_spread().unwrap_or_else(|e| {
423 log::error!("Failed to query fill model spread behavior: {e}");
424 false
425 })
426 }
427
428 pub fn set_settlement_price(&mut self, price: Price) {
429 self.settlement_price = Some(price);
430 }
431
432 fn snapshot_queue_position(&mut self, order: &OrderAny, price: Price) {
433 if !self.config.queue_position {
434 return;
435 }
436 let size_prec = self.instrument.size_precision();
437
438 let qty_ahead = self.book.get_quantity_at_level(
441 price,
442 OrderCore::opposite_side(order.order_side()),
443 size_prec,
444 );
445
446 let client_order_id = order.client_order_id();
447
448 self.queue_pending.shift_remove(&client_order_id);
450 self.queue_ahead_total.shift_remove(&client_order_id);
451 self.queue_ahead_orders.shift_remove(&client_order_id);
452
453 if self.book_type == BookType::L1_MBP && qty_ahead.raw == 0 {
458 let behind_bbo = match order.order_side() {
459 OrderSide::Buy => self.book.best_bid_price().is_some_and(|bid| price < bid),
460 OrderSide::Sell => self.book.best_ask_price().is_some_and(|ask| price > ask),
461 _ => false,
462 };
463
464 if behind_bbo {
465 self.queue_pending.insert(client_order_id, price.raw);
466 return;
467 }
468 }
469
470 self.queue_ahead_total
471 .insert(client_order_id, (price.raw, qty_ahead.raw));
472
473 if self.book_type == BookType::L3_MBO {
475 let orders_ahead: IndexMap<OrderId, QuantityRaw> = self
476 .book
477 .get_orders_at_level(price, OrderCore::opposite_side(order.order_side()))
478 .iter()
479 .map(|book_order| (book_order.order_id, book_order.size.raw))
480 .collect();
481 self.queue_ahead_orders
482 .insert(client_order_id, orders_ahead);
483 }
484 }
485
486 fn decrement_queue_on_trade(
487 &mut self,
488 price_raw: PriceRaw,
489 trade_size_raw: QuantityRaw,
490 aggressor_side: AggressorSide,
491 ) {
492 if !self.config.queue_position {
493 return;
494 }
495
496 self.queue_excess.clear();
497
498 let keys: Vec<ClientOrderId> = self.queue_ahead_total.keys().copied().collect();
499 let mut entries: Vec<(ClientOrderId, QuantityRaw, QuantityRaw)> = Vec::new();
500 let mut stale: Vec<ClientOrderId> = Vec::new();
501
502 for client_order_id in keys {
503 let (order_price_raw, ahead_raw) =
504 match self.queue_ahead_total.get(&client_order_id).copied() {
505 Some(v) => v,
506 None => continue,
507 };
508
509 let cache = self.cache.borrow();
510 let order_info = cache.order(&client_order_id).and_then(|order| {
511 if order.is_closed() {
512 None
513 } else {
514 Some((order.order_side(), order.leaves_qty().raw))
515 }
516 });
517 drop(cache);
518
519 let Some((order_side, leaves_raw)) = order_info else {
520 stale.push(client_order_id);
521 continue;
522 };
523
524 if order_price_raw != price_raw || ahead_raw == 0 {
525 continue;
526 }
527
528 let should_decrement = matches!(aggressor_side, AggressorSide::NoAggressor)
529 || (aggressor_side == AggressorSide::Buyer && order_side == OrderSide::Sell)
530 || (aggressor_side == AggressorSide::Seller && order_side == OrderSide::Buy);
531
532 if should_decrement {
533 entries.push((client_order_id, ahead_raw, leaves_raw));
534 }
535 }
536
537 for id in stale {
538 self.queue_ahead_total.shift_remove(&id);
539 self.queue_ahead_orders.shift_remove(&id);
540 }
541
542 entries.sort_by_key(|&(_, ahead, _)| ahead);
544
545 let mut remaining = trade_size_raw;
546 let mut prev_position: QuantityRaw = 0;
547
548 for (client_order_id, ahead_raw, leaves_raw) in &entries {
549 if remaining == 0 {
550 let new_ahead = ahead_raw.saturating_sub(trade_size_raw);
551 self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, new_ahead);
552 if new_ahead == 0 {
553 self.queue_excess.insert(*client_order_id, 0);
555 }
556 continue;
557 }
558
559 let gap = ahead_raw.saturating_sub(prev_position);
561 let queue_consumed = remaining.min(gap);
562 remaining -= queue_consumed;
563
564 if remaining == 0 && queue_consumed < gap {
565 let new_ahead = ahead_raw.saturating_sub(trade_size_raw);
566 self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, new_ahead);
567 continue;
568 }
569
570 self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, 0);
571 let excess = remaining.min(*leaves_raw);
572 self.queue_excess.insert(*client_order_id, excess);
573 remaining -= excess;
574 prev_position = ahead_raw + excess;
575 }
576 }
577
578 fn reduce_queue_ahead(
582 &mut self,
583 client_order_id: ClientOrderId,
584 price_raw: PriceRaw,
585 ahead_raw: QuantityRaw,
586 new_ahead_raw: QuantityRaw,
587 ) {
588 self.queue_ahead_total
589 .insert(client_order_id, (price_raw, new_ahead_raw));
590 self.consume_queue_ahead_orders(client_order_id, ahead_raw.saturating_sub(new_ahead_raw));
591 }
592
593 fn consume_queue_ahead_orders(
595 &mut self,
596 client_order_id: ClientOrderId,
597 mut amount_raw: QuantityRaw,
598 ) {
599 let Some(orders_ahead) = self.queue_ahead_orders.get_mut(&client_order_id) else {
600 return;
601 };
602
603 while amount_raw > 0 {
604 let Some((&book_order_id, &size_raw)) = orders_ahead.get_index(0) else {
605 break;
606 };
607
608 if size_raw <= amount_raw {
609 orders_ahead.shift_remove(&book_order_id);
610 amount_raw -= size_raw;
611 } else {
612 orders_ahead.insert(book_order_id, size_raw - amount_raw);
613 amount_raw = 0;
614 }
615 }
616 }
617
618 fn determine_trade_fill_qty(&self, order: &OrderAny) -> Option<QuantityRaw> {
619 if !self.config.queue_position {
620 return Some(order.leaves_qty().raw);
621 }
622
623 let client_order_id = order.client_order_id();
624
625 if self.queue_pending.contains_key(&client_order_id) {
627 return None;
628 }
629
630 if let Some(&(tracked_price_raw, ahead_raw)) = self.queue_ahead_total.get(&client_order_id)
631 && let Some(order_price) = order.price()
632 && order_price.raw == tracked_price_raw
633 && ahead_raw > 0
634 {
635 return None;
636 }
637
638 let leaves_raw = order.leaves_qty().raw;
639 if leaves_raw == 0 {
640 return None;
641 }
642
643 let mut available_raw = leaves_raw;
644
645 if let Some(trade_size) = self.last_trade_size {
647 let remaining = trade_size.raw.saturating_sub(self.trade_consumption);
648 available_raw = available_raw.min(remaining);
649
650 if let Some(&excess_raw) = self.queue_excess.get(&client_order_id) {
651 if excess_raw == 0 {
652 return None;
653 }
654 available_raw = available_raw.min(excess_raw);
655 }
656 }
657
658 if available_raw == 0 {
659 return None;
660 }
661
662 Some(available_raw)
663 }
664
665 fn clear_all_queue_positions(&mut self) {
666 for (_, (_, ahead_raw)) in &mut self.queue_ahead_total {
667 *ahead_raw = 0;
668 }
669
670 for orders_ahead in self.queue_ahead_orders.values_mut() {
671 orders_ahead.clear();
672 }
673 }
674
675 fn adjust_queue_for_delta(&mut self, delta: &OrderBookDelta) {
676 if delta.action == BookAction::Delete {
677 if self.is_order_granular_delta(delta.flags) {
678 self.advance_l3_queue_on_delete(delta.order.order_id);
679 } else {
680 self.clear_queue_on_delete(delta.order.price.raw, delta.order.side);
681 }
682 } else if delta.action == BookAction::Update {
683 if self.is_order_granular_delta(delta.flags) {
684 self.adjust_l3_queue_on_update(&delta.order);
685 } else {
686 self.cap_queue_ahead(
687 delta.order.price.raw,
688 delta.order.size.raw,
689 delta.order.side,
690 );
691 }
692 }
693 }
694
695 fn clear_queue_on_delete(&mut self, deleted_price_raw: PriceRaw, deleted_side: OrderSide) {
696 let keys: Vec<ClientOrderId> = self.queue_ahead_total.keys().copied().collect();
697 for client_order_id in keys {
698 if let Some(&(order_price_raw, ahead_raw)) =
699 self.queue_ahead_total.get(&client_order_id)
700 && order_price_raw == deleted_price_raw
701 {
702 let matches_side = self
703 .cache
704 .borrow()
705 .order(&client_order_id)
706 .is_some_and(|o| o.order_side() == deleted_side);
707
708 if matches_side {
709 self.reduce_queue_ahead(client_order_id, order_price_raw, ahead_raw, 0);
710 }
711 }
712 }
713 }
714
715 fn is_order_granular_delta(&self, flags: u8) -> bool {
718 self.book_type == BookType::L3_MBO
719 && !RecordFlag::F_TOB.matches(flags)
720 && !RecordFlag::F_MBP.matches(flags)
721 }
722
723 fn advance_l3_queue_on_delete(&mut self, book_order_id: OrderId) {
724 for (client_order_id, orders_ahead) in &mut self.queue_ahead_orders {
725 let Some(size_raw) = orders_ahead.shift_remove(&book_order_id) else {
726 continue;
727 };
728
729 if let Some((_, ahead_raw)) = self.queue_ahead_total.get_mut(client_order_id) {
730 *ahead_raw = ahead_raw.saturating_sub(size_raw);
731 }
732 }
733 }
734
735 fn adjust_l3_queue_on_update(&mut self, book_order: &BookOrder) {
741 for (client_order_id, orders_ahead) in &mut self.queue_ahead_orders {
742 let Some(&tracked_size_raw) = orders_ahead.get(&book_order.order_id) else {
743 continue;
744 };
745 let Some((tracked_price_raw, ahead_raw)) =
746 self.queue_ahead_total.get_mut(client_order_id)
747 else {
748 continue;
749 };
750
751 if book_order.price.raw != *tracked_price_raw {
752 *ahead_raw = ahead_raw.saturating_sub(tracked_size_raw);
753 orders_ahead.shift_remove(&book_order.order_id);
754 } else if book_order.size.raw < tracked_size_raw {
755 *ahead_raw = ahead_raw.saturating_sub(tracked_size_raw - book_order.size.raw);
757 orders_ahead.insert(book_order.order_id, book_order.size.raw);
758 } else if book_order.size.raw > tracked_size_raw {
759 *ahead_raw = ahead_raw.saturating_add(book_order.size.raw - tracked_size_raw);
760 orders_ahead.insert(book_order.order_id, book_order.size.raw);
761 }
762 }
763 }
764
765 fn cap_queue_ahead(
766 &mut self,
767 price_raw: PriceRaw,
768 size_raw: QuantityRaw,
769 order_side: OrderSide,
770 ) {
771 let keys: Vec<ClientOrderId> = self.queue_ahead_total.keys().copied().collect();
772 let mut stale: Vec<ClientOrderId> = Vec::new();
773
774 for client_order_id in keys {
775 let (order_price_raw, ahead_raw) =
776 match self.queue_ahead_total.get(&client_order_id).copied() {
777 Some(v) => v,
778 None => continue,
779 };
780
781 if order_price_raw != price_raw || ahead_raw <= size_raw {
782 continue;
783 }
784
785 let cache = self.cache.borrow();
786 let order_info = cache.order(&client_order_id).and_then(|order| {
787 if order.is_closed() {
788 None
789 } else {
790 Some(order.order_side())
791 }
792 });
793 drop(cache);
794
795 let Some(side) = order_info else {
796 stale.push(client_order_id);
797 continue;
798 };
799
800 if side != order_side {
801 continue;
802 }
803
804 self.reduce_queue_ahead(client_order_id, order_price_raw, ahead_raw, size_raw);
805 }
806
807 for id in stale {
808 self.queue_ahead_total.shift_remove(&id);
809 self.queue_ahead_orders.shift_remove(&id);
810 }
811 }
812
813 fn seed_tob_baseline(&mut self) {
814 let bid = self.book.best_bid_price();
815 let ask = self.book.best_ask_price();
816 self.prev_bid_price_raw = bid.map_or(0, |p| p.raw);
817 self.prev_bid_size_raw = self.book.best_bid_size().map_or(0, |q| q.raw);
818 self.prev_ask_price_raw = ask.map_or(0, |p| p.raw);
819 self.prev_ask_size_raw = self.book.best_ask_size().map_or(0, |q| q.raw);
820 self.tob_initialized = bid.is_some() || ask.is_some();
821 }
822
823 fn decrement_l1_queue_on_quote(
824 &mut self,
825 bid_price_raw: PriceRaw,
826 bid_size_raw: QuantityRaw,
827 ask_price_raw: PriceRaw,
828 ask_size_raw: QuantityRaw,
829 ) {
830 if !self.config.queue_position {
831 return;
832 }
833
834 if self.tob_initialized {
836 if bid_price_raw < self.prev_bid_price_raw {
838 self.adjust_l1_queue_on_price_move(bid_price_raw, bid_size_raw, OrderSide::Buy);
839 }
840
841 if ask_price_raw > self.prev_ask_price_raw {
843 self.adjust_l1_queue_on_price_move(ask_price_raw, ask_size_raw, OrderSide::Sell);
844 }
845 }
846
847 self.resolve_pending_l1_snapshots(bid_price_raw, bid_size_raw, ask_price_raw, ask_size_raw);
849 }
850
851 fn adjust_l1_queue_on_price_move(
852 &mut self,
853 new_price_raw: PriceRaw,
854 new_size_raw: QuantityRaw,
855 order_side: OrderSide,
856 ) {
857 let keys: Vec<ClientOrderId> = self.queue_ahead_total.keys().copied().collect();
858 let mut stale: Vec<ClientOrderId> = Vec::new();
859
860 for client_order_id in keys {
861 let Some(&(order_price_raw, ahead_raw)) = self.queue_ahead_total.get(&client_order_id)
862 else {
863 continue;
864 };
865
866 let cache = self.cache.borrow();
867 let order_info = cache.order(&client_order_id).and_then(|order| {
868 if order.is_closed() {
869 None
870 } else {
871 Some(order.order_side())
872 }
873 });
874 drop(cache);
875
876 let Some(side) = order_info else {
877 stale.push(client_order_id);
878 continue;
879 };
880
881 if side != order_side {
882 continue;
883 }
884
885 let crossed = match order_side {
888 OrderSide::Buy => order_price_raw > new_price_raw,
889 _ => order_price_raw < new_price_raw,
890 };
891
892 if crossed {
893 self.queue_ahead_total
894 .insert(client_order_id, (order_price_raw, 0));
895 } else if order_price_raw == new_price_raw && ahead_raw > new_size_raw {
896 self.queue_ahead_total
897 .insert(client_order_id, (order_price_raw, new_size_raw));
898 }
899 }
900
901 for id in stale {
902 self.queue_ahead_total.shift_remove(&id);
903 }
904
905 let pending_keys: Vec<ClientOrderId> = self.queue_pending.keys().copied().collect();
907 let mut pending_stale: Vec<ClientOrderId> = Vec::new();
908
909 for client_order_id in pending_keys {
910 let Some(&order_price_raw) = self.queue_pending.get(&client_order_id) else {
911 continue;
912 };
913
914 let cache = self.cache.borrow();
915 let order_info = cache.order(&client_order_id).and_then(|order| {
916 if order.is_closed() {
917 None
918 } else {
919 Some(order.order_side())
920 }
921 });
922 drop(cache);
923
924 let Some(side) = order_info else {
925 pending_stale.push(client_order_id);
926 continue;
927 };
928
929 if side != order_side {
930 continue;
931 }
932
933 let crossed = match order_side {
934 OrderSide::Buy => order_price_raw > new_price_raw,
935 _ => order_price_raw < new_price_raw,
936 };
937
938 if crossed {
939 self.queue_pending.shift_remove(&client_order_id);
940 self.queue_ahead_total
941 .insert(client_order_id, (order_price_raw, 0));
942 } else if order_price_raw == new_price_raw {
943 self.queue_pending.shift_remove(&client_order_id);
944 self.queue_ahead_total
945 .insert(client_order_id, (order_price_raw, new_size_raw));
946 }
947 }
948
949 for id in pending_stale {
950 self.queue_pending.shift_remove(&id);
951 }
952 }
953
954 fn resolve_pending_l1_snapshots(
955 &mut self,
956 bid_price_raw: PriceRaw,
957 bid_size_raw: QuantityRaw,
958 ask_price_raw: PriceRaw,
959 ask_size_raw: QuantityRaw,
960 ) {
961 let keys: Vec<ClientOrderId> = self.queue_pending.keys().copied().collect();
962 let mut stale: Vec<ClientOrderId> = Vec::new();
963
964 for client_order_id in keys {
965 let Some(&order_price_raw) = self.queue_pending.get(&client_order_id) else {
966 continue;
967 };
968
969 let cache = self.cache.borrow();
970 let order_info = cache.order(&client_order_id).and_then(|order| {
971 if order.is_closed() {
972 None
973 } else {
974 Some(order.order_side())
975 }
976 });
977 drop(cache);
978
979 let Some(side) = order_info else {
980 stale.push(client_order_id);
981 continue;
982 };
983
984 let matched_size = match side {
986 OrderSide::Buy if order_price_raw == bid_price_raw => Some(bid_size_raw),
987 OrderSide::Sell if order_price_raw == ask_price_raw => Some(ask_size_raw),
988 _ => None,
989 };
990
991 if let Some(size) = matched_size {
992 self.queue_pending.shift_remove(&client_order_id);
993 self.queue_ahead_total
994 .insert(client_order_id, (order_price_raw, size));
995 }
996 }
997
998 for id in stale {
999 self.queue_pending.shift_remove(&id);
1000 }
1001 }
1002
1003 fn resolve_pending_on_trade(&mut self, trade_price_raw: PriceRaw) {
1004 let keys: Vec<ClientOrderId> = self.queue_pending.keys().copied().collect();
1005 let mut stale: Vec<ClientOrderId> = Vec::new();
1006
1007 for client_order_id in keys {
1008 let Some(&order_price_raw) = self.queue_pending.get(&client_order_id) else {
1009 continue;
1010 };
1011
1012 let cache = self.cache.borrow();
1013 let order_side = cache.order(&client_order_id).and_then(|order| {
1014 if order.is_closed() {
1015 None
1016 } else {
1017 Some(order.order_side())
1018 }
1019 });
1020 drop(cache);
1021
1022 let Some(side) = order_side else {
1023 stale.push(client_order_id);
1024 continue;
1025 };
1026
1027 let crossed = match side {
1029 OrderSide::Buy => trade_price_raw < order_price_raw,
1030 OrderSide::Sell => trade_price_raw > order_price_raw,
1031 _ => false,
1032 };
1033
1034 if crossed {
1035 self.queue_pending.shift_remove(&client_order_id);
1036 self.queue_ahead_total
1037 .insert(client_order_id, (order_price_raw, 0));
1038 }
1039 }
1040
1041 for id in stale {
1042 self.queue_pending.shift_remove(&id);
1043 }
1044 }
1045
1046 #[must_use]
1047 pub fn best_bid_price(&self) -> Option<Price> {
1049 self.book.best_bid_price()
1050 }
1051
1052 #[must_use]
1053 pub fn best_ask_price(&self) -> Option<Price> {
1055 self.book.best_ask_price()
1056 }
1057
1058 #[must_use]
1059 pub const fn get_book(&self) -> &OrderBook {
1061 &self.book
1062 }
1063
1064 #[must_use]
1065 pub fn get_open_bid_orders(&self) -> Vec<RestingOrder> {
1067 self.core.get_orders_bid()
1068 }
1069
1070 #[must_use]
1071 pub fn get_open_ask_orders(&self) -> Vec<RestingOrder> {
1073 self.core.get_orders_ask()
1074 }
1075
1076 #[must_use]
1077 pub fn get_open_orders(&self) -> Vec<RestingOrder> {
1079 self.core.get_orders()
1080 }
1081
1082 #[must_use]
1083 pub fn order_exists(&self, client_order_id: ClientOrderId) -> bool {
1085 self.core.order_exists(client_order_id)
1086 }
1087
1088 #[must_use]
1089 pub fn cached_filled_qty_len(&self) -> usize {
1091 self.cached_filled_qty.len()
1092 }
1093
1094 #[must_use]
1095 pub const fn get_core(&self) -> &OrderMatchingCore {
1096 &self.core
1097 }
1098
1099 pub fn set_fill_at_market(&mut self, value: bool) {
1100 self.fill_at_market = value;
1101 }
1102
1103 pub fn update_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> {
1109 if instrument.id() != self.instrument.id() {
1110 anyhow::bail!(
1111 "Cannot update instrument {} with {}",
1112 self.instrument.id(),
1113 instrument.id()
1114 );
1115 }
1116
1117 let changed = instrument.price_increment() != self.instrument.price_increment()
1118 || instrument.price_precision() != self.instrument.price_precision()
1119 || instrument.size_precision() != self.instrument.size_precision();
1120
1121 if changed {
1122 self.core
1123 .update_price_increment(instrument.price_increment());
1124 self.book.reset();
1125 self.trade_consumption = 0;
1126 self.bid_consumption.clear();
1127 self.ask_consumption.clear();
1128 self.queue_pending.clear();
1129 self.queue_ahead_orders.clear();
1130 self.queue_ahead_total.clear();
1131 self.queue_excess.clear();
1132 self.prev_bid_price_raw = 0;
1133 self.prev_bid_size_raw = 0;
1134 self.prev_ask_price_raw = 0;
1135 self.prev_ask_size_raw = 0;
1136 self.tob_initialized = false;
1137 self.last_quote_bid = None;
1138 self.last_quote_ask = None;
1139 self.precision_mismatch_streak = 0;
1140 self.target_bid = None;
1141 self.target_ask = None;
1142 self.target_last = None;
1143 self.last_bar_bid = None;
1144 self.last_bar_ask = None;
1145 self.core.bid = None;
1146 self.core.ask = None;
1147 self.core.last = None;
1148 log::info!(
1149 "Updated instrument {} (price_precision={} size_precision={})",
1150 instrument.id(),
1151 instrument.price_precision(),
1152 instrument.size_precision()
1153 );
1154 }
1155
1156 self.instrument = instrument;
1157
1158 if changed {
1159 self.drop_incompatible_core_orders();
1160 }
1161 Ok(())
1162 }
1163
1164 fn check_price_precision(&self, actual: u8, field: &str) -> anyhow::Result<()> {
1165 let expected = self.instrument.price_precision();
1166 if actual != expected {
1167 anyhow::bail!(
1168 "Invalid {field} precision {actual}, expected {expected} for {}",
1169 self.instrument.id()
1170 );
1171 }
1172 Ok(())
1173 }
1174
1175 fn check_size_precision(&self, actual: u8, field: &str) -> anyhow::Result<()> {
1176 let expected = self.instrument.size_precision();
1177 if actual != expected {
1178 anyhow::bail!(
1179 "Invalid {field} precision {actual}, expected {expected} for {}",
1180 self.instrument.id()
1181 );
1182 }
1183 Ok(())
1184 }
1185
1186 fn log_precision_mismatch(
1187 &mut self,
1188 data_type: &str,
1189 instrument_id: InstrumentId,
1190 err: &anyhow::Error,
1191 ) {
1192 self.precision_mismatch_streak = self.precision_mismatch_streak.saturating_add(1);
1193 let streak = self.precision_mismatch_streak;
1194
1195 if streak <= 3 || streak.is_multiple_of(100) {
1196 log::warn!(
1197 "Skipping {data_type} for {instrument_id}: {err} \
1198 (consecutive_precision_mismatches={streak})"
1199 );
1200 }
1201
1202 if streak == 20 {
1203 log::error!(
1204 "Precision mismatches reached {streak} consecutive events for \
1205 {instrument_id}; check instrument update flow and upstream market data"
1206 );
1207 }
1208 }
1209
1210 fn drop_incompatible_core_orders(&mut self) {
1211 let client_order_ids: Vec<ClientOrderId> = self
1212 .core
1213 .iter_orders()
1214 .filter(|order| {
1215 !self.resting_order_matches_current_instrument(order)
1216 || !self.cached_order_matches_current_instrument(order.client_order_id)
1217 })
1218 .map(|order| order.client_order_id)
1219 .collect();
1220
1221 for client_order_id in client_order_ids {
1222 let order = self
1223 .cache
1224 .borrow()
1225 .order(&client_order_id)
1226 .map(|o| o.clone());
1227
1228 if let Some(order) = order
1229 && (order.is_inflight() || order.is_open())
1230 {
1231 log::warn!(
1232 "Canceling order {client_order_id} after instrument update: \
1233 price, trigger price, or quantity is not compatible with {}",
1234 self.instrument.id()
1235 );
1236 self.cancel_order(&order, None);
1237 } else {
1238 self.delete_core_order(client_order_id);
1239 self.cached_filled_qty.swap_remove(&client_order_id);
1240 }
1241 }
1242 }
1243
1244 fn cached_order_matches_current_instrument(&self, client_order_id: ClientOrderId) -> bool {
1245 self.cache
1246 .borrow()
1247 .order(&client_order_id)
1248 .is_none_or(|order| {
1249 Self::quantity_matches_precision(order.quantity(), self.instrument.size_precision())
1250 })
1251 }
1252
1253 fn resting_order_matches_current_instrument(&self, order: &RestingOrder) -> bool {
1254 order
1255 .limit_price
1256 .is_none_or(|price| self.price_matches_current_instrument(price))
1257 && order
1258 .trigger_price
1259 .is_none_or(|price| self.price_matches_current_instrument(price))
1260 }
1261
1262 fn price_matches_current_instrument(&self, price: Price) -> bool {
1263 Self::price_matches_precision(price, self.instrument.price_precision())
1264 && Self::price_matches_tick(price, self.instrument.price_increment())
1265 }
1266
1267 fn price_matches_precision(price: Price, precision: u8) -> bool {
1268 let precision_diff = FIXED_PRECISION.saturating_sub(precision);
1269 let scale = PriceRaw::pow(10, u32::from(precision_diff));
1270 price.raw % scale == 0
1271 }
1272
1273 fn price_matches_tick(price: Price, increment: Price) -> bool {
1274 let increment_raw = increment.raw.abs();
1275 increment_raw == 0 || price.raw % increment_raw == 0
1276 }
1277
1278 fn quantity_matches_precision(quantity: Quantity, precision: u8) -> bool {
1279 let precision_diff = FIXED_PRECISION.saturating_sub(precision);
1280 let scale = QuantityRaw::pow(10, u32::from(precision_diff));
1281 quantity.raw.is_multiple_of(scale)
1282 }
1283
1284 fn normalize_price_for_current_instrument(&self, price: Price) -> Option<Price> {
1285 if !self.price_matches_current_instrument(price) {
1286 return None;
1287 }
1288
1289 Some(Price::from_raw(
1290 price.raw,
1291 self.instrument.price_precision(),
1292 ))
1293 }
1294
1295 fn normalize_quantity_for_current_instrument(&self, quantity: Quantity) -> Option<Quantity> {
1296 let precision = self.instrument.size_precision();
1297 if !Self::quantity_matches_precision(quantity, precision) {
1298 return None;
1299 }
1300
1301 Some(Quantity::from_raw(quantity.raw, precision))
1302 }
1303
1304 pub fn process_order_book_delta(&mut self, delta: &OrderBookDelta) -> anyhow::Result<()> {
1312 log::debug!("Processing {delta}");
1313
1314 if matches!(delta.action, BookAction::Add | BookAction::Update) {
1316 self.check_price_precision(delta.order.price.precision, "delta order price")?;
1317 self.check_size_precision(delta.order.size.precision, "delta order size")?;
1318 }
1319
1320 if self.book_type == BookType::L1_MBP {
1322 self.iterate(delta.ts_init, AggressorSide::NoAggressor);
1323 return Ok(());
1324 }
1325
1326 self.book.apply_delta(delta)?;
1327
1328 let delta_snapshot_or_clear = (delta.flags & 32) != 0 || delta.action == BookAction::Clear;
1329
1330 if self.config.queue_position {
1331 if delta_snapshot_or_clear {
1332 self.clear_all_queue_positions();
1333 } else {
1334 self.adjust_queue_for_delta(delta);
1335 }
1336 }
1337
1338 if self.config.queue_position && delta_snapshot_or_clear {
1339 self.seed_tob_baseline();
1340 }
1341
1342 self.iterate(delta.ts_init, AggressorSide::NoAggressor);
1343 Ok(())
1344 }
1345
1346 pub fn process_order_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
1354 log::debug!("Processing {deltas}");
1355
1356 for delta in &deltas.deltas {
1358 if matches!(delta.action, BookAction::Add | BookAction::Update) {
1359 self.check_price_precision(delta.order.price.precision, "delta order price")?;
1360 self.check_size_precision(delta.order.size.precision, "delta order size")?;
1361 }
1362 }
1363
1364 if self.book_type == BookType::L1_MBP {
1366 self.iterate(deltas.ts_init, AggressorSide::NoAggressor);
1367 return Ok(());
1368 }
1369
1370 self.book.apply_deltas(deltas)?;
1371
1372 let mut has_snapshot_or_clear = false;
1373
1374 if self.config.queue_position {
1375 for delta in &deltas.deltas {
1376 if (delta.flags & 32) != 0 || delta.action == BookAction::Clear {
1377 self.clear_all_queue_positions();
1378 has_snapshot_or_clear = true;
1379 break;
1380 }
1381 self.adjust_queue_for_delta(delta);
1382 }
1383 }
1384
1385 if self.config.queue_position && has_snapshot_or_clear {
1386 self.seed_tob_baseline();
1387 }
1388
1389 self.iterate(deltas.ts_init, AggressorSide::NoAggressor);
1390 Ok(())
1391 }
1392
1393 pub fn process_order_book_depth10(&mut self, depth: &OrderBookDepth10) -> anyhow::Result<()> {
1402 log::debug!("Processing OrderBookDepth10 for {}", depth.instrument_id);
1403
1404 for order in &depth.bids {
1406 if order.side == OrderSide::NoOrderSide || !order.size.is_positive() {
1407 continue;
1408 }
1409 self.check_price_precision(order.price.precision, "bid price")?;
1410 self.check_size_precision(order.size.precision, "bid size")?;
1411 }
1412
1413 for order in &depth.asks {
1414 if order.side == OrderSide::NoOrderSide || !order.size.is_positive() {
1415 continue;
1416 }
1417 self.check_price_precision(order.price.precision, "ask price")?;
1418 self.check_size_precision(order.size.precision, "ask size")?;
1419 }
1420
1421 let top_bid = Self::first_valid_depth_order(&depth.bids, OrderSide::Buy);
1422 let top_ask = Self::first_valid_depth_order(&depth.asks, OrderSide::Sell);
1423
1424 if self.book_type == BookType::L1_MBP {
1427 let quote = QuoteTick::new(
1428 depth.instrument_id,
1429 Self::depth_quote_price(top_bid, self.instrument.price_precision()),
1430 Self::depth_quote_price(top_ask, self.instrument.price_precision()),
1431 Self::depth_quote_size(top_bid, self.instrument.size_precision()),
1432 Self::depth_quote_size(top_ask, self.instrument.size_precision()),
1433 depth.ts_event,
1434 depth.ts_init,
1435 );
1436 self.book.update_quote_tick("e)?;
1437 self.last_quote_bid = top_bid.map(|order| order.price);
1438 self.last_quote_ask = top_ask.map(|order| order.price);
1439 } else {
1440 self.book.apply_depth(depth)?;
1441 }
1442
1443 if self.config.queue_position {
1445 self.clear_all_queue_positions();
1446 let bid_price_raw = top_bid.map_or(0, |order| order.price.raw);
1447 let bid_size_raw = top_bid.map_or(0, |order| order.size.raw);
1448 let ask_price_raw = top_ask.map_or(0, |order| order.price.raw);
1449 let ask_size_raw = top_ask.map_or(0, |order| order.size.raw);
1450
1451 if self.tob_initialized {
1453 if bid_price_raw < self.prev_bid_price_raw {
1454 self.adjust_l1_queue_on_price_move(bid_price_raw, bid_size_raw, OrderSide::Buy);
1455 }
1456
1457 if ask_price_raw > self.prev_ask_price_raw {
1458 self.adjust_l1_queue_on_price_move(
1459 ask_price_raw,
1460 ask_size_raw,
1461 OrderSide::Sell,
1462 );
1463 }
1464 }
1465
1466 self.resolve_pending_l1_snapshots(
1467 bid_price_raw,
1468 bid_size_raw,
1469 ask_price_raw,
1470 ask_size_raw,
1471 );
1472
1473 self.prev_bid_price_raw = bid_price_raw;
1474 self.prev_bid_size_raw = bid_size_raw;
1475 self.prev_ask_price_raw = ask_price_raw;
1476 self.prev_ask_size_raw = ask_size_raw;
1477 self.tob_initialized = true;
1478 }
1479
1480 self.iterate(depth.ts_init, AggressorSide::NoAggressor);
1481 Ok(())
1482 }
1483
1484 fn first_valid_depth_order(orders: &[BookOrder], side: OrderSide) -> Option<BookOrder> {
1485 orders
1486 .iter()
1487 .copied()
1488 .find(|order| order.side == side && order.size.is_positive())
1489 }
1490
1491 fn depth_quote_price(order: Option<BookOrder>, price_precision: u8) -> Price {
1492 order.map_or_else(|| Price::zero(price_precision), |order| order.price)
1493 }
1494
1495 fn depth_quote_size(order: Option<BookOrder>, size_precision: u8) -> Quantity {
1496 order.map_or_else(|| Quantity::zero(size_precision), |order| order.size)
1497 }
1498
1499 pub fn process_quote_tick(&mut self, quote: &QuoteTick) {
1501 log::debug!("Processing {quote}");
1502
1503 if let Err(e) = self.check_price_precision(quote.bid_price.precision, "bid_price") {
1504 self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1505 return;
1506 }
1507
1508 if let Err(e) = self.check_price_precision(quote.ask_price.precision, "ask_price") {
1509 self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1510 return;
1511 }
1512
1513 if let Err(e) = self.check_size_precision(quote.bid_size.precision, "bid_size") {
1514 self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1515 return;
1516 }
1517
1518 if let Err(e) = self.check_size_precision(quote.ask_size.precision, "ask_size") {
1519 self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1520 return;
1521 }
1522
1523 self.precision_mismatch_streak = 0;
1524
1525 if self.book_type == BookType::L1_MBP {
1526 if quote.ts_event < self.book.ts_last {
1528 log::warn!(
1529 "Skipping stale quote: ts_event {} < book.ts_last {} for {}",
1530 quote.ts_event,
1531 self.book.ts_last,
1532 self.book.instrument_id,
1533 );
1534 self.iterate(quote.ts_init, AggressorSide::NoAggressor);
1535 return;
1536 }
1537
1538 if !self.update_quote_tick_or_skip(quote, "quote tick") {
1539 return;
1540 }
1541
1542 if self.config.queue_position {
1543 self.decrement_l1_queue_on_quote(
1544 quote.bid_price.raw,
1545 quote.bid_size.raw,
1546 quote.ask_price.raw,
1547 quote.ask_size.raw,
1548 );
1549 self.prev_bid_price_raw = quote.bid_price.raw;
1550 self.prev_bid_size_raw = quote.bid_size.raw;
1551 self.prev_ask_price_raw = quote.ask_price.raw;
1552 self.prev_ask_size_raw = quote.ask_size.raw;
1553 self.tob_initialized = true;
1554 }
1555 self.last_quote_bid = Some(quote.bid_price);
1556 self.last_quote_ask = Some(quote.ask_price);
1557 }
1558
1559 self.iterate(quote.ts_init, AggressorSide::NoAggressor);
1560 }
1561
1562 pub fn process_bar(&mut self, bar: &Bar) {
1571 log::debug!("Processing {bar}");
1572
1573 debug_assert!(
1574 bar.high >= bar.open
1575 && bar.high >= bar.low
1576 && bar.high >= bar.close
1577 && bar.low <= bar.open
1578 && bar.low <= bar.close,
1579 "OHLC invariant violated for {bar}"
1580 );
1581
1582 if !self.config.bar_execution || self.book_type != BookType::L1_MBP {
1584 return;
1585 }
1586
1587 let bar_type = bar.bar_type;
1588 if bar_type.aggregation_source() == AggregationSource::Internal {
1590 return;
1591 }
1592
1593 if let Err(e) = self.check_price_precision(bar.open.precision, "bar open") {
1594 self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1595 return;
1596 }
1597
1598 if let Err(e) = self.check_price_precision(bar.high.precision, "bar high") {
1599 self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1600 return;
1601 }
1602
1603 if let Err(e) = self.check_price_precision(bar.low.precision, "bar low") {
1604 self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1605 return;
1606 }
1607
1608 if let Err(e) = self.check_price_precision(bar.close.precision, "bar close") {
1609 self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1610 return;
1611 }
1612
1613 if let Err(e) = self.check_size_precision(bar.volume.precision, "bar volume") {
1614 self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1615 return;
1616 }
1617
1618 self.precision_mismatch_streak = 0;
1619
1620 let price_type = bar_type.spec().price_type;
1621 if price_type == PriceType::Mark {
1622 log::warn!(
1623 "Cannot process bar for {} with `PriceType::Mark`, mark price bars are not supported for bar execution",
1624 bar.instrument_id(),
1625 );
1626 return;
1627 }
1628
1629 let execution_bar_type =
1630 if let Some(execution_bar_type) = self.execution_bar_types.get(&bar.instrument_id()) {
1631 execution_bar_type.to_owned()
1632 } else {
1633 self.execution_bar_types
1634 .insert(bar.instrument_id(), bar_type);
1635 self.execution_bar_deltas
1636 .insert(bar_type, bar_type.spec().timedelta());
1637 bar_type
1638 };
1639
1640 if execution_bar_type != bar_type {
1641 let mut bar_type_timedelta = self.execution_bar_deltas.get(&bar_type).copied();
1642 if bar_type_timedelta.is_none() {
1643 bar_type_timedelta = Some(bar_type.spec().timedelta());
1644 self.execution_bar_deltas
1645 .insert(bar_type, bar_type_timedelta.unwrap());
1646 }
1647
1648 if self.execution_bar_deltas.get(&execution_bar_type).unwrap()
1649 >= &bar_type_timedelta.unwrap()
1650 {
1651 self.execution_bar_types
1652 .insert(bar_type.instrument_id(), bar_type);
1653 } else {
1654 return;
1655 }
1656 }
1657
1658 match price_type {
1659 PriceType::Last | PriceType::Mid => self.process_trade_ticks_from_bar(bar),
1660 PriceType::Bid => {
1661 self.last_bar_bid = Some(bar.to_owned());
1662 self.process_quote_ticks_from_bar();
1663 }
1664 PriceType::Ask => {
1665 self.last_bar_ask = Some(bar.to_owned());
1666 self.process_quote_ticks_from_bar();
1667 }
1668 PriceType::Mark => {
1669 unreachable!("PriceType::Mark bars return before execution bar state updates")
1670 }
1671 }
1672 }
1673
1674 fn process_trade_ticks_from_bar(&mut self, bar: &Bar) {
1675 let sizes = BarTickSizes::from_volume(bar.volume, self.instrument.size_increment());
1676
1677 let aggressor_side = if self.core.last.is_none_or(|last| bar.open > last) {
1678 AggressorSide::Buyer
1679 } else {
1680 AggressorSide::Seller
1681 };
1682
1683 if self.core.last.is_none() {
1685 self.fill_at_market = true;
1686
1687 if !self.process_bar_trade_tick(
1688 bar,
1689 bar.open,
1690 sizes.open,
1691 aggressor_side,
1692 "bar open trade tick",
1693 ) {
1694 return;
1695 }
1696 self.core.set_last_raw(bar.open);
1697 } else if self.core.last.is_some_and(|last| bar.open != last) {
1698 self.fill_at_market = true;
1700
1701 if !self.process_bar_trade_tick(
1702 bar,
1703 bar.open,
1704 sizes.open,
1705 aggressor_side,
1706 "bar gap-open trade tick",
1707 ) {
1708 return;
1709 }
1710 self.core.set_last_raw(bar.open);
1711 }
1712
1713 let high_first = !self.config.bar_adaptive_high_low_ordering
1716 || (bar.high.raw - bar.open.raw).abs() < (bar.low.raw - bar.open.raw).abs();
1717
1718 if high_first {
1719 self.process_bar_high(bar, sizes.high);
1720 self.process_bar_low(bar, sizes.low);
1721 } else {
1722 self.process_bar_low(bar, sizes.low);
1723 self.process_bar_high(bar, sizes.high);
1724 }
1725
1726 if self.core.last.is_some_and(|last| bar.close != last) {
1728 self.fill_at_market = false;
1729
1730 let aggressor_side = if bar.close > self.core.last.unwrap() {
1731 AggressorSide::Buyer
1732 } else {
1733 AggressorSide::Seller
1734 };
1735
1736 if !self.process_bar_trade_tick(
1737 bar,
1738 bar.close,
1739 sizes.close,
1740 aggressor_side,
1741 "bar close trade tick",
1742 ) {
1743 return;
1744 }
1745
1746 self.core.set_last_raw(bar.close);
1747 }
1748
1749 self.fill_at_market = true;
1750 }
1751
1752 fn process_bar_high(&mut self, bar: &Bar, size: Quantity) {
1753 if self.core.last.is_some_and(|last| bar.high > last) {
1754 self.fill_at_market = false;
1755
1756 if !self.process_bar_trade_tick(
1757 bar,
1758 bar.high,
1759 size,
1760 AggressorSide::Buyer,
1761 "bar high trade tick",
1762 ) {
1763 return;
1764 }
1765
1766 self.core.set_last_raw(bar.high);
1767 }
1768 }
1769
1770 fn process_bar_low(&mut self, bar: &Bar, size: Quantity) {
1771 if self.core.last.is_some_and(|last| bar.low < last) {
1772 self.fill_at_market = false;
1773
1774 if !self.process_bar_trade_tick(
1775 bar,
1776 bar.low,
1777 size,
1778 AggressorSide::Seller,
1779 "bar low trade tick",
1780 ) {
1781 return;
1782 }
1783
1784 self.core.set_last_raw(bar.low);
1785 }
1786 }
1787
1788 fn process_bar_trade_tick(
1789 &mut self,
1790 bar: &Bar,
1791 price: Price,
1792 size: Quantity,
1793 aggressor_side: AggressorSide,
1794 context: &str,
1795 ) -> bool {
1796 if size.is_zero() {
1797 return true;
1798 }
1799
1800 let trade_tick = TradeTick::new(
1801 bar.instrument_id(),
1802 price,
1803 size,
1804 aggressor_side,
1805 self.ids_generator.generate_trade_id(bar.ts_init),
1806 bar.ts_init,
1807 bar.ts_init,
1808 );
1809
1810 if !self.update_trade_tick_or_skip(&trade_tick, context) {
1811 return false;
1812 }
1813
1814 self.iterate(trade_tick.ts_init, AggressorSide::NoAggressor);
1815 true
1816 }
1817
1818 fn process_quote_ticks_from_bar(&mut self) {
1819 if self.last_bar_bid.is_none()
1821 || self.last_bar_ask.is_none()
1822 || self.last_bar_bid.unwrap().ts_init != self.last_bar_ask.unwrap().ts_init
1823 {
1824 return;
1825 }
1826 let bid_bar = self.last_bar_bid.unwrap();
1827 let ask_bar = self.last_bar_ask.unwrap();
1828
1829 let size_increment = self.instrument.size_increment();
1830 let bid_sizes = BarTickSizes::from_volume(bid_bar.volume, size_increment);
1831 let ask_sizes = BarTickSizes::from_volume(ask_bar.volume, size_increment);
1832 let mut has_current_bid = false;
1833 let mut has_current_ask = false;
1834
1835 let mut quote_tick = QuoteTick::new(
1836 self.book.instrument_id,
1837 bid_bar.open,
1838 ask_bar.open,
1839 bid_sizes.open,
1840 ask_sizes.open,
1841 bid_bar.ts_init,
1842 bid_bar.ts_init,
1843 );
1844
1845 self.fill_at_market = true;
1847
1848 if !self.process_bar_quote_tick(
1849 "e_tick,
1850 "bar open quote tick",
1851 &mut has_current_bid,
1852 &mut has_current_ask,
1853 ) {
1854 return;
1855 }
1856
1857 let high_first = !self.config.bar_adaptive_high_low_ordering
1860 || (bid_bar.high.raw - bid_bar.open.raw).abs()
1861 < (bid_bar.low.raw - bid_bar.open.raw).abs();
1862
1863 let high_leg = (
1864 bid_bar.high,
1865 ask_bar.high,
1866 bid_sizes.high,
1867 ask_sizes.high,
1868 "bar high quote tick",
1869 );
1870 let low_leg = (
1871 bid_bar.low,
1872 ask_bar.low,
1873 bid_sizes.low,
1874 ask_sizes.low,
1875 "bar low quote tick",
1876 );
1877 let legs = if high_first {
1878 [high_leg, low_leg]
1879 } else {
1880 [low_leg, high_leg]
1881 };
1882
1883 for (bid_price, ask_price, bid_size, ask_size, context) in legs {
1885 self.fill_at_market = false;
1886 quote_tick.bid_price = bid_price;
1887 quote_tick.ask_price = ask_price;
1888 quote_tick.bid_size = bid_size;
1889 quote_tick.ask_size = ask_size;
1890
1891 if !self.process_bar_quote_tick(
1892 "e_tick,
1893 context,
1894 &mut has_current_bid,
1895 &mut has_current_ask,
1896 ) {
1897 return;
1898 }
1899 }
1900
1901 self.fill_at_market = false;
1903 quote_tick.bid_price = bid_bar.close;
1904 quote_tick.ask_price = ask_bar.close;
1905 quote_tick.bid_size = bid_sizes.close;
1906 quote_tick.ask_size = ask_sizes.close;
1907
1908 if !self.process_bar_quote_tick(
1909 "e_tick,
1910 "bar close quote tick",
1911 &mut has_current_bid,
1912 &mut has_current_ask,
1913 ) {
1914 return;
1915 }
1916
1917 self.last_bar_bid = None;
1918 self.last_bar_ask = None;
1919 self.fill_at_market = true;
1920 }
1921
1922 fn process_bar_quote_tick(
1923 &mut self,
1924 quote: &QuoteTick,
1925 context: &str,
1926 has_current_bid: &mut bool,
1927 has_current_ask: &mut bool,
1928 ) -> bool {
1929 let has_bid_size = !quote.bid_size.is_zero();
1930 let has_ask_size = !quote.ask_size.is_zero();
1931 let mut book_changed = false;
1932 let mut bid_cleared = false;
1933 let mut ask_cleared = false;
1934
1935 match (has_bid_size, has_ask_size) {
1936 (true, true) => {
1937 if !self.update_quote_tick_or_skip(quote, context) {
1938 return false;
1939 }
1940 *has_current_bid = true;
1941 *has_current_ask = true;
1942 book_changed = true;
1943 }
1944 _ => {
1945 if has_bid_size {
1946 self.update_bar_quote_bid(quote);
1947 *has_current_bid = true;
1948 book_changed = true;
1949 } else if !*has_current_bid {
1950 self.clear_bar_quote_bid(quote);
1951 *has_current_bid = true;
1952 book_changed = true;
1953 bid_cleared = true;
1954 }
1955
1956 if has_ask_size {
1957 self.update_bar_quote_ask(quote);
1958 *has_current_ask = true;
1959 book_changed = true;
1960 } else if !*has_current_ask {
1961 self.clear_bar_quote_ask(quote);
1962 *has_current_ask = true;
1963 book_changed = true;
1964 ask_cleared = true;
1965 }
1966 }
1967 }
1968
1969 if book_changed
1970 && let (Some(best_bid), Some(best_ask)) =
1971 (self.book.best_bid_price(), self.book.best_ask_price())
1972 && best_bid > best_ask
1973 {
1974 if has_bid_size && !has_ask_size {
1975 self.clear_bar_quote_ask(quote);
1976 ask_cleared = true;
1977 } else if has_ask_size && !has_bid_size {
1978 self.clear_bar_quote_bid(quote);
1979 bid_cleared = true;
1980 }
1981 }
1982
1983 if has_bid_size {
1984 self.last_quote_bid = Some(quote.bid_price);
1985 } else if bid_cleared {
1986 self.last_quote_bid = None;
1987 }
1988
1989 if has_ask_size {
1990 self.last_quote_ask = Some(quote.ask_price);
1991 } else if ask_cleared {
1992 self.last_quote_ask = None;
1993 }
1994
1995 if !book_changed {
1996 return true;
1997 }
1998
1999 self.iterate(quote.ts_init, AggressorSide::NoAggressor);
2000 true
2001 }
2002
2003 fn update_bar_quote_bid(&mut self, quote: &QuoteTick) {
2004 let bid = BookOrder::new(
2005 OrderSide::Buy,
2006 quote.bid_price,
2007 quote.bid_size,
2008 OrderSide::Buy as u64,
2009 );
2010 self.book
2011 .add(bid, 0, self.book.sequence.saturating_add(1), quote.ts_event);
2012 }
2013
2014 fn clear_bar_quote_bid(&mut self, quote: &QuoteTick) {
2015 self.book
2016 .clear_bids(self.book.sequence.saturating_add(1), quote.ts_event);
2017 }
2018
2019 fn update_bar_quote_ask(&mut self, quote: &QuoteTick) {
2020 let ask = BookOrder::new(
2021 OrderSide::Sell,
2022 quote.ask_price,
2023 quote.ask_size,
2024 OrderSide::Sell as u64,
2025 );
2026 self.book
2027 .add(ask, 0, self.book.sequence.saturating_add(1), quote.ts_event);
2028 }
2029
2030 fn clear_bar_quote_ask(&mut self, quote: &QuoteTick) {
2031 self.book
2032 .clear_asks(self.book.sequence.saturating_add(1), quote.ts_event);
2033 }
2034
2035 pub fn process_trade_tick(&mut self, trade: &TradeTick) {
2042 log::debug!("Processing {trade}");
2043
2044 if let Err(e) = self.check_price_precision(trade.price.precision, "trade price") {
2045 self.log_precision_mismatch("trade tick", trade.instrument_id, &e);
2046 return;
2047 }
2048
2049 if let Err(e) = self.check_size_precision(trade.size.precision, "trade size") {
2050 self.log_precision_mismatch("trade tick", trade.instrument_id, &e);
2051 return;
2052 }
2053
2054 self.precision_mismatch_streak = 0;
2055
2056 let price_raw = trade.price.raw;
2057
2058 if self.book_type == BookType::L1_MBP {
2059 if trade.ts_event < self.book.ts_last {
2061 log::warn!(
2062 "Skipping stale trade: ts_event {} < book.ts_last {} for {}",
2063 trade.ts_event,
2064 self.book.ts_last,
2065 self.book.instrument_id,
2066 );
2067 self.iterate(trade.ts_init, AggressorSide::NoAggressor);
2068 return;
2069 }
2070
2071 if !self.update_trade_tick_or_skip(trade, "trade tick") {
2072 return;
2073 }
2074 }
2075
2076 self.core.set_last_raw(trade.price);
2077
2078 if !self.config.trade_execution {
2079 if self.book_type == BookType::L1_MBP {
2081 if let Some(bid) = self.book.best_bid_price() {
2082 self.core.set_bid_raw(bid);
2083 }
2084
2085 if let Some(ask) = self.book.best_ask_price() {
2086 self.core.set_ask_raw(ask);
2087 }
2088 }
2089 return;
2090 }
2091
2092 let aggressor_side = trade.aggressor_side;
2093
2094 match aggressor_side {
2095 AggressorSide::Buyer => {
2096 if self.core.ask.is_none() || price_raw > self.core.ask.map_or(0, |p| p.raw) {
2099 self.core.set_ask_raw(trade.price);
2100 }
2101
2102 if self.core.bid.is_none() {
2104 self.core.set_bid_raw(trade.price);
2105 }
2106 }
2107 AggressorSide::Seller => {
2108 if self.core.bid.is_none()
2111 || price_raw < self.core.bid.map_or(PriceRaw::MAX, |p| p.raw)
2112 {
2113 self.core.set_bid_raw(trade.price);
2114 }
2115
2116 if self.core.ask.is_none() {
2118 self.core.set_ask_raw(trade.price);
2119 }
2120 }
2121 AggressorSide::NoAggressor => {
2122 if self.core.bid.is_none()
2123 || price_raw <= self.core.bid.map_or(PriceRaw::MAX, |p| p.raw)
2124 {
2125 self.core.set_bid_raw(trade.price);
2126 }
2127
2128 if self.core.ask.is_none() || price_raw >= self.core.ask.map_or(0, |p| p.raw) {
2129 self.core.set_ask_raw(trade.price);
2130 }
2131 }
2132 }
2133
2134 let original_bid = self.core.bid;
2135 let original_ask = self.core.ask;
2136
2137 match aggressor_side {
2138 AggressorSide::Seller => {
2139 if original_ask.is_some_and(|ask| price_raw < ask.raw) {
2140 self.core.set_ask_raw(trade.price);
2141 }
2142 }
2143 AggressorSide::Buyer => {
2144 if original_bid.is_some_and(|bid| price_raw > bid.raw) {
2145 self.core.set_bid_raw(trade.price);
2146 }
2147 }
2148 AggressorSide::NoAggressor => {
2149 self.core.set_bid_raw(trade.price);
2151 self.core.set_ask_raw(trade.price);
2152 }
2153 }
2154
2155 self.last_trade_size = Some(trade.size);
2156 self.trade_consumption = 0;
2157
2158 if self.config.liquidity_consumption && self.book_type != BookType::L1_MBP {
2159 self.seed_trade_consumption(price_raw, trade.size.raw, trade.ts_event, aggressor_side);
2160 }
2161
2162 self.resolve_pending_on_trade(price_raw);
2163 self.decrement_queue_on_trade(price_raw, trade.size.raw, aggressor_side);
2164
2165 self.iterate(trade.ts_init, aggressor_side);
2166
2167 self.last_trade_size = None;
2168 self.trade_consumption = 0;
2169
2170 if self.book_type == BookType::L1_MBP {
2176 match aggressor_side {
2177 AggressorSide::Seller => {
2178 if let Some(ask) = self.last_quote_ask {
2179 self.core.ask = Some(ask);
2180 }
2181 }
2182 AggressorSide::Buyer => {
2183 if let Some(bid) = self.last_quote_bid {
2184 self.core.bid = Some(bid);
2185 }
2186 }
2187 AggressorSide::NoAggressor => {}
2188 }
2189 } else {
2190 match aggressor_side {
2191 AggressorSide::Seller => {
2192 if let Some(ask) = original_ask
2193 && price_raw < ask.raw
2194 {
2195 self.core.ask = Some(ask);
2196 }
2197 }
2198 AggressorSide::Buyer => {
2199 if let Some(bid) = original_bid
2200 && price_raw > bid.raw
2201 {
2202 self.core.bid = Some(bid);
2203 }
2204 }
2205 AggressorSide::NoAggressor => {}
2206 }
2207 }
2208 }
2209
2210 fn update_quote_tick_or_skip(&mut self, quote: &QuoteTick, context: &str) -> bool {
2211 if let Err(e) = self.book.update_quote_tick(quote) {
2212 log::warn!(
2213 "Skipping {context} for {}: update_quote_tick failed: {e}",
2214 quote.instrument_id,
2215 );
2216 return false;
2217 }
2218 true
2219 }
2220
2221 fn update_trade_tick_or_skip(&mut self, trade: &TradeTick, context: &str) -> bool {
2222 if let Err(e) = self.book.update_trade_tick(trade) {
2223 log::warn!(
2224 "Skipping {context} for {}: update_trade_tick failed: {e}",
2225 trade.instrument_id,
2226 );
2227 return false;
2228 }
2229 true
2230 }
2231
2232 pub fn process_status(&mut self, action: MarketStatusAction) {
2234 log::debug!("Processing {action}");
2235
2236 match action {
2237 MarketStatusAction::Trading | MarketStatusAction::PreOpen
2238 if matches!(
2239 self.market_status,
2240 MarketStatus::Closed | MarketStatus::Paused | MarketStatus::Suspended
2241 ) =>
2242 {
2243 self.market_status = MarketStatus::Open;
2244 }
2245 MarketStatusAction::Pause if self.market_status == MarketStatus::Open => {
2246 self.market_status = MarketStatus::Paused;
2247 }
2248 MarketStatusAction::Suspend if self.market_status == MarketStatus::Open => {
2249 self.market_status = MarketStatus::Suspended;
2250 }
2251 MarketStatusAction::Halt | MarketStatusAction::Close
2252 if self.market_status == MarketStatus::Open =>
2253 {
2254 self.market_status = MarketStatus::Closed;
2255 }
2256 _ => {}
2257 }
2258 }
2259
2260 pub fn process_instrument_close(&mut self, close: InstrumentClose) {
2265 if close.instrument_id != self.instrument.id() {
2266 log::warn!(
2267 "Received instrument close for unknown instrument_id: {}",
2268 close.instrument_id
2269 );
2270 return;
2271 }
2272
2273 if close.close_type == InstrumentCloseType::ContractExpired {
2274 self.instrument_close = Some(close);
2275 self.iterate(close.ts_init, AggressorSide::NoAggressor);
2276 }
2277 }
2278
2279 pub fn process_instrument_expiration(&mut self, timestamp_ns: UnixNanos) {
2281 self.check_instrument_expiration(timestamp_ns);
2282 }
2283
2284 #[must_use]
2286 pub const fn is_expiration_processed(&self) -> bool {
2287 self.expiration_processed
2288 }
2289
2290 fn requires_pending_resolution(&self) -> bool {
2291 matches!(self.instrument, InstrumentAny::BinaryOption(_))
2292 }
2293
2294 fn cancel_open_orders_for_expiration(&mut self) {
2295 let instrument_id = self.instrument.id();
2300 let expiration_order_ids: IndexSet<ClientOrderId> = {
2301 let cache = self.cache.borrow();
2302 let mut order_ids = IndexSet::new();
2303
2304 for order_info in self.get_open_orders() {
2305 order_ids.insert(order_info.client_order_id);
2306 }
2307
2308 for order in cache.orders(None, Some(&instrument_id), None, None, None) {
2309 if order.is_open() || order.is_inflight() {
2310 order_ids.insert(order.client_order_id());
2311 }
2312 }
2313
2314 order_ids
2315 };
2316
2317 for client_order_id in expiration_order_ids {
2318 let order = {
2319 let cache = self.cache.borrow();
2320 cache.order(&client_order_id).map(|order| order.clone())
2321 };
2322
2323 if let Some(order) = order {
2324 self.cancel_order(&order, None);
2325 }
2326 }
2327 }
2328
2329 fn enter_pending_resolution(&mut self) {
2330 if self.pending_resolution {
2331 return;
2332 }
2333
2334 self.pending_resolution = true;
2335 self.market_status = MarketStatus::Closed;
2336 self.cancel_open_orders_for_expiration();
2337 log::info!(
2338 "{} expired and is now pending resolution; open orders canceled and new orders blocked",
2339 self.instrument.id()
2340 );
2341 }
2342
2343 fn check_instrument_expiration(&mut self, timestamp_ns: UnixNanos) {
2344 if self.expiration_processed {
2345 return;
2346 }
2347
2348 let timestamp_triggered = self
2349 .instrument
2350 .expiration_ns()
2351 .is_some_and(|ns| timestamp_ns >= ns);
2352
2353 if !timestamp_triggered && self.instrument_close.is_none() {
2354 return;
2355 }
2356
2357 if self.instrument_close.is_none()
2358 && timestamp_triggered
2359 && self.requires_pending_resolution()
2360 {
2361 self.enter_pending_resolution();
2362 return;
2363 }
2364
2365 self.expiration_processed = true;
2366 self.pending_resolution = false;
2367 let close = self.instrument_close.take();
2368 log::info!("{} reached expiration", self.instrument.id());
2369 self.cancel_open_orders_for_expiration();
2370
2371 if matches!(
2372 self.instrument,
2373 InstrumentAny::OptionContract(_) | InstrumentAny::CryptoOption(_)
2374 ) {
2375 self.process_option_expiry(timestamp_ns);
2376 return;
2377 }
2378
2379 let instrument_id = self.instrument.id();
2380 let positions: Vec<(TraderId, StrategyId, PositionId, OrderSide, Quantity)> = {
2381 let cache = self.cache.borrow();
2382 cache
2383 .positions_open(None, Some(&instrument_id), None, None, None)
2384 .into_iter()
2385 .map(|pos| {
2386 let closing_side = match pos.side {
2387 PositionSide::Long => OrderSide::Sell,
2388 PositionSide::Short => OrderSide::Buy,
2389 _ => OrderSide::NoOrderSide,
2390 };
2391 (
2392 pos.trader_id,
2393 pos.strategy_id,
2394 pos.id,
2395 closing_side,
2396 pos.quantity,
2397 )
2398 })
2399 .collect()
2400 };
2401
2402 let ts_now = self.clock.borrow().timestamp_ns();
2403 let close_price_fallback = close.as_ref().map(|c| c.close_price);
2404
2405 for (trader_id, strategy_id, position_id, closing_side, quantity) in positions {
2406 let client_order_id =
2407 ClientOrderId::from(format!("EXPIRATION-{}-{}", self.venue, UUID4::new()).as_str());
2408 let mut order = OrderAny::Market(MarketOrder::new(
2409 trader_id,
2410 strategy_id,
2411 instrument_id,
2412 client_order_id,
2413 closing_side,
2414 quantity,
2415 TimeInForce::Gtc,
2416 UUID4::new(),
2417 ts_now,
2418 true, false,
2420 None,
2421 None,
2422 None,
2423 None,
2424 None,
2425 None,
2426 None,
2427 Some(vec![Ustr::from(&format!(
2428 "EXPIRATION_{}_CLOSE",
2429 self.venue
2430 ))]),
2431 ));
2432 order.set_liquidity_side(LiquiditySide::Taker);
2433
2434 let add_result =
2435 self.cache
2436 .borrow_mut()
2437 .add_order(order.clone(), Some(position_id), None, false);
2438 if add_result.is_err() {
2439 log::debug!("Expiration order already in cache: {client_order_id}");
2440 } else {
2441 self.publish_order_initialized(&order);
2442 }
2443
2444 let venue_order_id = self.ids_generator.get_venue_order_id(&order).unwrap();
2445 self.generate_order_accepted(&order, venue_order_id);
2446
2447 let fill_price = self.settlement_price.or(close_price_fallback);
2448 if let Some(fill_price) = fill_price {
2449 if let Err(e) = self.apply_fills(
2450 &order,
2451 &[(fill_price, quantity)],
2452 LiquiditySide::Taker,
2453 Some(position_id),
2454 None,
2455 None,
2456 ) {
2457 log::error!("Cannot fill expiration order {client_order_id}: {e}");
2458 }
2459 } else {
2460 self.fill_market_order(client_order_id);
2461 }
2462 }
2463 }
2464
2465 fn process_option_expiry(&mut self, ts_now: UnixNanos) {
2466 let instrument_id = self.instrument.id();
2467
2468 let positions: Vec<Position> = {
2469 let cache = self.cache.borrow();
2470 cache
2471 .positions_open(None, Some(&instrument_id), None, None, None)
2472 .into_iter()
2473 .map(|p| p.cloned())
2474 .collect()
2475 };
2476
2477 if positions.is_empty() {
2478 return;
2479 }
2480
2481 let underlying = match self.instrument.underlying() {
2482 Some(u) => u,
2483 None => {
2484 log::error!("No underlying for option {instrument_id}");
2485 return;
2486 }
2487 };
2488 let underlying_id = InstrumentId::from(format!("{underlying}.{}", self.venue).as_str());
2489
2490 let underlying_instrument = {
2491 let cache = self.cache.borrow();
2492 cache.instrument(&underlying_id).cloned()
2493 };
2494
2495 let underlying_instrument = match underlying_instrument {
2496 Some(u) => u,
2497 None => {
2498 log::error!("No underlying instrument for option {instrument_id}");
2499 return;
2500 }
2501 };
2502
2503 let underlying_price = {
2508 let cache = self.cache.borrow();
2509 if matches!(underlying_instrument, InstrumentAny::IndexInstrument(_)) {
2510 cache.index_price(&underlying_id).map(|ip| ip.value)
2511 } else {
2512 cache.price(&underlying_id, PriceType::Last)
2513 }
2514 };
2515
2516 let underlying_price = match underlying_price {
2517 Some(p) => p,
2518 None => {
2519 log::error!("No underlying price for option {instrument_id}");
2520 return;
2521 }
2522 };
2523
2524 let custom_option_price = self.settlement_price;
2525 let should_exercise = self.option_should_exercise(underlying_price);
2526
2527 for position in positions {
2528 self.account_ids
2529 .insert(position.trader_id, position.account_id);
2530
2531 if should_exercise {
2532 self.option_exercise_position(
2533 &position,
2534 &underlying_instrument,
2535 underlying_price,
2536 ts_now,
2537 custom_option_price,
2538 );
2539 } else {
2540 self.option_otm_expiry(&position, ts_now, custom_option_price);
2541 }
2542 }
2543 }
2544
2545 fn option_should_exercise(&self, underlying_price: Price) -> bool {
2546 let strike = match self.instrument.strike_price() {
2547 Some(p) => p.as_decimal(),
2548 None => return false,
2549 };
2550 let spot = underlying_price.as_decimal();
2551 match self.instrument.option_kind() {
2552 Some(OptionKind::Call) => spot > strike,
2553 Some(OptionKind::Put) => strike > spot,
2554 None => false,
2555 }
2556 }
2557
2558 fn option_settlement_price(&self, underlying_price: Price, cash_settled: bool) -> Price {
2559 let strike = self
2560 .instrument
2561 .strike_price()
2562 .expect("option must have strike");
2563 if !cash_settled {
2564 return strike;
2565 }
2566
2567 let spot = underlying_price.as_decimal();
2568 let strike_value = strike.as_decimal();
2569 let value = match self.instrument.option_kind() {
2570 Some(OptionKind::Call) => (spot - strike_value).max(Decimal::ZERO),
2571 _ => (strike_value - spot).max(Decimal::ZERO),
2572 };
2573 Price::from_decimal_dp(value, strike.precision).expect("Invalid option settlement price")
2574 }
2575
2576 fn option_exercise_position(
2577 &self,
2578 position: &Position,
2579 underlying_instrument: &InstrumentAny,
2580 underlying_price: Price,
2581 ts_now: UnixNanos,
2582 custom_option_price: Option<Price>,
2583 ) {
2584 if matches!(underlying_instrument, InstrumentAny::IndexInstrument(_)) {
2585 self.option_cash_settlement(position, underlying_price, ts_now, custom_option_price);
2586 } else {
2587 self.option_physical_settlement(
2588 position,
2589 underlying_instrument,
2590 underlying_price,
2591 ts_now,
2592 custom_option_price,
2593 );
2594 }
2595 }
2596
2597 fn option_cash_settlement(
2598 &self,
2599 position: &Position,
2600 underlying_price: Price,
2601 ts_now: UnixNanos,
2602 custom_option_price: Option<Price>,
2603 ) {
2604 let venue = self.venue;
2605 let trade_id = format!("{venue}-LEG-CASH-{}", &UUID4::new().to_string()[..8]);
2606 let close_px = custom_option_price
2607 .unwrap_or_else(|| self.option_settlement_price(underlying_price, true));
2608 let close_side = OrderCore::closing_side(position.side);
2609 self.option_register_settlement_order(
2610 position,
2611 self.instrument.id(),
2612 close_side,
2613 position.quantity,
2614 ClientOrderId::from(trade_id.as_str()),
2615 VenueOrderId::from(trade_id.as_str()),
2616 Some(position.id),
2617 true,
2618 &format!("EXPIRATION_{venue}_CASH"),
2619 );
2620 let fill = self.option_create_close_fill(position, close_px, &trade_id, ts_now);
2621 self.dispatch_order_event(OrderEventAny::Filled(fill));
2622 }
2623
2624 fn option_physical_settlement(
2625 &self,
2626 position: &Position,
2627 underlying_instrument: &InstrumentAny,
2628 underlying_price: Price,
2629 ts_now: UnixNanos,
2630 custom_option_price: Option<Price>,
2631 ) {
2632 let multiplier = self.instrument.multiplier();
2633 let underlying_qty = Quantity::from_decimal_dp(
2634 position.quantity.as_decimal() * multiplier.as_decimal(),
2635 underlying_instrument.size_precision(),
2636 )
2637 .expect("Invalid underlying settlement quantity");
2638
2639 let underlying_side = if self.instrument.option_kind() == Some(OptionKind::Call) {
2640 position.side
2641 } else {
2642 match position.side {
2643 PositionSide::Long => PositionSide::Short,
2644 PositionSide::Short => PositionSide::Long,
2645 other => other,
2646 }
2647 };
2648
2649 let venue = self.venue;
2650 let trade_base = format!("{venue}-LEG-EX-{}", &UUID4::new().to_string()[..8]);
2651 let close_trade_id = format!("{trade_base}-CLOSE");
2652 let open_trade_id = format!("{trade_base}-OPEN");
2653 let settlement_px = self.option_settlement_price(underlying_price, false);
2654 let option_close_px =
2655 custom_option_price.unwrap_or_else(|| Price::zero(self.instrument.price_precision()));
2656 let close_side = OrderCore::closing_side(position.side);
2657 let underlying_order_side = match underlying_side {
2658 PositionSide::Long => OrderSide::Buy,
2659 _ => OrderSide::Sell,
2660 };
2661
2662 self.option_register_settlement_order(
2663 position,
2664 self.instrument.id(),
2665 close_side,
2666 position.quantity,
2667 ClientOrderId::from(close_trade_id.as_str()),
2668 VenueOrderId::from(close_trade_id.as_str()),
2669 Some(position.id),
2670 true,
2671 &format!("EXPIRATION_{venue}_PHYSICAL_CLOSE"),
2672 );
2673 self.option_register_settlement_order(
2674 position,
2675 underlying_instrument.id(),
2676 underlying_order_side,
2677 underlying_qty,
2678 ClientOrderId::from(open_trade_id.as_str()),
2679 VenueOrderId::from(open_trade_id.as_str()),
2680 None,
2681 false,
2682 &format!("EXPIRATION_{venue}_PHYSICAL_OPEN"),
2683 );
2684
2685 let option_fill =
2686 self.option_create_close_fill(position, option_close_px, &close_trade_id, ts_now);
2687 let underlying_fill = self.option_create_underlying_fill(
2688 position,
2689 underlying_instrument,
2690 underlying_qty,
2691 underlying_side,
2692 settlement_px,
2693 &open_trade_id,
2694 ts_now,
2695 );
2696 self.dispatch_order_event(OrderEventAny::Filled(option_fill));
2697 self.dispatch_order_event(OrderEventAny::Filled(underlying_fill));
2698 }
2699
2700 fn option_otm_expiry(
2701 &self,
2702 position: &Position,
2703 ts_now: UnixNanos,
2704 custom_option_price: Option<Price>,
2705 ) {
2706 let venue = self.venue;
2707 let trade_id = format!("{venue}-LEG-OTM-{}", &UUID4::new().to_string()[..8]);
2708 let close_px =
2709 custom_option_price.unwrap_or_else(|| Price::zero(self.instrument.price_precision()));
2710 let close_side = OrderCore::closing_side(position.side);
2711 self.option_register_settlement_order(
2712 position,
2713 self.instrument.id(),
2714 close_side,
2715 position.quantity,
2716 ClientOrderId::from(trade_id.as_str()),
2717 VenueOrderId::from(trade_id.as_str()),
2718 Some(position.id),
2719 true,
2720 &format!("EXPIRATION_{venue}_OTM"),
2721 );
2722 let fill = self.option_create_close_fill(position, close_px, &trade_id, ts_now);
2723 self.dispatch_order_event(OrderEventAny::Filled(fill));
2724 }
2725
2726 #[expect(clippy::too_many_arguments)]
2727 fn option_register_settlement_order(
2728 &self,
2729 position: &Position,
2730 instrument_id: InstrumentId,
2731 order_side: OrderSide,
2732 quantity: Quantity,
2733 client_order_id: ClientOrderId,
2734 venue_order_id: VenueOrderId,
2735 position_id: Option<PositionId>,
2736 reduce_only: bool,
2737 tag: &str,
2738 ) {
2739 let ts_now = self.clock.borrow().timestamp_ns();
2740 let order = OrderAny::Market(MarketOrder::new(
2741 position.trader_id,
2742 position.strategy_id,
2743 instrument_id,
2744 client_order_id,
2745 order_side,
2746 quantity,
2747 TimeInForce::Gtc,
2748 UUID4::new(),
2749 ts_now,
2750 reduce_only,
2751 false,
2752 None,
2753 None,
2754 None,
2755 None,
2756 None,
2757 None,
2758 None,
2759 Some(vec![Ustr::from(tag)]),
2760 ));
2761
2762 {
2763 let mut cache = self.cache.borrow_mut();
2764 if let Err(e) = cache.add_order(order.clone(), position_id, None, false) {
2765 log::debug!("Settlement order already in cache: {e}");
2766 } else {
2767 drop(cache);
2768 self.publish_order_initialized(&order);
2769 self.cache
2770 .borrow_mut()
2771 .add_venue_order_id(&client_order_id, &venue_order_id, false)
2772 .ok();
2773 }
2774 }
2775
2776 self.generate_order_accepted(&order, venue_order_id);
2777 }
2778
2779 fn option_create_close_fill(
2780 &self,
2781 position: &Position,
2782 price: Price,
2783 trade_id_str: &str,
2784 ts_now: UnixNanos,
2785 ) -> OrderFilled {
2786 let close_side = OrderCore::closing_side(position.side);
2787 OrderFilled::new(
2788 position.trader_id,
2789 position.strategy_id,
2790 self.instrument.id(),
2791 ClientOrderId::from(trade_id_str),
2792 VenueOrderId::from(trade_id_str),
2793 position.account_id,
2794 TradeId::from(trade_id_str),
2795 close_side,
2796 OrderType::Market,
2797 position.quantity,
2798 price,
2799 self.instrument.quote_currency(),
2800 LiquiditySide::Taker,
2801 UUID4::new(),
2802 ts_now,
2803 ts_now,
2804 false,
2805 Some(position.id),
2806 Some(Money::zero(self.instrument.quote_currency())),
2807 None,
2808 )
2809 }
2810
2811 #[expect(clippy::too_many_arguments)]
2812 fn option_create_underlying_fill(
2813 &self,
2814 position: &Position,
2815 underlying_instrument: &InstrumentAny,
2816 quantity: Quantity,
2817 side: PositionSide,
2818 price: Price,
2819 trade_id_str: &str,
2820 ts_now: UnixNanos,
2821 ) -> OrderFilled {
2822 let order_side = match side {
2823 PositionSide::Long => OrderSide::Buy,
2824 _ => OrderSide::Sell,
2825 };
2826 OrderFilled::new(
2827 position.trader_id,
2828 position.strategy_id,
2829 underlying_instrument.id(),
2830 ClientOrderId::from(trade_id_str),
2831 VenueOrderId::from(trade_id_str),
2832 position.account_id,
2833 TradeId::from(trade_id_str),
2834 order_side,
2835 OrderType::Market,
2836 quantity,
2837 price,
2838 underlying_instrument.quote_currency(),
2839 LiquiditySide::Taker,
2840 UUID4::new(),
2841 ts_now,
2842 ts_now,
2843 false,
2844 None,
2845 Some(Money::zero(underlying_instrument.quote_currency())),
2846 None,
2847 )
2848 }
2849
2850 pub fn liquidate_open_positions(
2865 &mut self,
2866 ts_now: UnixNanos,
2867 cancel_open_orders: bool,
2868 settlement_currency: Currency,
2869 ) {
2870 if self.instrument.settlement_currency() != settlement_currency {
2872 return;
2873 }
2874
2875 if cancel_open_orders {
2876 let open_orders: Vec<RestingOrder> = self.get_open_orders();
2877 for order_info in &open_orders {
2878 let order = {
2879 let cache = self.cache.borrow();
2880 cache.order_owned(&order_info.client_order_id)
2881 };
2882
2883 if let Some(order) = order {
2884 self.cancel_order(&order, None);
2885 }
2886 }
2887 }
2888
2889 let instrument_id = self.instrument.id();
2890 let positions: Vec<(
2891 TraderId,
2892 StrategyId,
2893 AccountId,
2894 PositionId,
2895 OrderSide,
2896 Quantity,
2897 )> = {
2898 let cache = self.cache.borrow();
2899 cache
2900 .positions_open(None, Some(&instrument_id), None, None, None)
2901 .into_iter()
2902 .map(|pos| {
2903 (
2904 pos.trader_id,
2905 pos.strategy_id,
2906 pos.account_id,
2907 pos.id,
2908 OrderCore::closing_side(pos.side),
2909 pos.quantity,
2910 )
2911 })
2912 .collect()
2913 };
2914
2915 for (trader_id, strategy_id, account_id, position_id, closing_side, quantity) in positions {
2916 let has_price = if closing_side == OrderSide::Sell {
2918 self.best_bid_price().is_some() || self.settlement_price.is_some()
2919 } else {
2920 self.best_ask_price().is_some() || self.settlement_price.is_some()
2921 };
2922
2923 if !has_price {
2924 log::warn!(
2925 "LIQUIDATION: no price available for {instrument_id} position {position_id}, skipping"
2926 );
2927 continue;
2928 }
2929
2930 let client_order_id = ClientOrderId::from(
2931 format!("LIQUIDATION-{}-{}", self.venue, UUID4::new()).as_str(),
2932 );
2933 let order = OrderAny::Market(MarketOrder::new(
2934 trader_id,
2935 strategy_id,
2936 instrument_id,
2937 client_order_id,
2938 closing_side,
2939 quantity,
2940 TimeInForce::Ioc,
2941 UUID4::new(),
2942 ts_now,
2943 true, false,
2945 None,
2946 None,
2947 None,
2948 None,
2949 None,
2950 None,
2951 None,
2952 Some(vec![Ustr::from(&format!(
2953 "LIQUIDATION_{}_CLOSE",
2954 self.venue
2955 ))]),
2956 ));
2957
2958 let venue_order_id = self.ids_generator.get_venue_order_id(&order).unwrap();
2959 {
2960 let mut cache = self.cache.borrow_mut();
2961 if let Err(e) = cache.add_order(order.clone(), Some(position_id), None, false) {
2962 log::debug!("Liquidation order already in cache: {e}");
2963 } else {
2964 drop(cache);
2965 self.publish_order_initialized(&order);
2966 self.cache
2967 .borrow_mut()
2968 .add_venue_order_id(&client_order_id, &venue_order_id, false)
2969 .ok();
2970 }
2971 }
2972
2973 self.account_ids.insert(trader_id, account_id);
2976 self.generate_order_submitted(&order, account_id);
2977 self.generate_order_accepted(&order, venue_order_id);
2978 self.fill_market_order(client_order_id);
2979 }
2980 }
2981
2982 pub fn process_order(&mut self, order: &mut OrderAny, account_id: AccountId) {
2991 if self.core.order_exists(order.client_order_id()) {
2993 return;
2994 }
2995
2996 let ts_now = self.clock.borrow().timestamp_ns();
2999 self.check_instrument_expiration(ts_now);
3000
3001 let reject_reason: Option<Ustr> = 'validate: {
3006 let cache_borrow = self.cache.as_ref().borrow();
3007
3008 self.account_ids.insert(order.trader_id(), account_id);
3010
3011 if self.pending_resolution {
3012 break 'validate Some(
3013 format!(
3014 "Contract {} has expired and is pending resolution",
3015 self.instrument.id()
3016 )
3017 .into(),
3018 );
3019 }
3020
3021 if self.market_status != MarketStatus::Open {
3022 break 'validate Some(
3023 format!(
3024 "Market {} is {}, cannot accept order {}",
3025 self.instrument.id(),
3026 self.market_status,
3027 order.client_order_id()
3028 )
3029 .into(),
3030 );
3031 }
3032
3033 if self.instrument.has_expiration() {
3035 if let Some(activation_ns) = self.instrument.activation_ns()
3036 && self.clock.borrow().timestamp_ns() < activation_ns
3037 {
3038 break 'validate Some(
3039 format!(
3040 "Contract {} is not yet active, activation {activation_ns}",
3041 self.instrument.id(),
3042 )
3043 .into(),
3044 );
3045 }
3046
3047 if let Some(expiration_ns) = self.instrument.expiration_ns()
3048 && self.clock.borrow().timestamp_ns() >= expiration_ns
3049 {
3050 break 'validate Some(
3051 format!(
3052 "Contract {} has expired, expiration {expiration_ns}",
3053 self.instrument.id(),
3054 )
3055 .into(),
3056 );
3057 }
3058 }
3059
3060 if self.config.support_contingent_orders {
3062 if let Some(parent_order_id) = order.parent_order_id() {
3063 let parent_order = match cache_borrow.order(&parent_order_id) {
3064 Some(o) if o.contingency_type().unwrap() == ContingencyType::Oto => o,
3065 _ => panic!("OTO parent not found"),
3066 };
3067
3068 if parent_order.status() == OrderStatus::Rejected && order.is_open() {
3069 break 'validate Some(
3070 format!("Rejected OTO order from {parent_order_id}").into(),
3071 );
3072 } else if parent_order.status() == OrderStatus::Accepted
3073 || parent_order.status() == OrderStatus::Triggered
3074 || (self.config.oto_full_trigger
3075 && parent_order.status() == OrderStatus::PartiallyFilled)
3076 {
3077 log::info!(
3078 "Pending OTO order {} triggers from {parent_order_id}",
3079 order.client_order_id(),
3080 );
3081 return;
3082 }
3083 }
3084
3085 if let Some(linked_order_ids) = order.linked_order_ids() {
3086 for client_order_id in linked_order_ids {
3087 match cache_borrow.order(client_order_id) {
3088 Some(contingent_order)
3089 if (order.contingency_type().unwrap() == ContingencyType::Oco
3090 || order.contingency_type().unwrap()
3091 == ContingencyType::Ouo)
3092 && !order.is_closed()
3093 && contingent_order.is_closed() =>
3094 {
3095 break 'validate Some(
3096 format!("Contingent order {client_order_id} already closed")
3097 .into(),
3098 );
3099 }
3100 None => panic!("Cannot find contingent order for {client_order_id}"),
3101 _ => {}
3102 }
3103 }
3104 }
3105 }
3106
3107 if order.quantity().precision != self.instrument.size_precision() {
3109 break 'validate Some(
3110 format!(
3111 "Invalid order quantity precision for order {}, was {} when {} size precision is {}",
3112 order.client_order_id(),
3113 order.quantity().precision,
3114 self.instrument.id(),
3115 self.instrument.size_precision()
3116 )
3117 .into(),
3118 );
3119 }
3120
3121 if let Some(display_qty) = order.display_qty()
3123 && display_qty.precision != self.instrument.size_precision()
3124 {
3125 break 'validate Some(
3126 format!(
3127 "Invalid order display quantity precision for order {}, was {} when {} size precision is {}",
3128 order.client_order_id(),
3129 display_qty.precision,
3130 self.instrument.id(),
3131 self.instrument.size_precision()
3132 )
3133 .into(),
3134 );
3135 }
3136
3137 if let Some(price) = order.price()
3139 && price.precision != self.instrument.price_precision()
3140 {
3141 break 'validate Some(
3142 format!(
3143 "Invalid order price precision for order {}, was {} when {} price precision is {}",
3144 order.client_order_id(),
3145 price.precision,
3146 self.instrument.id(),
3147 self.instrument.price_precision()
3148 )
3149 .into(),
3150 );
3151 }
3152
3153 if let Some(trigger_price) = order.trigger_price()
3155 && trigger_price.precision != self.instrument.price_precision()
3156 {
3157 break 'validate Some(
3158 format!(
3159 "Invalid order trigger price precision for order {}, was {} when {} price precision is {}",
3160 order.client_order_id(),
3161 trigger_price.precision,
3162 self.instrument.id(),
3163 self.instrument.price_precision()
3164 )
3165 .into(),
3166 );
3167 }
3168
3169 let position = self.position_for_order_in_cache(&cache_borrow, order);
3170
3171 if order.order_side() == OrderSide::Sell
3173 && self.account_type != AccountType::Margin
3174 && matches!(self.instrument, InstrumentAny::Equity(_))
3175 && position
3176 .as_ref()
3177 .is_none_or(|pos| !order.would_reduce_only(pos.side, pos.quantity))
3178 {
3179 let position_string = position
3180 .as_ref()
3181 .map_or("None".to_string(), |pos| pos.id.to_string());
3182 break 'validate Some(
3183 format!(
3184 "Short selling not permitted on a CASH account with position {position_string} and order {order}",
3185 )
3186 .into(),
3187 );
3188 }
3189
3190 if self.config.use_reduce_only
3192 && order.is_reduce_only()
3193 && !order.is_closed()
3194 && position.as_ref().is_none_or(|pos| {
3195 pos.is_closed()
3196 || (order.is_buy() && pos.is_long())
3197 || (order.is_sell() && pos.is_short())
3198 })
3199 {
3200 break 'validate Some(
3201 format!(
3202 "Reduce-only order {} ({}-{}) would have increased position",
3203 order.client_order_id(),
3204 order.order_type().to_string().to_uppercase(),
3205 order.order_side().to_string().to_uppercase()
3206 )
3207 .into(),
3208 );
3209 }
3210
3211 None
3212 };
3213
3214 if let Some(reason) = reject_reason {
3215 self.generate_order_rejected(order, reason);
3216 return;
3217 }
3218
3219 if order.is_quote_quantity()
3227 && !self.instrument.is_inverse()
3228 && !matches!(
3229 order.order_type(),
3230 OrderType::TrailingStopLimit | OrderType::TrailingStopMarket,
3231 )
3232 && (order.price().is_some()
3233 || matches!(
3234 order.order_type(),
3235 OrderType::Market | OrderType::MarketToLimit,
3236 ))
3237 && !self.convert_quote_to_base_quantity(order)
3238 {
3239 return;
3240 }
3241
3242 match order.order_type() {
3243 OrderType::Market => self.process_market_order(order),
3244 OrderType::Limit => self.process_limit_order(order),
3245 OrderType::MarketToLimit => self.process_market_to_limit_order(order),
3246 OrderType::StopMarket => self.process_stop_market_order(order),
3247 OrderType::StopLimit => self.process_stop_limit_order(order),
3248 OrderType::MarketIfTouched => self.process_market_if_touched_order(order),
3249 OrderType::LimitIfTouched => self.process_limit_if_touched_order(order),
3250 OrderType::TrailingStopMarket => self.process_trailing_stop_order(order),
3251 OrderType::TrailingStopLimit => self.process_trailing_stop_order(order),
3252 }
3253 }
3254
3255 fn convert_quote_to_base_quantity(&self, order: &mut OrderAny) -> bool {
3256 let reference_price = if let Some(price) = order.price() {
3260 Some(price)
3261 } else {
3262 match order.order_side() {
3263 OrderSide::Buy => self.core.ask,
3264 OrderSide::Sell => self.core.bid,
3265 OrderSide::NoOrderSide => None,
3266 }
3267 };
3268
3269 let Some(reference_price) = reference_price else {
3270 self.generate_order_rejected(
3271 order,
3272 format!(
3273 "No market for {} to convert quote quantity to base",
3274 order.instrument_id(),
3275 )
3276 .into(),
3277 );
3278 return false;
3279 };
3280
3281 let base_quantity = self
3282 .instrument
3283 .calculate_base_quantity(order.quantity(), reference_price);
3284
3285 let ts_now = self.clock.borrow().timestamp_ns();
3286 let event = OrderEventAny::Updated(OrderUpdated::new(
3287 order.trader_id(),
3288 order.strategy_id(),
3289 order.instrument_id(),
3290 order.client_order_id(),
3291 base_quantity,
3292 UUID4::new(),
3293 ts_now,
3294 ts_now,
3295 false,
3296 order.venue_order_id(),
3297 order.account_id(),
3298 None,
3299 None,
3300 None,
3301 false,
3302 ));
3303
3304 if let Err(e) = order.apply(event.clone()) {
3308 log::error!(
3309 "Failed to apply quote-to-base update for {}: {e}",
3310 order.client_order_id(),
3311 );
3312 return false;
3313 }
3314 self.dispatch_order_event(event);
3315 true
3316 }
3317
3318 pub fn process_modify(&mut self, command: &ModifyOrder, account_id: AccountId) {
3320 if !self.core.order_exists(command.client_order_id) {
3321 self.generate_order_modify_rejected(
3322 command.trader_id,
3323 command.strategy_id,
3324 command.instrument_id,
3325 command.client_order_id,
3326 Ustr::from(format!("Order {} not found", command.client_order_id).as_str()),
3327 command.venue_order_id,
3328 Some(account_id),
3329 );
3330 return;
3331 }
3332
3333 let order = match self
3334 .cache
3335 .borrow()
3336 .order(&command.client_order_id)
3337 .map(|o| o.clone())
3338 {
3339 Some(order) => order,
3340 None => {
3341 log::error!(
3342 "Cannot modify order: order {} not found in cache",
3343 command.client_order_id
3344 );
3345 return;
3346 }
3347 };
3348
3349 let update_success = self.update_order(
3350 &order,
3351 command.quantity,
3352 command.price,
3353 command.trigger_price,
3354 None,
3355 );
3356
3357 if !update_success {
3358 return;
3359 }
3360
3361 let Some(refreshed) = self.resync_core_entry(command.client_order_id) else {
3363 return;
3364 };
3365
3366 let price_changed = refreshed.price() != order.price()
3368 || refreshed.trigger_price() != order.trigger_price();
3369
3370 if price_changed
3371 && refreshed.is_open()
3372 && self.config.queue_position
3373 && let Some(new_price) = refreshed.price()
3374 {
3375 self.snapshot_queue_position(&refreshed, new_price);
3376 self.queue_excess.swap_remove(&refreshed.client_order_id());
3377 }
3378 }
3379
3380 pub fn process_cancel(&mut self, command: &CancelOrder, account_id: AccountId) {
3382 if !self.core.order_exists(command.client_order_id) {
3383 self.generate_order_cancel_rejected(
3384 command.trader_id,
3385 command.strategy_id,
3386 account_id,
3387 command.instrument_id,
3388 command.client_order_id,
3389 command.venue_order_id,
3390 Ustr::from(format!("Order {} not found", command.client_order_id).as_str()),
3391 );
3392 return;
3393 }
3394
3395 let order = match self
3396 .cache
3397 .borrow()
3398 .order(&command.client_order_id)
3399 .map(|o| o.clone())
3400 {
3401 Some(order) => order,
3402 None => {
3403 log::error!(
3404 "Cannot cancel order: order {} not found in cache",
3405 command.client_order_id
3406 );
3407 return;
3408 }
3409 };
3410
3411 if !order.is_inflight() && !order.is_open() {
3412 self.purge_stale_core_entry(command.client_order_id);
3413 return;
3414 }
3415
3416 self.cancel_order(&order, None);
3417 }
3418
3419 pub fn process_cancel_all(&mut self, command: &CancelAllOrders, _account_id: AccountId) {
3421 let instrument_id = command.instrument_id;
3422 let order_side = if command.order_side == OrderSide::NoOrderSide {
3423 None
3424 } else {
3425 Some(command.order_side)
3426 };
3427
3428 let client_order_ids: Vec<ClientOrderId> = self
3429 .cache
3430 .borrow()
3431 .orders_open(None, Some(&instrument_id), None, None, order_side)
3432 .iter()
3433 .map(|o| o.client_order_id())
3434 .collect();
3435
3436 for client_order_id in client_order_ids {
3437 let order = match self
3438 .cache
3439 .borrow()
3440 .order(&client_order_id)
3441 .map(|o| o.clone())
3442 {
3443 Some(order) => order,
3444 None => continue,
3445 };
3446
3447 if !order.is_inflight() && !order.is_open() {
3448 self.purge_stale_core_entry(client_order_id);
3449 continue;
3450 }
3451
3452 self.cancel_order(&order, None);
3453 }
3454 }
3455
3456 fn purge_stale_core_entry(&mut self, client_order_id: ClientOrderId) {
3459 if self.core.order_exists(client_order_id) {
3460 self.delete_core_order(client_order_id);
3461 }
3462 self.cached_filled_qty.swap_remove(&client_order_id);
3463 }
3464
3465 fn resync_core_entry(&mut self, client_order_id: ClientOrderId) -> Option<OrderAny> {
3466 let order = self
3467 .cache
3468 .borrow()
3469 .order(&client_order_id)
3470 .map(|o| o.clone())?;
3471
3472 if order.is_closed() {
3475 self.delete_core_order(client_order_id);
3476 return Some(order);
3477 }
3478
3479 let new_match_info = Self::matching_core_entry(&order);
3480
3481 let unchanged = self
3483 .core
3484 .get_order(client_order_id)
3485 .is_some_and(|existing| *existing == new_match_info);
3486
3487 if unchanged {
3488 self.track_post_match_order(&order);
3489 return Some(order);
3490 }
3491
3492 self.delete_core_order(client_order_id);
3493 self.track_post_match_order(&order);
3494 self.core.add_order(new_match_info);
3495 Some(order)
3496 }
3497
3498 pub fn process_batch_cancel(&mut self, command: &BatchCancelOrders, account_id: AccountId) {
3500 for order in &command.cancels {
3501 self.process_cancel(order, account_id);
3502 }
3503 }
3504
3505 pub fn process_batch_modify(&mut self, command: &BatchModifyOrders, account_id: AccountId) {
3507 for order in &command.modifies {
3508 self.process_modify(order, account_id);
3509 }
3510 }
3511
3512 fn process_market_order(&mut self, order: &OrderAny) {
3513 if order.time_in_force() == TimeInForce::AtTheOpen
3514 || order.time_in_force() == TimeInForce::AtTheClose
3515 {
3516 self.generate_order_rejected(
3517 order,
3518 format!(
3519 "time in force {} is not currently supported",
3520 order.time_in_force()
3521 )
3522 .into(),
3523 );
3524 return;
3525 }
3526
3527 if (order.order_side() == OrderSide::Buy && self.core.ask.is_none())
3529 || (order.order_side() == OrderSide::Sell && self.core.bid.is_none())
3530 {
3531 self.generate_order_rejected(
3532 order,
3533 format!("No market for {}", order.instrument_id()).into(),
3534 );
3535 return;
3536 }
3537
3538 if self.config.use_market_order_acks {
3539 let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
3540 self.generate_order_accepted(order, venue_order_id);
3541 }
3542
3543 if let Err(e) = self
3545 .cache
3546 .borrow_mut()
3547 .add_order(order.clone(), None, None, false)
3548 {
3549 log::debug!("Order already in cache: {e}");
3550 }
3551
3552 self.fill_market_order(order.client_order_id());
3553 }
3554
3555 fn process_limit_order(&mut self, order: &mut OrderAny) {
3556 if order.time_in_force() == TimeInForce::AtTheOpen
3557 || order.time_in_force() == TimeInForce::AtTheClose
3558 {
3559 self.generate_order_rejected(
3560 order,
3561 format!(
3562 "time in force {} is not currently supported",
3563 order.time_in_force()
3564 )
3565 .into(),
3566 );
3567 return;
3568 }
3569
3570 let limit_px = order.price().expect("Limit order must have a price");
3571 if order.is_post_only()
3572 && self
3573 .core
3574 .is_limit_matched(order.order_side_specified(), limit_px)
3575 {
3576 self.generate_order_rejected(
3577 order,
3578 format!(
3579 "POST_ONLY {} {} order limit px of {} would have been a TAKER: bid={}, ask={}",
3580 order.order_type(),
3581 order.order_side(),
3582 order.price().unwrap(),
3583 self.core
3584 .bid
3585 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3586 self.core
3587 .ask
3588 .map_or_else(|| "None".to_string(), |p| p.to_string())
3589 )
3590 .into(),
3591 );
3592 return;
3593 }
3594
3595 self.accept_order(order);
3597
3598 if self
3600 .core
3601 .is_limit_matched(order.order_side_specified(), limit_px)
3602 {
3603 order.set_liquidity_side(LiquiditySide::Taker);
3605
3606 if self
3607 .cache
3608 .borrow_mut()
3609 .add_order(order.clone(), None, None, false)
3610 .is_err()
3611 && let Err(e) = self.cache.borrow_mut().replace_order(order)
3612 {
3613 log::debug!("Failed to update order in cache: {e}");
3614 }
3615 self.fill_limit_order(order.client_order_id());
3616
3617 if self.core.order_exists(order.client_order_id())
3620 && let Some(mut order) = self.cache.borrow_mut().order_mut(&order.client_order_id())
3621 {
3622 order.set_liquidity_side(LiquiditySide::Maker);
3623 }
3624 } else if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc) {
3625 self.cancel_order(order, None);
3626 } else {
3627 order.set_liquidity_side(LiquiditySide::Maker);
3629
3630 if let Some(price) = order.price() {
3631 self.snapshot_queue_position(order, price);
3632 }
3633
3634 let add_result = self
3635 .cache
3636 .borrow_mut()
3637 .add_order(order.clone(), None, None, false);
3638
3639 if let Err(e) = add_result {
3640 log::debug!("Failed to add order to cache: {e}");
3641
3642 if let Some(mut order) = self.cache.borrow_mut().order_mut(&order.client_order_id())
3645 && !matches!(
3646 order.liquidity_side(),
3647 Some(LiquiditySide::Maker | LiquiditySide::Taker)
3648 )
3649 {
3650 order.set_liquidity_side(LiquiditySide::Maker);
3651 }
3652 }
3653 }
3654 }
3655
3656 fn process_market_to_limit_order(&mut self, order: &OrderAny) {
3657 if (order.order_side() == OrderSide::Buy && self.core.ask.is_none())
3659 || (order.order_side() == OrderSide::Sell && self.core.bid.is_none())
3660 {
3661 self.generate_order_rejected(
3662 order,
3663 format!("No market for {}", order.instrument_id()).into(),
3664 );
3665 return;
3666 }
3667
3668 if self.config.use_market_order_acks {
3669 let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
3670 self.generate_order_accepted(order, venue_order_id);
3671 }
3672
3673 if let Err(e) = self
3675 .cache
3676 .borrow_mut()
3677 .add_order(order.clone(), None, None, false)
3678 {
3679 log::debug!("Order already in cache: {e}");
3680 }
3681 let client_order_id = order.client_order_id();
3682 self.fill_market_order(client_order_id);
3683
3684 let filled_qty = self
3686 .cached_filled_qty
3687 .get(&client_order_id)
3688 .copied()
3689 .unwrap_or_default();
3690 let leaves_qty = order.quantity().saturating_sub(filled_qty);
3691 if leaves_qty.is_zero() {
3692 self.purge_cached_filled_qty_if_closed(client_order_id);
3693 return;
3694 }
3695
3696 let updated_order = self
3697 .cache
3698 .borrow()
3699 .order(&client_order_id)
3700 .map(|o| o.clone());
3701
3702 if let Some(mut updated_order) = updated_order {
3703 self.accept_order(&mut updated_order);
3704 }
3705 }
3706
3707 fn process_stop_market_order(&mut self, order: &mut OrderAny) {
3708 let stop_px = order
3709 .trigger_price()
3710 .expect("Stop order must have a trigger price");
3711
3712 if self.core.is_stop_matched_with_trigger_type(
3713 order.order_side_specified(),
3714 stop_px,
3715 order.trigger_type().unwrap_or(TriggerType::Default),
3716 ) {
3717 if self.config.reject_stop_orders {
3718 self.generate_order_rejected(
3719 order,
3720 format!(
3721 "{} {} order stop px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3722 order.order_type(),
3723 order.order_side(),
3724 order.trigger_price().unwrap(),
3725 self.core
3726 .bid
3727 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3728 self.core
3729 .ask
3730 .map_or_else(|| "None".to_string(), |p| p.to_string())
3731 ).into(),
3732 );
3733 return;
3734 }
3735
3736 if let Err(e) = self
3737 .cache
3738 .borrow_mut()
3739 .add_order(order.clone(), None, None, false)
3740 {
3741 log::debug!("Order already in cache: {e}");
3742 }
3743 self.fill_market_order(order.client_order_id());
3744 return;
3745 }
3746
3747 self.accept_order(order);
3749
3750 order.set_liquidity_side(LiquiditySide::Maker);
3752
3753 if let Err(e) = self
3754 .cache
3755 .borrow_mut()
3756 .add_order(order.clone(), None, None, false)
3757 {
3758 log::debug!("Order already in cache: {e}");
3759 }
3760 }
3761
3762 fn process_stop_limit_order(&mut self, order: &mut OrderAny) {
3763 let stop_px = order
3764 .trigger_price()
3765 .expect("Stop order must have a trigger price");
3766
3767 if self.core.is_stop_matched_with_trigger_type(
3768 order.order_side_specified(),
3769 stop_px,
3770 order.trigger_type().unwrap_or(TriggerType::Default),
3771 ) {
3772 if self.config.reject_stop_orders {
3773 self.generate_order_rejected(
3774 order,
3775 format!(
3776 "{} {} order stop px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3777 order.order_type(),
3778 order.order_side(),
3779 order.trigger_price().unwrap(),
3780 self.core
3781 .bid
3782 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3783 self.core
3784 .ask
3785 .map_or_else(|| "None".to_string(), |p| p.to_string())
3786 ).into(),
3787 );
3788 return;
3789 }
3790
3791 self.accept_triggered_limit_style_order(order);
3792 return;
3793 }
3794
3795 self.accept_order(order);
3796
3797 order.set_liquidity_side(LiquiditySide::Maker);
3799
3800 if let Err(e) = self
3801 .cache
3802 .borrow_mut()
3803 .add_order(order.clone(), None, None, false)
3804 {
3805 log::debug!("Order already in cache: {e}");
3806 }
3807 }
3808
3809 fn process_market_if_touched_order(&mut self, order: &mut OrderAny) {
3810 if self.core.is_touch_triggered_with_trigger_type(
3811 order.order_side_specified(),
3812 order.trigger_price().unwrap(),
3813 order.trigger_type().unwrap_or(TriggerType::Default),
3814 ) {
3815 if self.config.reject_stop_orders {
3816 self.generate_order_rejected(
3817 order,
3818 format!(
3819 "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3820 order.order_type(),
3821 order.order_side(),
3822 order.trigger_price().unwrap(),
3823 self.core
3824 .bid
3825 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3826 self.core
3827 .ask
3828 .map_or_else(|| "None".to_string(), |p| p.to_string())
3829 ).into(),
3830 );
3831 return;
3832 }
3833
3834 if let Err(e) = self
3835 .cache
3836 .borrow_mut()
3837 .add_order(order.clone(), None, None, false)
3838 {
3839 log::debug!("Order already in cache: {e}");
3840 }
3841 self.fill_market_order(order.client_order_id());
3842 return;
3843 }
3844
3845 self.accept_order(order);
3847
3848 order.set_liquidity_side(LiquiditySide::Maker);
3850
3851 if let Err(e) = self
3852 .cache
3853 .borrow_mut()
3854 .add_order(order.clone(), None, None, false)
3855 {
3856 log::debug!("Order already in cache: {e}");
3857 }
3858 }
3859
3860 fn process_limit_if_touched_order(&mut self, order: &mut OrderAny) {
3861 if self.core.is_touch_triggered_with_trigger_type(
3862 order.order_side_specified(),
3863 order.trigger_price().unwrap(),
3864 order.trigger_type().unwrap_or(TriggerType::Default),
3865 ) {
3866 if self.config.reject_stop_orders {
3867 self.generate_order_rejected(
3868 order,
3869 format!(
3870 "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3871 order.order_type(),
3872 order.order_side(),
3873 order.trigger_price().unwrap(),
3874 self.core
3875 .bid
3876 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3877 self.core
3878 .ask
3879 .map_or_else(|| "None".to_string(), |p| p.to_string())
3880 ).into(),
3881 );
3882 return;
3883 }
3884 self.accept_triggered_limit_style_order(order);
3885 return;
3886 }
3887
3888 self.accept_order(order);
3890
3891 order.set_liquidity_side(LiquiditySide::Maker);
3893
3894 if let Err(e) = self
3895 .cache
3896 .borrow_mut()
3897 .add_order(order.clone(), None, None, false)
3898 {
3899 log::debug!("Order already in cache: {e}");
3900 }
3901 }
3902
3903 fn accept_triggered_limit_style_order(&mut self, order: &mut OrderAny) {
3904 self.accept_order(order);
3905
3906 if let Err(e) = self
3907 .cache
3908 .borrow_mut()
3909 .add_order(order.clone(), None, None, false)
3910 {
3911 log::debug!("Order already in cache: {e}");
3912 }
3913
3914 self.trigger_limit_style_stop_order(order.client_order_id(), order.clone());
3915
3916 if let Some(cached_order) = self
3917 .cache
3918 .borrow()
3919 .order(&order.client_order_id())
3920 .map(|order| order.clone())
3921 {
3922 *order = cached_order;
3923 }
3924 }
3925
3926 fn process_trailing_stop_order(&mut self, order: &mut OrderAny) {
3927 if let Some(trigger_price) = order.trigger_price()
3928 && self.core.is_stop_matched_with_trigger_type(
3929 order.order_side_specified(),
3930 trigger_price,
3931 order.trigger_type().unwrap_or(TriggerType::Default),
3932 )
3933 {
3934 self.generate_order_rejected(
3935 order,
3936 format!(
3937 "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3938 order.order_type(),
3939 order.order_side(),
3940 trigger_price,
3941 self.core
3942 .bid
3943 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3944 self.core
3945 .ask
3946 .map_or_else(|| "None".to_string(), |p| p.to_string())
3947 ).into(),
3948 );
3949 return;
3950 }
3951
3952 order.set_liquidity_side(LiquiditySide::Maker);
3956
3957 self.accept_order(order);
3958
3959 if let Err(e) = self
3960 .cache
3961 .borrow_mut()
3962 .add_order(order.clone(), None, None, false)
3963 {
3964 log::debug!("Order already in cache: {e}");
3965 }
3966 }
3967
3968 pub fn iterate(&mut self, timestamp_ns: UnixNanos, aggressor_side: AggressorSide) {
3975 self.purge_closed_cached_filled_qty();
3977
3978 if aggressor_side == AggressorSide::NoAggressor && self.last_trade_size.is_none() {
3984 if let Some(bid) = self.book.best_bid_price() {
3985 self.core.set_bid_raw(bid);
3986 }
3987
3988 if let Some(ask) = self.book.best_ask_price() {
3989 self.core.set_ask_raw(ask);
3990 }
3991 }
3992
3993 let mut matched_order = false;
3994
3995 if self.market_status == MarketStatus::Open {
3996 for action in self.core.iterate_bids() {
3999 matched_order = true;
4000
4001 match action {
4002 MatchAction::FillLimit(id) => self.fill_limit_order(id),
4003 MatchAction::TriggerStop(id) => self.trigger_stop_order(id),
4004 }
4005 }
4006
4007 for action in self.core.iterate_asks() {
4008 matched_order = true;
4009
4010 match action {
4011 MatchAction::FillLimit(id) => self.fill_limit_order(id),
4012 MatchAction::TriggerStop(id) => self.trigger_stop_order(id),
4013 }
4014 }
4015 }
4016
4017 let order_ids: Vec<ClientOrderId> = if matched_order {
4018 self.core.iter_orders().map(|m| m.client_order_id).collect()
4019 } else if self.post_match_order_ids.is_empty() {
4020 Vec::new()
4021 } else {
4022 self.core
4023 .iter_orders()
4024 .filter_map(|order| {
4025 self.post_match_order_ids
4026 .contains(&order.client_order_id)
4027 .then_some(order.client_order_id)
4028 })
4029 .collect()
4030 };
4031 let support_gtd_orders = self.config.support_gtd_orders;
4032
4033 for client_order_id in order_ids {
4034 let (action, keep_tracking) = {
4035 let cache = self.cache.borrow();
4036 let Some(order) = cache.order(&client_order_id) else {
4037 self.post_match_order_ids.swap_remove(&client_order_id);
4038 continue;
4039 };
4040
4041 (
4042 post_match_order_action(&order, support_gtd_orders, timestamp_ns, |order| {
4043 order.clone()
4044 }),
4045 Self::requires_post_match_maintenance(&order),
4046 )
4047 };
4048
4049 match action {
4050 PostMatchOrderAction::RemoveClosed => {
4051 self.delete_core_order(client_order_id);
4052 self.cached_filled_qty.swap_remove(&client_order_id);
4053 continue;
4054 }
4055 PostMatchOrderAction::Expire(order) => {
4056 self.delete_core_order(client_order_id);
4057 self.cached_filled_qty.swap_remove(&client_order_id);
4058 self.expire_order(&order);
4059 continue;
4060 }
4061 PostMatchOrderAction::UpdateTrailing(mut order) => {
4062 if self.maybe_activate_trailing_stop(
4063 &mut order,
4064 self.core.bid,
4065 self.core.ask,
4066 self.core.last,
4067 ) {
4068 self.update_trailing_stop_order(&order);
4069 self.resync_core_entry(client_order_id);
4070 }
4071 }
4072 PostMatchOrderAction::NoMaintenance => {
4073 if !keep_tracking {
4074 self.post_match_order_ids.swap_remove(&client_order_id);
4075 }
4076 }
4077 }
4078
4079 if self.target_bid.is_some() || self.target_ask.is_some() || self.target_last.is_some()
4082 {
4083 if let Some(t) = self.target_bid.take() {
4084 self.core.bid = Some(t);
4085 }
4086
4087 if let Some(t) = self.target_ask.take() {
4088 self.core.ask = Some(t);
4089 }
4090
4091 if let Some(t) = self.target_last.take() {
4092 self.core.last = Some(t);
4093 }
4094 }
4095 }
4096
4097 if let Some(t) = self.target_bid.take() {
4101 self.core.bid = Some(t);
4102 }
4103
4104 if let Some(t) = self.target_ask.take() {
4105 self.core.ask = Some(t);
4106 }
4107
4108 if let Some(t) = self.target_last.take() {
4109 self.core.last = Some(t);
4110 }
4111
4112 self.core.bid = self.book.best_bid_price();
4115 self.core.ask = self.book.best_ask_price();
4116
4117 self.check_instrument_expiration(timestamp_ns);
4120 self.purge_closed_cached_filled_qty();
4121 }
4122
4123 fn get_trailing_activation_price(
4124 &self,
4125 trigger_type: TriggerType,
4126 order_side: OrderSide,
4127 bid: Option<Price>,
4128 ask: Option<Price>,
4129 last: Option<Price>,
4130 ) -> Option<Price> {
4131 match trigger_type {
4132 TriggerType::LastPrice => last,
4133 TriggerType::LastOrBidAsk => last.or(match order_side {
4134 OrderSide::Buy => ask,
4135 OrderSide::Sell => bid,
4136 _ => None,
4137 }),
4138 _ => match order_side {
4140 OrderSide::Buy => ask,
4141 OrderSide::Sell => bid,
4142 _ => None,
4143 },
4144 }
4145 }
4146
4147 fn maybe_activate_trailing_stop(
4148 &self,
4149 order: &mut OrderAny,
4150 bid: Option<Price>,
4151 ask: Option<Price>,
4152 last: Option<Price>,
4153 ) -> bool {
4154 match order {
4155 OrderAny::TrailingStopMarket(inner) => {
4156 if inner.is_activated {
4157 return true;
4158 }
4159
4160 if inner.activation_price.is_none() {
4161 let px = self.get_trailing_activation_price(
4162 inner.trigger_type,
4163 inner.order_side(),
4164 bid,
4165 ask,
4166 last,
4167 );
4168
4169 if let Some(p) = px {
4170 inner.activation_price = Some(p);
4171 inner.set_activated();
4172
4173 if let Err(e) = self.cache.borrow_mut().replace_order(order) {
4174 log::error!("Failed to update order: {e}");
4175 }
4176 return true;
4177 }
4178 return false;
4179 }
4180
4181 let activation_price = inner.activation_price.unwrap();
4182 let hit = match inner.order_side() {
4183 OrderSide::Buy => ask.is_some_and(|a| a <= activation_price),
4184 OrderSide::Sell => bid.is_some_and(|b| b >= activation_price),
4185 _ => false,
4186 };
4187
4188 if hit {
4189 inner.set_activated();
4190
4191 if let Err(e) = self.cache.borrow_mut().replace_order(order) {
4192 log::error!("Failed to update order: {e}");
4193 }
4194 }
4195 hit
4196 }
4197 OrderAny::TrailingStopLimit(inner) => {
4198 if inner.is_activated {
4199 return true;
4200 }
4201
4202 if inner.activation_price.is_none() {
4203 let px = self.get_trailing_activation_price(
4204 inner.trigger_type,
4205 inner.order_side(),
4206 bid,
4207 ask,
4208 last,
4209 );
4210
4211 if let Some(p) = px {
4212 inner.activation_price = Some(p);
4213 inner.set_activated();
4214
4215 if let Err(e) = self.cache.borrow_mut().replace_order(order) {
4216 log::error!("Failed to update order: {e}");
4217 }
4218 return true;
4219 }
4220 return false;
4221 }
4222
4223 let activation_price = inner.activation_price.unwrap();
4224 let hit = match inner.order_side() {
4225 OrderSide::Buy => ask.is_some_and(|a| a <= activation_price),
4226 OrderSide::Sell => bid.is_some_and(|b| b >= activation_price),
4227 _ => false,
4228 };
4229
4230 if hit {
4231 inner.set_activated();
4232
4233 if let Err(e) = self.cache.borrow_mut().replace_order(order) {
4234 log::error!("Failed to update order: {e}");
4235 }
4236 }
4237 hit
4238 }
4239 _ => true,
4240 }
4241 }
4242
4243 fn determine_limit_price_and_volume(&mut self, order: &OrderAny) -> Vec<(Price, Quantity)> {
4244 match order.price() {
4245 Some(order_price) => {
4246 let mut fills = if self.config.liquidity_consumption {
4251 let size_prec = self.instrument.size_precision();
4252 self.book
4253 .get_all_crossed_levels(order.order_side(), order_price, size_prec)
4254 } else {
4255 let book_order =
4256 BookOrder::new(order.order_side(), order_price, order.quantity(), 1);
4257 self.book.simulate_fills(&book_order)
4258 };
4259
4260 if let Some(trade_size) = self.last_trade_size
4262 && let Some(trade_price) = self.core.last
4263 {
4264 let fills_at_trade_price = fills.iter().any(|(px, _)| *px == trade_price);
4265
4266 if !fills_at_trade_price
4267 && self
4268 .core
4269 .is_limit_matched(order.order_side_specified(), order_price)
4270 {
4271 let leaves_qty = order.leaves_qty();
4274 let available_qty = if self.config.liquidity_consumption {
4275 let remaining = trade_size.raw.saturating_sub(self.trade_consumption);
4276 Quantity::from_raw(remaining, trade_size.precision)
4277 } else {
4278 trade_size
4279 };
4280
4281 let fill_qty = min(leaves_qty, available_qty);
4282
4283 if !fill_qty.is_zero() {
4284 log::debug!(
4285 "Trade execution fill: {} @ {} (trade_price={}, available: {}, book had {} fills)",
4286 fill_qty,
4287 order_price,
4288 trade_price,
4289 available_qty,
4290 fills.len()
4291 );
4292
4293 if self.config.liquidity_consumption {
4294 self.trade_consumption += fill_qty.raw;
4295 }
4296
4297 return vec![(order_price, fill_qty)];
4302 }
4303 }
4304 }
4305
4306 if fills.is_empty() {
4308 return fills;
4309 }
4310
4311 let book_prices: Vec<Price> = if self.config.liquidity_consumption {
4315 fills.iter().map(|(px, _)| *px).collect()
4316 } else {
4317 Vec::new()
4318 };
4319 let book_prices_ref: Option<&[Price]> = if book_prices.is_empty() {
4320 None
4321 } else {
4322 Some(&book_prices)
4323 };
4324
4325 if order
4327 .liquidity_side()
4328 .is_some_and(|liquidity_side| liquidity_side == LiquiditySide::Maker)
4329 {
4330 match order.order_side().as_specified() {
4331 OrderSideSpecified::Buy => {
4332 let target_price = if order
4333 .trigger_price()
4334 .is_some_and(|trigger_price| order_price > trigger_price)
4335 {
4336 order.trigger_price().unwrap()
4337 } else {
4338 order_price
4339 };
4340
4341 for fill in &mut fills {
4342 let last_px = fill.0;
4343 if last_px < order_price {
4344 self.target_bid = self.core.bid;
4346 self.target_ask = self.core.ask;
4347 self.target_last = self.core.last;
4348 self.core.set_ask_raw(target_price);
4349 self.core.set_last_raw(target_price);
4350 fill.0 = target_price;
4351 }
4352 }
4353 }
4354 OrderSideSpecified::Sell => {
4355 let target_price = if order
4356 .trigger_price()
4357 .is_some_and(|trigger_price| order_price < trigger_price)
4358 {
4359 order.trigger_price().unwrap()
4360 } else {
4361 order_price
4362 };
4363
4364 for fill in &mut fills {
4365 let last_px = fill.0;
4366 if last_px > order_price {
4367 self.target_bid = self.core.bid;
4369 self.target_ask = self.core.ask;
4370 self.target_last = self.core.last;
4371 self.core.set_bid_raw(target_price);
4372 self.core.set_last_raw(target_price);
4373 fill.0 = target_price;
4374 }
4375 }
4376 }
4377 }
4378 }
4379
4380 self.apply_liquidity_consumption(
4381 fills,
4382 order.order_side(),
4383 order.leaves_qty(),
4384 book_prices_ref,
4385 )
4386 }
4387 None => panic!("Limit order must have a price"),
4388 }
4389 }
4390
4391 fn determine_market_price_and_volume(&self, order: &OrderAny) -> Vec<(Price, Quantity)> {
4392 let price = match order.order_side().as_specified() {
4393 OrderSideSpecified::Buy => Price::max(FIXED_PRECISION),
4394 OrderSideSpecified::Sell => Price::min(FIXED_PRECISION),
4395 };
4396
4397 let mut fills = if self.config.liquidity_consumption {
4400 let size_prec = self.instrument.size_precision();
4401 self.book
4402 .get_all_crossed_levels(order.order_side(), price, size_prec)
4403 } else {
4404 let book_order = BookOrder::new(order.order_side(), price, order.quantity(), 0);
4405 self.book.simulate_fills(&book_order)
4406 };
4407
4408 if !self.fill_at_market
4411 && self.book_type == BookType::L1_MBP
4412 && !fills.is_empty()
4413 && matches!(
4414 order.order_type(),
4415 OrderType::StopMarket | OrderType::TrailingStopMarket | OrderType::MarketIfTouched
4416 )
4417 && let Some(trigger_price) = order.trigger_price()
4418 {
4419 fills[0] = (trigger_price, fills[0].1);
4420
4421 let mut remaining_qty = order.leaves_qty().raw;
4423 let mut capped_fills = Vec::with_capacity(fills.len());
4424
4425 for (price, qty) in fills {
4426 if remaining_qty == 0 {
4427 break;
4428 }
4429
4430 let capped_qty_raw = min(qty.raw, remaining_qty);
4431 if capped_qty_raw == 0 {
4432 continue;
4433 }
4434
4435 remaining_qty -= capped_qty_raw;
4436 capped_fills.push((price, Quantity::from_raw(capped_qty_raw, qty.precision)));
4437 }
4438
4439 return capped_fills;
4440 }
4441
4442 fills
4443 }
4444
4445 fn determine_market_fill_model_price_and_volume(
4446 &mut self,
4447 order: &OrderAny,
4448 ) -> anyhow::Result<(Vec<(Price, Quantity)>, bool)> {
4449 if let (Some(best_bid), Some(best_ask)) = (self.core.bid, self.core.ask)
4450 && let Some(book) = self.fill_model.get_orderbook_for_fill_simulation(
4451 &self.instrument,
4452 order,
4453 best_bid,
4454 best_ask,
4455 )?
4456 {
4457 let price = match order.order_side().as_specified() {
4458 OrderSideSpecified::Buy => Price::max(FIXED_PRECISION),
4459 OrderSideSpecified::Sell => Price::min(FIXED_PRECISION),
4460 };
4461 let book_order = BookOrder::new(order.order_side(), price, order.quantity(), 0);
4462 let fills = book.simulate_fills(&book_order);
4463 if !fills.is_empty() {
4464 return Ok((fills, true));
4465 }
4466 }
4467 Ok((self.determine_market_price_and_volume(order), false))
4468 }
4469
4470 fn determine_limit_fill_model_price_and_volume(
4471 &mut self,
4472 order: &OrderAny,
4473 ) -> anyhow::Result<Vec<(Price, Quantity)>> {
4474 if let (Some(best_bid), Some(best_ask)) = (self.core.bid, self.core.ask)
4475 && let Some(book) = self.fill_model.get_orderbook_for_fill_simulation(
4476 &self.instrument,
4477 order,
4478 best_bid,
4479 best_ask,
4480 )?
4481 && let Some(limit_price) = order.price()
4482 {
4483 let book_order = BookOrder::new(order.order_side(), limit_price, order.quantity(), 0);
4484 let fills = book.simulate_fills(&book_order);
4485 if !fills.is_empty() {
4486 return Ok(fills);
4487 }
4488 }
4489 Ok(self.determine_limit_price_and_volume(order))
4490 }
4491
4492 pub fn fill_market_order(&mut self, client_order_id: ClientOrderId) {
4497 let mut order = match self
4498 .cache
4499 .borrow()
4500 .order(&client_order_id)
4501 .map(|o| o.clone())
4502 {
4503 Some(order) => order,
4504 None => {
4505 log::error!("Cannot fill market order: order {client_order_id} not found in cache");
4506 return;
4507 }
4508 };
4509
4510 if order.is_closed() {
4511 self.purge_stale_core_entry(client_order_id);
4512 return;
4513 }
4514
4515 if order.is_quote_quantity()
4519 && !self.instrument.is_inverse()
4520 && !self.convert_quote_to_base_quantity(&mut order)
4521 {
4522 return;
4523 }
4524
4525 if let Some(filled_qty) = self.cached_filled_qty.get(&order.client_order_id())
4526 && filled_qty >= &order.quantity()
4527 {
4528 log::debug!(
4529 "Ignoring fill as already filled pending application of events: {:?}, {:?}, {:?}, {:?}",
4530 filled_qty,
4531 order.quantity(),
4532 order.filled_qty(),
4533 order.quantity()
4534 );
4535 return;
4536 }
4537
4538 let (venue_position_id, position) = self.fill_position_for_order(&order, Some(true));
4539
4540 if self.config.use_reduce_only && order.is_reduce_only() && position.is_none() {
4541 log::warn!(
4542 "Canceling REDUCE_ONLY {} as would increase position",
4543 order.order_type()
4544 );
4545 self.cancel_order(&order, None);
4546 return;
4547 }
4548
4549 order.set_liquidity_side(LiquiditySide::Taker);
4550 let (mut fills, from_synthetic) =
4551 match self.determine_market_fill_model_price_and_volume(&order) {
4552 Ok(result) => result,
4553 Err(e) => {
4554 log::error!(
4555 "Cannot fill market order {}: fill model failed: {e}",
4556 order.client_order_id()
4557 );
4558 return;
4559 }
4560 };
4561
4562 let protection_price: Option<Price> = if let Some(protection_points) =
4564 self.config.price_protection_points
4565 && matches!(
4566 order.order_type(),
4567 OrderType::Market | OrderType::StopMarket
4568 ) {
4569 protection_price_calculate(
4570 self.instrument.price_increment(),
4571 &order,
4572 protection_points,
4573 self.core.bid,
4574 self.core.ask,
4575 )
4576 .ok()
4577 } else {
4578 None
4579 };
4580
4581 if let Some(protection_price) = protection_price {
4582 fills = self.filter_fills_by_protection(fills, &order, protection_price);
4583 }
4584
4585 let is_trigger_price_fill = !self.fill_at_market
4588 && self.book_type == BookType::L1_MBP
4589 && matches!(
4590 order.order_type(),
4591 OrderType::StopMarket | OrderType::TrailingStopMarket | OrderType::MarketIfTouched
4592 )
4593 && order.trigger_price().is_some();
4594
4595 if !from_synthetic && !is_trigger_price_fill {
4596 fills = self.apply_liquidity_consumption(
4597 fills,
4598 order.order_side(),
4599 order.leaves_qty(),
4600 None,
4601 );
4602 }
4603
4604 if let Err(e) = self.apply_fills(
4605 &order,
4606 &fills,
4607 LiquiditySide::Taker,
4608 if self.config.use_reduce_only && order.is_reduce_only() {
4609 venue_position_id
4610 } else {
4611 None
4612 },
4613 position.as_ref(),
4614 protection_price,
4615 ) {
4616 log::error!("Cannot fill market order {}: {e}", order.client_order_id());
4617 }
4618 }
4619
4620 fn filter_fills_by_protection(
4621 &self,
4622 fills: Vec<(Price, Quantity)>,
4623 order: &OrderAny,
4624 protection_price: Price,
4625 ) -> Vec<(Price, Quantity)> {
4626 let protection_raw = protection_price.raw;
4627 fills
4628 .into_iter()
4629 .filter(|(fill_price, _)| {
4630 match order.order_side() {
4631 OrderSide::Buy => fill_price.raw <= protection_raw,
4633 OrderSide::Sell => fill_price.raw >= protection_raw,
4635 OrderSide::NoOrderSide => false,
4636 }
4637 })
4638 .collect()
4639 }
4640
4641 pub fn fill_limit_order(&mut self, client_order_id: ClientOrderId) {
4650 let mut order = match self
4651 .cache
4652 .borrow()
4653 .order(&client_order_id)
4654 .map(|o| o.clone())
4655 {
4656 Some(order) => order,
4657 None => {
4658 log::error!("Cannot fill limit order: order {client_order_id} not found in cache");
4659 return;
4660 }
4661 };
4662
4663 if order.is_closed() {
4664 self.purge_stale_core_entry(client_order_id);
4665 return;
4666 }
4667
4668 if order.is_quote_quantity()
4672 && !self.instrument.is_inverse()
4673 && !self.convert_quote_to_base_quantity(&mut order)
4674 {
4675 return;
4676 }
4677
4678 match order.price() {
4679 Some(order_price) => {
4680 let cached_filled_qty = self.cached_filled_qty.get(&order.client_order_id());
4681 if let Some(&qty) = cached_filled_qty
4682 && qty >= order.quantity()
4683 {
4684 log::debug!(
4685 "Ignoring fill as already filled pending application of events: {}, {}, {}, {}",
4686 qty,
4687 order.quantity(),
4688 order.filled_qty(),
4689 order.leaves_qty(),
4690 );
4691 return;
4692 }
4693
4694 if order
4696 .liquidity_side()
4697 .is_some_and(|liquidity_side| liquidity_side == LiquiditySide::Maker)
4698 {
4699 let at_limit = if self.last_trade_size.is_some() && self.core.last.is_some() {
4702 self.core.last.is_some_and(|last| last == order_price)
4703 } else if order.order_side() == OrderSide::Buy {
4704 self.core.bid.is_some_and(|bid| bid == order_price)
4705 } else {
4706 self.core.ask.is_some_and(|ask| ask == order_price)
4707 };
4708
4709 if at_limit {
4710 let is_limit_filled = match self.fill_model.is_limit_filled() {
4711 Ok(value) => value,
4712 Err(e) => {
4713 log::error!(
4714 "Cannot fill limit order {}: fill model failed: {e}",
4715 order.client_order_id()
4716 );
4717 return;
4718 }
4719 };
4720
4721 if !is_limit_filled {
4722 return; }
4724 }
4725 }
4726
4727 let queue_allowed_raw = if self.config.queue_position {
4728 match self.determine_trade_fill_qty(&order) {
4729 None | Some(0) => {
4730 if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc)
4731 {
4732 self.cancel_order(&order, None);
4733 }
4734 return;
4735 }
4736 Some(allowed) => Some(allowed),
4737 }
4738 } else {
4739 None
4740 };
4741
4742 let (venue_position_id, position) = self.fill_position_for_order(&order, None);
4743
4744 if self.config.use_reduce_only && order.is_reduce_only() && position.is_none() {
4745 log::warn!(
4746 "Canceling REDUCE_ONLY {} as would increase position",
4747 order.order_type()
4748 );
4749 self.cancel_order(&order, None);
4750 return;
4751 }
4752
4753 let tc_before = self.trade_consumption;
4754 let mut fills = match self.determine_limit_fill_model_price_and_volume(&order) {
4755 Ok(fills) => fills,
4756 Err(e) => {
4757 log::error!(
4758 "Cannot fill limit order {}: fill model failed: {e}",
4759 order.client_order_id()
4760 );
4761 return;
4762 }
4763 };
4764
4765 if let Some(allowed_raw) = queue_allowed_raw {
4766 let size_prec = self.instrument.size_precision();
4767 let mut remaining = allowed_raw;
4768 fills = fills
4769 .into_iter()
4770 .filter_map(|(price, qty)| {
4771 if remaining == 0 {
4772 return None;
4773 }
4774 let capped = qty.raw.min(remaining);
4775 remaining -= capped;
4776 Some((price, Quantity::from_raw(capped, size_prec)))
4777 })
4778 .collect();
4779
4780 let consumed: QuantityRaw = fills.iter().map(|(_, qty)| qty.raw).sum();
4782
4783 if let Some(excess) = self.queue_excess.get_mut(&order.client_order_id()) {
4784 *excess = excess.saturating_sub(consumed);
4785 }
4786 self.trade_consumption = tc_before + consumed;
4787 }
4788
4789 if fills.is_empty() && self.config.liquidity_consumption {
4793 log::debug!(
4794 "Skipping fill for {}: no liquidity available after consumption",
4795 order.client_order_id()
4796 );
4797
4798 if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc) {
4799 self.cancel_order(&order, None);
4800 }
4801
4802 return;
4803 }
4804
4805 let liquidity_side = order.liquidity_side().unwrap();
4806 if let Err(e) = self.apply_fills(
4807 &order,
4808 &fills,
4809 liquidity_side,
4810 venue_position_id,
4811 position.as_ref(),
4812 None,
4813 ) {
4814 log::error!("Cannot fill limit order {}: {e}", order.client_order_id());
4815 }
4816 }
4817 None => panic!("Limit order must have a price"),
4818 }
4819 }
4820
4821 fn fill_position_for_order(
4822 &mut self,
4823 order: &OrderAny,
4824 generate: Option<bool>,
4825 ) -> (Option<PositionId>, Option<Position>) {
4826 if self.oms_type == OmsType::Hedging
4827 && self.config.use_reduce_only
4828 && order.is_reduce_only()
4829 {
4830 let cache = self.cache.as_ref().borrow();
4831
4832 if let Some(position) = cache.position_for_order(&order.client_order_id()) {
4833 let position = position.cloned();
4834 return (Some(position.id), Some(position));
4835 }
4836
4837 if let Some(position) = Self::open_position_reduced_by_order(&cache, order) {
4838 return (Some(position.id), Some(position));
4839 }
4840 }
4841
4842 let venue_position_id = self.ids_generator.get_position_id(order, generate);
4843
4844 let position = {
4845 let cache = self.cache.as_ref().borrow();
4846 venue_position_id
4847 .as_ref()
4848 .and_then(|position_id| cache.position_owned(position_id))
4849 };
4850
4851 (venue_position_id, position)
4852 }
4853
4854 fn position_for_order_in_cache(&self, cache: &Cache, order: &OrderAny) -> Option<Position> {
4855 if let Some(position) = cache.position_for_order(&order.client_order_id()) {
4856 return Some(position.cloned());
4857 }
4858
4859 if self.oms_type == OmsType::Netting {
4860 let position_id = PositionId::new(
4861 format!("{}-{}", order.instrument_id(), order.strategy_id()).as_str(),
4862 );
4863 return cache
4864 .position(&position_id)
4865 .map(|position| position.cloned());
4866 }
4867
4868 if self.oms_type == OmsType::Hedging
4869 && self.config.use_reduce_only
4870 && order.is_reduce_only()
4871 {
4872 return Self::open_position_reduced_by_order(cache, order);
4873 }
4874
4875 None
4876 }
4877
4878 fn open_position_reduced_by_order(cache: &Cache, order: &OrderAny) -> Option<Position> {
4879 cache
4880 .positions_open(
4881 None,
4882 Some(&order.instrument_id()),
4883 Some(&order.strategy_id()),
4884 None,
4885 None,
4886 )
4887 .into_iter()
4888 .find(|position| order.would_reduce_only(position.side, position.quantity))
4889 .map(|position| position.cloned())
4890 }
4891
4892 fn apply_fills(
4893 &mut self,
4894 order: &OrderAny,
4895 fills: &[(Price, Quantity)],
4896 liquidity_side: LiquiditySide,
4897 venue_position_id: Option<PositionId>,
4898 position: Option<&Position>,
4899 protection_price: Option<Price>,
4900 ) -> anyhow::Result<()> {
4901 if order.time_in_force() == TimeInForce::Fok {
4902 let mut total_size = Quantity::zero(order.quantity().precision);
4903
4904 for &(fill_px, fill_qty) in fills {
4905 if self
4906 .normalize_price_for_current_instrument(fill_px)
4907 .is_some()
4908 && let Some(fill_qty) = self.normalize_quantity_for_current_instrument(fill_qty)
4909 {
4910 total_size = total_size.add(fill_qty);
4911 }
4912 }
4913
4914 if order.leaves_qty() > total_size {
4915 self.cancel_order(order, None);
4916 return Ok(());
4917 }
4918 }
4919
4920 if fills.is_empty() {
4921 if order.status() == OrderStatus::Submitted {
4922 self.generate_order_rejected(
4923 order,
4924 format!("No market for {}", order.instrument_id()).into(),
4925 );
4926 } else {
4927 log::error!(
4928 "Cannot fill order: no fills from book when fills were expected (check size in data)"
4929 );
4930 return Ok(());
4931 }
4932 }
4933
4934 let venue_position_id = if self.oms_type == OmsType::Netting {
4936 None
4937 } else {
4938 venue_position_id
4939 };
4940
4941 let mut initial_market_to_limit_fill = false;
4942 let mut total_filled = self
4943 .cached_filled_qty
4944 .get(&order.client_order_id())
4945 .copied()
4946 .unwrap_or_else(|| order.filled_qty());
4947 let initial_total_filled = total_filled;
4948 let mut last_fill_px: Option<Price> = None;
4949 let mut reduce_only_remaining_raw = None;
4950 let mut reduce_only_filled_raw = None;
4951
4952 if self.config.use_reduce_only
4953 && order.is_reduce_only()
4954 && let Some(current_position) = position
4955 {
4956 reduce_only_remaining_raw = Some(current_position.quantity.raw);
4957 reduce_only_filled_raw = Some(total_filled.raw);
4958 }
4959
4960 for &(fill_px, fill_qty) in fills {
4961 let Some(mut fill_px) = self.normalize_fill_price(fill_px, order.client_order_id())
4962 else {
4963 continue;
4964 };
4965
4966 let Some(fill_qty) = self.normalize_fill_quantity(fill_qty, order.client_order_id())
4967 else {
4968 continue;
4969 };
4970
4971 if order.filled_qty() == Quantity::zero(order.filled_qty().precision)
4972 && order.order_type() == OrderType::MarketToLimit
4973 {
4974 self.generate_order_updated(order, order.quantity(), Some(fill_px), None, None);
4975 initial_market_to_limit_fill = true;
4976 }
4977
4978 if self.book_type == BookType::L1_MBP && self.fill_model.is_slipped()? {
4979 fill_px = match order.order_side().as_specified() {
4980 OrderSideSpecified::Buy => fill_px.add(self.instrument.price_increment()),
4981 OrderSideSpecified::Sell => fill_px.sub(self.instrument.price_increment()),
4982 }
4983 }
4984
4985 let mut effective_fill_qty = fill_qty;
4986
4987 if let Some(remaining_raw) = reduce_only_remaining_raw {
4988 if remaining_raw == 0 {
4989 return Ok(());
4990 }
4991
4992 if effective_fill_qty.raw > remaining_raw {
4993 effective_fill_qty =
4994 Quantity::from_raw(remaining_raw, effective_fill_qty.precision);
4995 }
4996 }
4997
4998 if fill_qty.is_zero() {
4999 if fills.len() == 1 && order.status() == OrderStatus::Submitted {
5000 self.generate_order_rejected(
5001 order,
5002 format!("No market for {}", order.instrument_id()).into(),
5003 );
5004 }
5005 return Ok(());
5006 }
5007
5008 let capped_fill_qty = min(
5010 effective_fill_qty,
5011 order.quantity().saturating_sub(total_filled),
5012 );
5013 let reduce_only_exhausts_position = reduce_only_remaining_raw
5014 .is_some_and(|remaining_raw| capped_fill_qty.raw >= remaining_raw);
5015
5016 if reduce_only_exhausts_position {
5017 let reduce_only_target_raw = reduce_only_filled_raw
5018 .unwrap_or(initial_total_filled.raw)
5019 .checked_add(capped_fill_qty.raw)
5020 .expect("Overflow occurred when adding reduce-only target quantity");
5021 let reduce_only_target =
5022 Quantity::from_raw(reduce_only_target_raw, order.quantity().precision);
5023
5024 if order.quantity() != reduce_only_target {
5025 self.generate_order_updated(order, reduce_only_target, None, None, None);
5026 }
5027 }
5028
5029 total_filled = total_filled.add(capped_fill_qty);
5030
5031 if let Some(remaining_raw) = reduce_only_remaining_raw.as_mut() {
5032 *remaining_raw = remaining_raw.saturating_sub(capped_fill_qty.raw);
5033 }
5034
5035 if let Some(filled_raw) = reduce_only_filled_raw.as_mut() {
5036 *filled_raw = filled_raw
5037 .checked_add(capped_fill_qty.raw)
5038 .expect("Overflow occurred when adding reduce-only filled quantity");
5039 }
5040
5041 self.fill_order(
5042 order,
5043 fill_px,
5044 effective_fill_qty,
5045 liquidity_side,
5046 venue_position_id,
5047 position,
5048 )?;
5049 last_fill_px = Some(fill_px);
5050
5051 if order.order_type() == OrderType::MarketToLimit && initial_market_to_limit_fill {
5052 return Ok(());
5054 }
5055
5056 if reduce_only_exhausts_position {
5057 self.purge_cached_filled_qty_if_closed(order.client_order_id());
5058 return Ok(());
5059 }
5060 }
5061
5062 let leaves_remaining = total_filled < order.quantity();
5063 let filled_in_loop = total_filled > initial_total_filled;
5064
5065 if order.time_in_force() == TimeInForce::Ioc && leaves_remaining {
5066 self.cancel_order(order, None);
5067 return Ok(());
5068 }
5069
5070 if leaves_remaining
5073 && (order.is_open() || filled_in_loop)
5074 && self.book_type == BookType::L1_MBP
5075 && matches!(
5076 order.order_type(),
5077 OrderType::Market
5078 | OrderType::MarketIfTouched
5079 | OrderType::StopMarket
5080 | OrderType::TrailingStopMarket
5081 )
5082 {
5083 let Some(last_fill_px) = last_fill_px else {
5085 return Ok(());
5086 };
5087
5088 let side = order.order_side().as_specified();
5089 let slip_fill_px = match side {
5090 OrderSideSpecified::Buy => last_fill_px.add(self.instrument.price_increment()),
5091 OrderSideSpecified::Sell => last_fill_px.sub(self.instrument.price_increment()),
5092 };
5093
5094 if let Some(protection_price) = protection_price {
5095 let exceeds_boundary = match side {
5096 OrderSideSpecified::Buy => slip_fill_px.raw > protection_price.raw,
5097 OrderSideSpecified::Sell => slip_fill_px.raw < protection_price.raw,
5098 };
5099
5100 if exceeds_boundary {
5101 return Ok(());
5102 }
5103 }
5104
5105 let mut leaves_qty = order.quantity().saturating_sub(total_filled);
5106
5107 if let Some(remaining_raw) = reduce_only_remaining_raw {
5108 if remaining_raw == 0 {
5109 return Ok(());
5110 }
5111
5112 if leaves_qty.raw > remaining_raw {
5113 leaves_qty = Quantity::from_raw(remaining_raw, leaves_qty.precision);
5114 }
5115
5116 if leaves_qty.raw >= remaining_raw {
5117 let reduce_only_target_raw = reduce_only_filled_raw
5118 .unwrap_or(initial_total_filled.raw)
5119 .checked_add(leaves_qty.raw)
5120 .expect("Overflow occurred when adding reduce-only target quantity");
5121 let reduce_only_target =
5122 Quantity::from_raw(reduce_only_target_raw, order.quantity().precision);
5123
5124 if order.quantity() != reduce_only_target {
5125 self.generate_order_updated(order, reduce_only_target, None, None, None);
5126 }
5127 }
5128 }
5129
5130 if leaves_qty.is_zero() {
5131 return Ok(());
5132 }
5133
5134 self.fill_order(
5135 order,
5136 slip_fill_px,
5137 leaves_qty,
5138 liquidity_side,
5139 venue_position_id,
5140 position,
5141 )?;
5142 self.purge_cached_filled_qty_if_closed(order.client_order_id());
5143 }
5144
5145 Ok(())
5146 }
5147
5148 fn normalize_fill_price(
5149 &self,
5150 fill_px: Price,
5151 client_order_id: ClientOrderId,
5152 ) -> Option<Price> {
5153 let normalized = self.normalize_price_for_current_instrument(fill_px);
5154 if normalized.is_none() {
5155 log::warn!(
5156 "Skipping fill for {client_order_id}: fill price {fill_px} is not compatible \
5157 with {} price_precision={} price_increment={}",
5158 self.instrument.id(),
5159 self.instrument.price_precision(),
5160 self.instrument.price_increment()
5161 );
5162 }
5163 normalized
5164 }
5165
5166 fn normalize_fill_quantity(
5167 &self,
5168 fill_qty: Quantity,
5169 client_order_id: ClientOrderId,
5170 ) -> Option<Quantity> {
5171 let normalized = self.normalize_quantity_for_current_instrument(fill_qty);
5172 if normalized.is_none() {
5173 log::warn!(
5174 "Skipping fill for {client_order_id}: fill quantity {fill_qty} is not compatible \
5175 with {} size_precision={}",
5176 self.instrument.id(),
5177 self.instrument.size_precision()
5178 );
5179 }
5180 normalized
5181 }
5182
5183 fn fill_order(
5184 &mut self,
5185 order: &OrderAny,
5186 last_px: Price,
5187 last_qty: Quantity,
5188 liquidity_side: LiquiditySide,
5189 venue_position_id: Option<PositionId>,
5190 _position: Option<&Position>,
5191 ) -> anyhow::Result<()> {
5192 self.check_size_precision(last_qty.precision, "fill quantity")?;
5193
5194 let (last_qty, new_filled_qty) =
5195 if let Some(filled_qty) = self.cached_filled_qty.get(&order.client_order_id()) {
5196 let leaves_qty = order.quantity().saturating_sub(*filled_qty);
5197 let last_qty = min(last_qty, leaves_qty);
5198 (last_qty, *filled_qty + last_qty)
5199 } else {
5200 let last_qty = min(last_qty, order.quantity());
5201 (last_qty, last_qty)
5202 };
5203
5204 if last_qty.is_zero() {
5205 return Ok(());
5206 }
5207
5208 let fee_order;
5209 let commission_order = {
5210 let mut cloned = order.clone();
5213 write_filled_qty(&mut cloned, new_filled_qty.saturating_sub(last_qty));
5214 if order.liquidity_side() != Some(liquidity_side) {
5215 cloned.set_liquidity_side(liquidity_side);
5216 }
5217 fee_order = cloned;
5218 &fee_order
5219 };
5220
5221 let underlying_px = self.fee_underlying_price()?;
5222 let commission = self.fee_model.get_commission_with_context(
5223 commission_order,
5224 last_qty,
5225 last_px,
5226 &self.instrument,
5227 underlying_px,
5228 )?;
5229
5230 self.cached_filled_qty
5231 .insert(order.client_order_id(), new_filled_qty);
5232
5233 let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
5234 self.generate_order_filled(
5235 order,
5236 venue_order_id,
5237 venue_position_id,
5238 last_qty,
5239 last_px,
5240 self.instrument.quote_currency(),
5241 commission,
5242 liquidity_side,
5243 );
5244
5245 let post_fill_filled_qty = self
5246 .cached_filled_qty
5247 .get(&order.client_order_id())
5248 .copied()
5249 .unwrap_or(order.filled_qty());
5250 let post_fill_leaves_qty = order.quantity().saturating_sub(post_fill_filled_qty);
5251 let fully_filled = post_fill_leaves_qty.is_zero();
5252
5253 if order.is_closed() || fully_filled {
5254 if self.core.order_exists(order.client_order_id()) {
5255 self.delete_core_order(order.client_order_id());
5256 }
5257 if order.order_type() != OrderType::MarketToLimit {
5260 self.purge_cached_filled_qty_if_closed(order.client_order_id());
5261 }
5262 }
5263
5264 if !self.config.support_contingent_orders {
5265 return Ok(());
5266 }
5267
5268 if let Some(contingency_type) = order.contingency_type() {
5269 match contingency_type {
5270 ContingencyType::Oto => {
5271 if let Some(linked_orders_ids) = order.linked_order_ids() {
5272 for client_order_id in linked_orders_ids {
5273 let mut child_order = match self.cache.borrow().order(client_order_id) {
5274 Some(child_order) => child_order.clone(),
5275 None => anyhow::bail!("Order {client_order_id} not found in cache"),
5276 };
5277
5278 if child_order.is_closed() || child_order.is_active_local() {
5279 continue;
5280 }
5281
5282 if let (None, Some(position_id)) =
5284 (child_order.position_id(), order.position_id())
5285 {
5286 self.cache
5287 .borrow_mut()
5288 .add_position_id(
5289 &position_id,
5290 &self.venue,
5291 client_order_id,
5292 &child_order.strategy_id(),
5293 )
5294 .unwrap();
5295 log::debug!(
5296 "Added position id {position_id} to cache for order {client_order_id}"
5297 );
5298 }
5299
5300 if (!child_order.is_open())
5301 || (matches!(child_order.status(), OrderStatus::PendingUpdate)
5302 && child_order
5303 .previous_status()
5304 .is_some_and(|s| matches!(s, OrderStatus::Submitted)))
5305 {
5306 let account_id = order
5307 .account_id()
5308 .or_else(|| self.account_ids.get(&order.trader_id()).copied())
5309 .ok_or_else(|| {
5310 anyhow::anyhow!(
5311 "Account ID not found for trader {}",
5312 order.trader_id()
5313 )
5314 })?;
5315 self.process_order(&mut child_order, account_id);
5316 }
5317 }
5318 } else {
5319 log::error!(
5320 "OTO order {} does not have linked orders",
5321 order.client_order_id()
5322 );
5323 }
5324 }
5325 ContingencyType::Oco => {
5326 if let Some(linked_orders_ids) = order.linked_order_ids() {
5327 for client_order_id in linked_orders_ids {
5328 let child_order = match self.cache.borrow().order(client_order_id) {
5329 Some(child_order) => child_order.clone(),
5330 None => anyhow::bail!("Order {client_order_id} not found in cache"),
5331 };
5332
5333 if child_order.is_closed() || child_order.is_active_local() {
5334 continue;
5335 }
5336
5337 self.cancel_order(&child_order, None);
5338 }
5339 } else {
5340 log::error!(
5341 "OCO order {} does not have linked orders",
5342 order.client_order_id()
5343 );
5344 }
5345 }
5346 ContingencyType::Ouo => {
5347 if let Some(linked_orders_ids) = order.linked_order_ids() {
5348 for client_order_id in linked_orders_ids {
5349 let child_order = match self.cache.borrow().order(client_order_id) {
5350 Some(child_order) => child_order.clone(),
5351 None => anyhow::bail!("Order {client_order_id} not found in cache"),
5352 };
5353
5354 if child_order.is_active_local() {
5355 continue;
5356 }
5357
5358 let child_filled_qty = self
5359 .cached_filled_qty
5360 .get(&child_order.client_order_id())
5361 .copied()
5362 .unwrap_or(child_order.filled_qty());
5363
5364 if post_fill_leaves_qty.is_zero() && child_order.is_open() {
5365 self.cancel_order(&child_order, None);
5366 } else if child_order.is_open()
5367 && child_filled_qty >= post_fill_leaves_qty
5368 {
5369 self.cancel_order(&child_order, Some(false));
5370 } else if !post_fill_leaves_qty.is_zero()
5371 && post_fill_leaves_qty != child_order.leaves_qty()
5372 {
5373 let price = child_order.price();
5374 let trigger_price = child_order.trigger_price();
5375 self.update_order(
5376 &child_order,
5377 Some(post_fill_leaves_qty),
5378 price,
5379 trigger_price,
5380 Some(false),
5381 );
5382 }
5383 }
5384 } else {
5385 log::error!(
5386 "OUO order {} does not have linked orders",
5387 order.client_order_id()
5388 );
5389 }
5390 }
5391 _ => {}
5392 }
5393 }
5394
5395 Ok(())
5396 }
5397
5398 fn fee_underlying_price(&self) -> CorrectnessResult<Option<Price>> {
5399 if !matches!(
5400 self.instrument,
5401 InstrumentAny::CryptoOption(_) | InstrumentAny::OptionContract(_)
5402 ) {
5403 return Ok(None);
5404 }
5405
5406 let Some(underlying) = self.instrument.underlying() else {
5407 return Ok(None);
5408 };
5409
5410 let underlying_id = InstrumentId::from(format!("{underlying}.{}", self.venue).as_str());
5411 let instrument_id = self.instrument.id();
5412 let cache = self.cache.borrow();
5413 if let Some(price) = cache
5414 .price(&underlying_id, PriceType::Last)
5415 .or_else(|| cache.price(&underlying_id, PriceType::Mark))
5416 .or_else(|| cache.price(&underlying_id, PriceType::Mid))
5417 {
5418 return Ok(Some(price));
5419 }
5420
5421 cache
5422 .option_greeks(&instrument_id)
5423 .and_then(|greeks| greeks.underlying_price)
5424 .map(|price| Price::new_checked(price, FIXED_PRECISION))
5425 .transpose()
5426 }
5427
5428 fn cached_order_is_closed(&self, client_order_id: ClientOrderId) -> bool {
5429 self.cache
5430 .borrow()
5431 .order(&client_order_id)
5432 .is_none_or(|order| order.is_closed())
5433 }
5434
5435 fn purge_cached_filled_qty_if_closed(&mut self, client_order_id: ClientOrderId) {
5436 if self.cached_order_is_closed(client_order_id) {
5437 self.cached_filled_qty.swap_remove(&client_order_id);
5438 }
5439 }
5440
5441 fn purge_closed_cached_filled_qty(&mut self) {
5442 let client_order_ids: Vec<ClientOrderId> = self.cached_filled_qty.keys().copied().collect();
5443
5444 for client_order_id in client_order_ids {
5445 self.purge_cached_filled_qty_if_closed(client_order_id);
5446 }
5447 }
5448
5449 fn update_limit_order(
5450 &mut self,
5451 order: &OrderAny,
5452 quantity: Quantity,
5453 price: Price,
5454 ) -> ModifyOutcome {
5455 if self
5456 .core
5457 .is_limit_matched(order.order_side_specified(), price)
5458 {
5459 if order.is_post_only() {
5460 self.generate_order_modify_rejected(
5461 order.trader_id(),
5462 order.strategy_id(),
5463 order.instrument_id(),
5464 order.client_order_id(),
5465 Ustr::from(format!(
5466 "POST_ONLY {} {} order with new limit px of {} would have been a TAKER: bid={}, ask={}",
5467 order.order_type(),
5468 order.order_side(),
5469 price,
5470 self.core.bid.map_or_else(|| "None".to_string(), |p| p.to_string()),
5471 self.core.ask.map_or_else(|| "None".to_string(), |p| p.to_string())
5472 ).as_str()),
5473 order.venue_order_id(),
5474 order.account_id(),
5475 );
5476 return ModifyOutcome::Rejected;
5477 }
5478
5479 self.generate_order_updated(order, quantity, Some(price), None, None);
5480
5481 let client_order_id = order.client_order_id();
5483 if let Some(mut order) = self.cache.borrow_mut().order_mut(&client_order_id) {
5484 order.set_liquidity_side(LiquiditySide::Taker);
5485 }
5486 self.fill_limit_order(client_order_id);
5487 return ModifyOutcome::Applied;
5488 }
5489 self.generate_order_updated(order, quantity, Some(price), None, None);
5490 ModifyOutcome::Applied
5491 }
5492
5493 fn update_stop_market_order(
5494 &self,
5495 order: &OrderAny,
5496 quantity: Quantity,
5497 trigger_price: Price,
5498 ) -> ModifyOutcome {
5499 if self.core.is_stop_matched_with_trigger_type(
5500 order.order_side_specified(),
5501 trigger_price,
5502 order.trigger_type().unwrap_or(TriggerType::Default),
5503 ) {
5504 self.generate_order_modify_rejected(
5505 order.trader_id(),
5506 order.strategy_id(),
5507 order.instrument_id(),
5508 order.client_order_id(),
5509 Ustr::from(
5510 format!(
5511 "{} {} order new stop px of {} was in the market: bid={}, ask={}",
5512 order.order_type(),
5513 order.order_side(),
5514 trigger_price,
5515 self.core
5516 .bid
5517 .map_or_else(|| "None".to_string(), |p| p.to_string()),
5518 self.core
5519 .ask
5520 .map_or_else(|| "None".to_string(), |p| p.to_string())
5521 )
5522 .as_str(),
5523 ),
5524 order.venue_order_id(),
5525 order.account_id(),
5526 );
5527 return ModifyOutcome::Rejected;
5528 }
5529
5530 self.generate_order_updated(order, quantity, None, Some(trigger_price), None);
5531 ModifyOutcome::Applied
5532 }
5533
5534 fn update_stop_limit_order(
5535 &mut self,
5536 order: &OrderAny,
5537 quantity: Quantity,
5538 price: Price,
5539 trigger_price: Price,
5540 ) -> ModifyOutcome {
5541 if order.is_triggered().is_some_and(|t| t) {
5542 if self
5544 .core
5545 .is_limit_matched(order.order_side_specified(), price)
5546 {
5547 if order.is_post_only() {
5548 self.generate_order_modify_rejected(
5549 order.trader_id(),
5550 order.strategy_id(),
5551 order.instrument_id(),
5552 order.client_order_id(),
5553 Ustr::from(format!(
5554 "POST_ONLY {} {} order with new limit px of {} would have been a TAKER: bid={}, ask={}",
5555 order.order_type(),
5556 order.order_side(),
5557 price,
5558 self.core.bid.map_or_else(|| "None".to_string(), |p| p.to_string()),
5559 self.core.ask.map_or_else(|| "None".to_string(), |p| p.to_string())
5560 ).as_str()),
5561 order.venue_order_id(),
5562 order.account_id(),
5563 );
5564 return ModifyOutcome::Rejected;
5565 }
5566 self.generate_order_updated(order, quantity, Some(price), None, None);
5567
5568 let client_order_id = order.client_order_id();
5570 if let Some(mut order) = self.cache.borrow_mut().order_mut(&client_order_id) {
5571 order.set_liquidity_side(LiquiditySide::Taker);
5572 }
5573 self.fill_limit_order(client_order_id);
5574 return ModifyOutcome::Applied;
5575 }
5576 } else {
5577 if self.core.is_stop_matched_with_trigger_type(
5579 order.order_side_specified(),
5580 trigger_price,
5581 order.trigger_type().unwrap_or(TriggerType::Default),
5582 ) {
5583 self.generate_order_modify_rejected(
5584 order.trader_id(),
5585 order.strategy_id(),
5586 order.instrument_id(),
5587 order.client_order_id(),
5588 Ustr::from(
5589 format!(
5590 "{} {} order new stop px of {} was in the market: bid={}, ask={}",
5591 order.order_type(),
5592 order.order_side(),
5593 trigger_price,
5594 self.core
5595 .bid
5596 .map_or_else(|| "None".to_string(), |p| p.to_string()),
5597 self.core
5598 .ask
5599 .map_or_else(|| "None".to_string(), |p| p.to_string())
5600 )
5601 .as_str(),
5602 ),
5603 order.venue_order_id(),
5604 order.account_id(),
5605 );
5606 return ModifyOutcome::Rejected;
5607 }
5608 }
5609
5610 self.generate_order_updated(order, quantity, Some(price), Some(trigger_price), None);
5611 ModifyOutcome::Applied
5612 }
5613
5614 fn update_market_if_touched_order(
5615 &self,
5616 order: &OrderAny,
5617 quantity: Quantity,
5618 trigger_price: Price,
5619 ) -> ModifyOutcome {
5620 if self.core.is_touch_triggered_with_trigger_type(
5621 order.order_side_specified(),
5622 trigger_price,
5623 order.trigger_type().unwrap_or(TriggerType::Default),
5624 ) {
5625 self.generate_order_modify_rejected(
5626 order.trader_id(),
5627 order.strategy_id(),
5628 order.instrument_id(),
5629 order.client_order_id(),
5630 Ustr::from(
5631 format!(
5632 "{} {} order new trigger px of {} was in the market: bid={}, ask={}",
5633 order.order_type(),
5634 order.order_side(),
5635 trigger_price,
5636 self.core
5637 .bid
5638 .map_or_else(|| "None".to_string(), |p| p.to_string()),
5639 self.core
5640 .ask
5641 .map_or_else(|| "None".to_string(), |p| p.to_string())
5642 )
5643 .as_str(),
5644 ),
5645 order.venue_order_id(),
5646 order.account_id(),
5647 );
5648 return ModifyOutcome::Rejected;
5650 }
5651
5652 self.generate_order_updated(order, quantity, None, Some(trigger_price), None);
5653 ModifyOutcome::Applied
5654 }
5655
5656 fn update_limit_if_touched_order(
5657 &mut self,
5658 order: &OrderAny,
5659 quantity: Quantity,
5660 price: Price,
5661 trigger_price: Price,
5662 ) -> ModifyOutcome {
5663 if order.is_triggered().is_some_and(|t| t) {
5664 if self
5666 .core
5667 .is_limit_matched(order.order_side_specified(), price)
5668 {
5669 if order.is_post_only() {
5670 self.generate_order_modify_rejected(
5671 order.trader_id(),
5672 order.strategy_id(),
5673 order.instrument_id(),
5674 order.client_order_id(),
5675 Ustr::from(format!(
5676 "POST_ONLY {} {} order with new limit px of {} would have been a TAKER: bid={}, ask={}",
5677 order.order_type(),
5678 order.order_side(),
5679 price,
5680 self.core.bid.map_or_else(|| "None".to_string(), |p| p.to_string()),
5681 self.core.ask.map_or_else(|| "None".to_string(), |p| p.to_string())
5682 ).as_str()),
5683 order.venue_order_id(),
5684 order.account_id(),
5685 );
5686 return ModifyOutcome::Rejected;
5688 }
5689 self.generate_order_updated(order, quantity, Some(price), None, None);
5690
5691 let client_order_id = order.client_order_id();
5693 if let Some(mut order) = self.cache.borrow_mut().order_mut(&client_order_id) {
5694 order.set_liquidity_side(LiquiditySide::Taker);
5695 }
5696 self.fill_limit_order(client_order_id);
5697 return ModifyOutcome::Applied;
5698 }
5699 } else {
5700 if self.core.is_touch_triggered_with_trigger_type(
5702 order.order_side_specified(),
5703 trigger_price,
5704 order.trigger_type().unwrap_or(TriggerType::Default),
5705 ) {
5706 self.generate_order_modify_rejected(
5707 order.trader_id(),
5708 order.strategy_id(),
5709 order.instrument_id(),
5710 order.client_order_id(),
5711 Ustr::from(
5712 format!(
5713 "{} {} order new trigger px of {} was in the market: bid={}, ask={}",
5714 order.order_type(),
5715 order.order_side(),
5716 trigger_price,
5717 self.core
5718 .bid
5719 .map_or_else(|| "None".to_string(), |p| p.to_string()),
5720 self.core
5721 .ask
5722 .map_or_else(|| "None".to_string(), |p| p.to_string())
5723 )
5724 .as_str(),
5725 ),
5726 order.venue_order_id(),
5727 order.account_id(),
5728 );
5729 return ModifyOutcome::Rejected;
5730 }
5731 }
5732
5733 self.generate_order_updated(order, quantity, Some(price), Some(trigger_price), None);
5734 ModifyOutcome::Applied
5735 }
5736
5737 fn update_trailing_stop_order(&self, order: &OrderAny) {
5738 let (new_trigger_price, new_price) = match trailing_stop_calculate(
5739 self.instrument.price_increment(),
5740 order.trigger_price(),
5741 order,
5742 self.core.bid,
5743 self.core.ask,
5744 self.core.last,
5745 ) {
5746 Ok(prices) => prices,
5747 Err(e) => {
5748 log::debug!("Cannot calculate trailing-stop update: {e}");
5750 return;
5751 }
5752 };
5753
5754 if new_trigger_price.is_none() && new_price.is_none() {
5755 return;
5756 }
5757
5758 self.generate_order_updated(order, order.quantity(), new_price, new_trigger_price, None);
5759 }
5760
5761 fn accept_order(&mut self, order: &mut OrderAny) {
5762 if order.is_closed() {
5763 return;
5765 }
5766
5767 if order.status() != OrderStatus::Accepted {
5768 let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
5769 let event = self.create_order_accepted(order, venue_order_id);
5770 if let Err(e) = order.apply(event.clone()) {
5773 log::warn!(
5774 "Skipping local apply of accepted event for {}: {e}",
5775 order.client_order_id(),
5776 );
5777 }
5778 self.dispatch_order_event(event);
5779
5780 if matches!(
5783 order.order_type(),
5784 OrderType::TrailingStopLimit | OrderType::TrailingStopMarket
5785 ) && order.trigger_price().is_none()
5786 && self.maybe_activate_trailing_stop(
5787 order,
5788 self.core.bid,
5789 self.core.ask,
5790 self.core.last,
5791 )
5792 {
5793 self.update_trailing_stop_order(order);
5794 }
5795 }
5796
5797 let match_info = Self::matching_core_entry(order);
5798 self.track_post_match_order(order);
5799 self.core.add_order(match_info);
5800 }
5801
5802 fn track_post_match_order(&mut self, order: &OrderAny) {
5803 self.post_match_order_ids.insert(order.client_order_id());
5804 }
5805
5806 fn delete_core_order(&mut self, client_order_id: ClientOrderId) {
5807 self.post_match_order_ids.swap_remove(&client_order_id);
5808 let _ = self.core.delete_order(client_order_id);
5809 }
5810
5811 fn requires_post_match_maintenance(order: &OrderAny) -> bool {
5812 order.expire_time().is_some()
5813 || matches!(
5814 order.order_type(),
5815 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
5816 )
5817 }
5818
5819 fn matching_core_entry(order: &OrderAny) -> RestingOrder {
5820 let triggered_limit_style = matches!(
5821 order.order_type(),
5822 OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit
5823 ) && order.is_triggered().is_some_and(|triggered| triggered);
5824
5825 RestingOrder::new_with_trigger_type(
5826 order.client_order_id(),
5827 order.order_side().as_specified(),
5828 order.order_type(),
5829 order.trigger_type().unwrap_or(TriggerType::Default),
5830 if triggered_limit_style {
5831 None
5832 } else {
5833 order.trigger_price()
5834 },
5835 order.price(),
5836 match order {
5837 OrderAny::TrailingStopMarket(o) => o.is_activated,
5838 OrderAny::TrailingStopLimit(o) => o.is_activated,
5839 _ => true,
5840 },
5841 )
5842 }
5843
5844 fn expire_order(&mut self, order: &OrderAny) {
5845 if self.config.support_contingent_orders
5846 && order
5847 .contingency_type()
5848 .is_some_and(|c| c != ContingencyType::NoContingency)
5849 {
5850 self.cancel_contingent_orders(order);
5851 }
5852
5853 self.generate_order_expired(order);
5854 }
5855
5856 fn cancel_order(&mut self, order: &OrderAny, cancel_contingencies: Option<bool>) {
5857 let cancel_contingencies = cancel_contingencies.unwrap_or(true);
5858
5859 if order.is_active_local()
5860 && !matches!(
5861 (order.status(), order.order_type(), order.time_in_force()),
5862 (
5863 OrderStatus::Initialized | OrderStatus::Released,
5864 OrderType::Market,
5865 TimeInForce::Ioc | TimeInForce::Fok
5866 )
5867 )
5868 {
5869 log::error!(
5870 "Cannot cancel an order with {} from the matching engine",
5871 order.status()
5872 );
5873 return;
5874 }
5875
5876 if self.core.order_exists(order.client_order_id()) {
5878 self.delete_core_order(order.client_order_id());
5879 }
5880 self.cached_filled_qty.swap_remove(&order.client_order_id());
5881
5882 let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
5883 self.generate_order_canceled(order, venue_order_id);
5884
5885 if self.config.support_contingent_orders
5886 && order.contingency_type().is_some()
5887 && order.contingency_type().unwrap() != ContingencyType::NoContingency
5888 && cancel_contingencies
5889 {
5890 self.cancel_contingent_orders(order);
5891 }
5892 }
5893
5894 fn update_order(
5895 &mut self,
5896 order: &OrderAny,
5897 quantity: Option<Quantity>,
5898 price: Option<Price>,
5899 trigger_price: Option<Price>,
5900 update_contingencies: Option<bool>,
5901 ) -> bool {
5902 let update_contingencies = update_contingencies.unwrap_or(true);
5903 let quantity = quantity.unwrap_or(order.quantity());
5904
5905 let price_prec = self.instrument.price_precision();
5906 let size_prec = self.instrument.size_precision();
5907 let instrument_id = self.instrument.id();
5908
5909 if quantity.precision != size_prec {
5910 self.generate_order_modify_rejected(
5911 order.trader_id(),
5912 order.strategy_id(),
5913 order.instrument_id(),
5914 order.client_order_id(),
5915 Ustr::from(&format!(
5916 "Invalid update quantity precision {}, expected {size_prec} for {instrument_id}",
5917 quantity.precision
5918 )),
5919 order.venue_order_id(),
5920 order.account_id(),
5921 );
5922 return false;
5923 }
5924
5925 if let Some(px) = price
5926 && px.precision != price_prec
5927 {
5928 self.generate_order_modify_rejected(
5929 order.trader_id(),
5930 order.strategy_id(),
5931 order.instrument_id(),
5932 order.client_order_id(),
5933 Ustr::from(&format!(
5934 "Invalid update price precision {}, expected {price_prec} for {instrument_id}",
5935 px.precision
5936 )),
5937 order.venue_order_id(),
5938 order.account_id(),
5939 );
5940 return false;
5941 }
5942
5943 if let Some(tp) = trigger_price
5944 && tp.precision != price_prec
5945 {
5946 self.generate_order_modify_rejected(
5947 order.trader_id(),
5948 order.strategy_id(),
5949 order.instrument_id(),
5950 order.client_order_id(),
5951 Ustr::from(&format!(
5952 "Invalid update trigger_price precision {}, expected {price_prec} for {instrument_id}",
5953 tp.precision
5954 )),
5955 order.venue_order_id(),
5956 order.account_id(),
5957 );
5958 return false;
5959 }
5960
5961 let filled_qty = self
5963 .cached_filled_qty
5964 .get(&order.client_order_id())
5965 .copied()
5966 .unwrap_or(order.filled_qty());
5967 if quantity < filled_qty {
5968 self.generate_order_modify_rejected(
5969 order.trader_id(),
5970 order.strategy_id(),
5971 order.instrument_id(),
5972 order.client_order_id(),
5973 Ustr::from(&format!(
5974 "Cannot reduce order quantity {quantity} below filled quantity {filled_qty}",
5975 )),
5976 order.venue_order_id(),
5977 order.account_id(),
5978 );
5979 return false;
5980 }
5981
5982 let outcome = match order {
5983 OrderAny::Limit(_) | OrderAny::MarketToLimit(_) => {
5984 let price = price.unwrap_or(order.price().unwrap());
5985 self.update_limit_order(order, quantity, price)
5986 }
5987 OrderAny::StopMarket(_) => {
5988 let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
5989 self.update_stop_market_order(order, quantity, trigger_price)
5990 }
5991 OrderAny::StopLimit(_) => {
5992 let price = price.unwrap_or(order.price().unwrap());
5993 let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
5994 self.update_stop_limit_order(order, quantity, price, trigger_price)
5995 }
5996 OrderAny::MarketIfTouched(_) => {
5997 let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
5998 self.update_market_if_touched_order(order, quantity, trigger_price)
5999 }
6000 OrderAny::LimitIfTouched(_) => {
6001 let price = price.unwrap_or(order.price().unwrap());
6002 let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
6003 self.update_limit_if_touched_order(order, quantity, price, trigger_price)
6004 }
6005 OrderAny::TrailingStopMarket(_) => {
6006 if let Some(trigger_price) = trigger_price.or(order.trigger_price()) {
6007 self.update_market_if_touched_order(order, quantity, trigger_price)
6008 } else {
6009 self.generate_order_updated(order, quantity, None, trigger_price, None);
6010 ModifyOutcome::Applied
6011 }
6012 }
6013 OrderAny::TrailingStopLimit(_) => {
6014 match (
6015 price.or(order.price()),
6016 trigger_price.or(order.trigger_price()),
6017 ) {
6018 (Some(price), Some(trigger_price)) => {
6019 self.update_limit_if_touched_order(order, quantity, price, trigger_price)
6020 }
6021 _ => {
6022 self.generate_order_updated(order, quantity, price, trigger_price, None);
6023 ModifyOutcome::Applied
6024 }
6025 }
6026 }
6027 _ => {
6028 panic!(
6029 "Unsupported order type {} for update_order",
6030 order.order_type()
6031 );
6032 }
6033 };
6034
6035 if outcome == ModifyOutcome::Rejected {
6036 return false;
6037 }
6038
6039 let new_leaves_qty = quantity.saturating_sub(filled_qty);
6041 if new_leaves_qty.is_zero() {
6042 if self.config.support_contingent_orders
6043 && order
6044 .contingency_type()
6045 .is_some_and(|c| c != ContingencyType::NoContingency)
6046 && update_contingencies
6047 {
6048 self.update_contingent_order(order, quantity);
6049 }
6050 self.cancel_order(order, Some(false));
6052 return true;
6053 }
6054
6055 if self.config.support_contingent_orders
6056 && order
6057 .contingency_type()
6058 .is_some_and(|c| c != ContingencyType::NoContingency)
6059 && update_contingencies
6060 {
6061 self.update_contingent_order(order, quantity);
6062 }
6063
6064 true
6065 }
6066
6067 pub fn trigger_stop_order(&mut self, client_order_id: ClientOrderId) {
6069 let order = match self
6070 .cache
6071 .borrow()
6072 .order(&client_order_id)
6073 .map(|o| o.clone())
6074 {
6075 Some(order) => order,
6076 None => {
6077 log::error!(
6078 "Cannot trigger stop order: order {client_order_id} not found in cache"
6079 );
6080 return;
6081 }
6082 };
6083
6084 if order.is_closed() {
6085 log::debug!("Cannot trigger stop order: {client_order_id} already closed");
6086 return;
6087 }
6088
6089 match order.order_type() {
6090 OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit => {
6091 self.trigger_limit_style_stop_order(client_order_id, order);
6092 }
6093 OrderType::StopMarket | OrderType::MarketIfTouched | OrderType::TrailingStopMarket => {
6094 self.fill_market_order(client_order_id);
6095 }
6096 _ => {
6097 log::error!(
6098 "Cannot trigger stop order: invalid order type {}",
6099 order.order_type()
6100 );
6101 }
6102 }
6103 }
6104
6105 fn trigger_limit_style_stop_order(&mut self, client_order_id: ClientOrderId, order: OrderAny) {
6106 if order.is_triggered().is_some_and(|triggered| triggered) {
6107 let liquidity_side = match (order.price(), order.trigger_price()) {
6108 (Some(price), Some(trigger_price)) => Self::determine_triggered_limit_liquidity(
6109 order.order_side(),
6110 price,
6111 trigger_price,
6112 ),
6113 _ => LiquiditySide::Maker,
6114 };
6115
6116 if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id)
6117 && !matches!(
6118 cached_order.liquidity_side(),
6119 Some(LiquiditySide::Maker | LiquiditySide::Taker)
6120 )
6121 {
6122 cached_order.set_liquidity_side(liquidity_side);
6123 }
6124 self.fill_limit_order(client_order_id);
6125 return;
6126 }
6127
6128 let event = self.create_order_triggered(&order);
6129 let order = match self.cache.borrow_mut().update_order(&event) {
6130 Ok(order) => order,
6131 Err(e) => {
6132 log::debug!(
6133 "Failed to apply triggered event for {} before fill: {e}",
6134 order.client_order_id(),
6135 );
6136 order
6137 }
6138 };
6139 self.dispatch_order_event(event);
6140
6141 let trigger_price = order
6142 .trigger_price()
6143 .expect("Limit-style stop order must have a trigger price");
6144 let price = order
6145 .price()
6146 .expect("Limit-style stop order must have a price");
6147
6148 let maker_inside = match order.order_side() {
6149 OrderSide::Buy => self
6150 .core
6151 .ask
6152 .is_some_and(|ask| trigger_price > price && price > ask),
6153 OrderSide::Sell => self
6154 .core
6155 .bid
6156 .is_some_and(|bid| trigger_price < price && price < bid),
6157 OrderSide::NoOrderSide => false,
6158 };
6159
6160 if maker_inside {
6161 if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6162 cached_order.set_liquidity_side(LiquiditySide::Maker);
6163 }
6164 self.resync_core_entry(client_order_id);
6165 self.fill_limit_order(client_order_id);
6166 return;
6167 }
6168
6169 if self
6170 .core
6171 .is_limit_matched(order.order_side_specified(), price)
6172 {
6173 if order.is_post_only() {
6174 self.delete_core_order(client_order_id);
6175 self.cached_filled_qty.swap_remove(&client_order_id);
6176 let event = self.create_order_rejected(
6177 &order,
6178 format!(
6179 "POST_ONLY {} {} order limit px of {} would have been a TAKER: bid={}, ask={}",
6180 order.order_type(),
6181 order.order_side(),
6182 price,
6183 self.core
6184 .bid
6185 .map_or_else(|| "None".to_string(), |p| p.to_string()),
6186 self.core
6187 .ask
6188 .map_or_else(|| "None".to_string(), |p| p.to_string())
6189 )
6190 .into(),
6191 );
6192
6193 if let Err(e) = self.cache.borrow_mut().update_order(&event) {
6194 log::debug!(
6195 "Failed to apply rejected event for {} after post-only trigger: {e}",
6196 order.client_order_id(),
6197 );
6198 }
6199 self.dispatch_order_event(event);
6200 return;
6201 }
6202
6203 if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6204 cached_order.set_liquidity_side(LiquiditySide::Taker);
6205 }
6206 self.resync_core_entry(client_order_id);
6207 self.fill_limit_order(client_order_id);
6208 return;
6209 }
6210
6211 if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6212 cached_order.set_liquidity_side(Self::determine_triggered_limit_liquidity(
6213 order.order_side(),
6214 price,
6215 trigger_price,
6216 ));
6217 }
6218 self.resync_core_entry(client_order_id);
6219 }
6220
6221 fn determine_triggered_limit_liquidity(
6222 side: OrderSide,
6223 price: Price,
6224 trigger_price: Price,
6225 ) -> LiquiditySide {
6226 if (side == OrderSide::Buy && trigger_price > price)
6227 || (side == OrderSide::Sell && trigger_price < price)
6228 {
6229 LiquiditySide::Maker
6230 } else {
6231 LiquiditySide::Taker
6232 }
6233 }
6234
6235 fn update_contingent_order(&mut self, order: &OrderAny, parent_quantity: Quantity) {
6236 log::debug!(
6237 "Updating contingent orders from {}",
6238 order.client_order_id()
6239 );
6240
6241 if let Some(linked_order_ids) = order.linked_order_ids() {
6242 let parent_filled_qty = self
6243 .cached_filled_qty
6244 .get(&order.client_order_id())
6245 .copied()
6246 .unwrap_or(order.filled_qty());
6247 let parent_leaves_qty = parent_quantity.saturating_sub(parent_filled_qty);
6248
6249 for client_order_id in linked_order_ids {
6250 let child_order = match self.cache.borrow().order(client_order_id) {
6251 Some(order) => order.clone(),
6252 None => panic!("Order {client_order_id} not found in cache."),
6253 };
6254
6255 if child_order.is_active_local() {
6256 continue;
6257 }
6258
6259 let child_filled_qty = self
6260 .cached_filled_qty
6261 .get(&child_order.client_order_id())
6262 .copied()
6263 .unwrap_or(child_order.filled_qty());
6264
6265 if parent_leaves_qty.is_zero() {
6266 self.cancel_order(&child_order, Some(false));
6267 } else if child_filled_qty >= parent_leaves_qty {
6268 self.cancel_order(&child_order, Some(false));
6270 } else {
6271 let child_leaves_qty = child_order.quantity().saturating_sub(child_filled_qty);
6272 if child_leaves_qty != parent_leaves_qty {
6273 let price = child_order.price();
6274 let trigger_price = child_order.trigger_price();
6275 self.update_order(
6276 &child_order,
6277 Some(parent_leaves_qty),
6278 price,
6279 trigger_price,
6280 Some(false),
6281 );
6282 }
6283 }
6284 }
6285 }
6286 }
6287
6288 fn cancel_contingent_orders(&mut self, order: &OrderAny) {
6289 if let Some(linked_order_ids) = order.linked_order_ids() {
6290 for client_order_id in linked_order_ids {
6291 let contingent_order = match self.cache.borrow().order(client_order_id) {
6292 Some(order) => order.clone(),
6293 None => panic!("Cannot find contingent order for {client_order_id}"),
6294 };
6295
6296 if contingent_order.is_active_local() {
6297 continue;
6299 }
6300
6301 if !contingent_order.is_closed() {
6302 self.cancel_order(&contingent_order, Some(false));
6303 }
6304 }
6305 }
6306 }
6307
6308 fn generate_order_submitted(&self, order: &OrderAny, account_id: AccountId) {
6309 let ts_now = self.clock.borrow().timestamp_ns();
6310 let event = OrderEventAny::Submitted(OrderSubmitted::new(
6311 order.trader_id(),
6312 order.strategy_id(),
6313 order.instrument_id(),
6314 order.client_order_id(),
6315 account_id,
6316 UUID4::new(),
6317 ts_now,
6318 ts_now,
6319 ));
6320 self.dispatch_order_event(event);
6321 }
6322
6323 fn create_order_rejected(&self, order: &OrderAny, reason: Ustr) -> OrderEventAny {
6324 let ts_now = self.clock.borrow().timestamp_ns();
6325 let account_id = order
6326 .account_id()
6327 .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
6328
6329 let due_post_only = reason.as_str().starts_with("POST_ONLY");
6330
6331 OrderEventAny::Rejected(OrderRejected::new(
6332 order.trader_id(),
6333 order.strategy_id(),
6334 order.instrument_id(),
6335 order.client_order_id(),
6336 account_id,
6337 reason,
6338 UUID4::new(),
6339 ts_now,
6340 ts_now,
6341 false,
6342 due_post_only,
6343 ))
6344 }
6345
6346 fn generate_order_rejected(&self, order: &OrderAny, reason: Ustr) {
6347 let event = self.create_order_rejected(order, reason);
6348 self.dispatch_order_event(event);
6349 }
6350
6351 fn publish_order_initialized(&self, order: &OrderAny) {
6352 let event = OrderEventAny::Initialized(order.init_event().clone());
6353 msgbus::publish_order_event(
6354 format!("events.order.{}", order.strategy_id()).into(),
6355 &event,
6356 );
6357 }
6358
6359 fn create_order_accepted(
6360 &self,
6361 order: &OrderAny,
6362 venue_order_id: VenueOrderId,
6363 ) -> OrderEventAny {
6364 let ts_now = self.clock.borrow().timestamp_ns();
6365 let account_id = order
6366 .account_id()
6367 .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
6368 OrderEventAny::Accepted(OrderAccepted::new(
6369 order.trader_id(),
6370 order.strategy_id(),
6371 order.instrument_id(),
6372 order.client_order_id(),
6373 venue_order_id,
6374 account_id,
6375 UUID4::new(),
6376 ts_now,
6377 ts_now,
6378 false,
6379 ))
6380 }
6381
6382 fn generate_order_accepted(&self, order: &OrderAny, venue_order_id: VenueOrderId) {
6383 let event = self.create_order_accepted(order, venue_order_id);
6384 self.dispatch_order_event(event);
6385 }
6386
6387 #[expect(clippy::too_many_arguments)]
6388 fn generate_order_modify_rejected(
6389 &self,
6390 trader_id: TraderId,
6391 strategy_id: StrategyId,
6392 instrument_id: InstrumentId,
6393 client_order_id: ClientOrderId,
6394 reason: Ustr,
6395 venue_order_id: Option<VenueOrderId>,
6396 account_id: Option<AccountId>,
6397 ) {
6398 let ts_now = self.clock.borrow().timestamp_ns();
6399 let event = OrderEventAny::ModifyRejected(OrderModifyRejected::new(
6400 trader_id,
6401 strategy_id,
6402 instrument_id,
6403 client_order_id,
6404 reason,
6405 UUID4::new(),
6406 ts_now,
6407 ts_now,
6408 false,
6409 venue_order_id,
6410 account_id,
6411 ));
6412 self.dispatch_order_event(event);
6413 }
6414
6415 #[expect(clippy::too_many_arguments)]
6416 fn generate_order_cancel_rejected(
6417 &self,
6418 trader_id: TraderId,
6419 strategy_id: StrategyId,
6420 account_id: AccountId,
6421 instrument_id: InstrumentId,
6422 client_order_id: ClientOrderId,
6423 venue_order_id: Option<VenueOrderId>,
6424 reason: Ustr,
6425 ) {
6426 let ts_now = self.clock.borrow().timestamp_ns();
6427 let event = OrderEventAny::CancelRejected(OrderCancelRejected::new(
6428 trader_id,
6429 strategy_id,
6430 instrument_id,
6431 client_order_id,
6432 reason,
6433 UUID4::new(),
6434 ts_now,
6435 ts_now,
6436 false,
6437 venue_order_id,
6438 Some(account_id),
6439 ));
6440 self.dispatch_order_event(event);
6441 }
6442
6443 fn generate_order_updated(
6444 &self,
6445 order: &OrderAny,
6446 quantity: Quantity,
6447 price: Option<Price>,
6448 trigger_price: Option<Price>,
6449 protection_price: Option<Price>,
6450 ) {
6451 let ts_now = self.clock.borrow().timestamp_ns();
6452 let event = OrderEventAny::Updated(OrderUpdated::new(
6453 order.trader_id(),
6454 order.strategy_id(),
6455 order.instrument_id(),
6456 order.client_order_id(),
6457 quantity,
6458 UUID4::new(),
6459 ts_now,
6460 ts_now,
6461 false,
6462 order.venue_order_id(),
6463 order.account_id(),
6464 price,
6465 trigger_price,
6466 protection_price,
6467 order.is_quote_quantity(),
6468 ));
6469
6470 self.dispatch_order_event(event);
6471 }
6472
6473 fn generate_order_canceled(&self, order: &OrderAny, venue_order_id: VenueOrderId) {
6474 let ts_now = self.clock.borrow().timestamp_ns();
6475 let event = OrderEventAny::Canceled(OrderCanceled::new(
6476 order.trader_id(),
6477 order.strategy_id(),
6478 order.instrument_id(),
6479 order.client_order_id(),
6480 UUID4::new(),
6481 ts_now,
6482 ts_now,
6483 false,
6484 Some(venue_order_id),
6485 order.account_id(),
6486 ));
6487 self.dispatch_order_event(event);
6488 }
6489
6490 fn create_order_triggered(&self, order: &OrderAny) -> OrderEventAny {
6491 let ts_now = self.clock.borrow().timestamp_ns();
6492 OrderEventAny::Triggered(OrderTriggered::new(
6493 order.trader_id(),
6494 order.strategy_id(),
6495 order.instrument_id(),
6496 order.client_order_id(),
6497 UUID4::new(),
6498 ts_now,
6499 ts_now,
6500 false,
6501 order.venue_order_id(),
6502 order.account_id(),
6503 ))
6504 }
6505
6506 fn generate_order_expired(&self, order: &OrderAny) {
6507 let ts_now = self.clock.borrow().timestamp_ns();
6508 let event = OrderEventAny::Expired(OrderExpired::new(
6509 order.trader_id(),
6510 order.strategy_id(),
6511 order.instrument_id(),
6512 order.client_order_id(),
6513 UUID4::new(),
6514 ts_now,
6515 ts_now,
6516 false,
6517 order.venue_order_id(),
6518 order.account_id(),
6519 ));
6520 self.dispatch_order_event(event);
6521 }
6522
6523 #[expect(clippy::too_many_arguments)]
6524 fn generate_order_filled(
6525 &mut self,
6526 order: &OrderAny,
6527 venue_order_id: VenueOrderId,
6528 venue_position_id: Option<PositionId>,
6529 last_qty: Quantity,
6530 last_px: Price,
6531 quote_currency: Currency,
6532 commission: Money,
6533 liquidity_side: LiquiditySide,
6534 ) {
6535 debug_assert!(
6536 last_qty <= order.quantity(),
6537 "Fill quantity {last_qty} exceeds order quantity {order_qty} for {client_order_id}",
6538 order_qty = order.quantity(),
6539 client_order_id = order.client_order_id()
6540 );
6541
6542 let ts_now = self.clock.borrow().timestamp_ns();
6543 let account_id = order
6544 .account_id()
6545 .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
6546 let event = OrderEventAny::Filled(OrderFilled::new(
6547 order.trader_id(),
6548 order.strategy_id(),
6549 order.instrument_id(),
6550 order.client_order_id(),
6551 venue_order_id,
6552 account_id,
6553 self.ids_generator.generate_trade_id(ts_now),
6554 order.order_side(),
6555 order.order_type(),
6556 last_qty,
6557 last_px,
6558 quote_currency,
6559 liquidity_side,
6560 UUID4::new(),
6561 ts_now,
6562 ts_now,
6563 false,
6564 venue_position_id,
6565 Some(commission),
6566 None,
6567 ));
6568
6569 self.dispatch_order_event(event);
6570 }
6571}
6572
6573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6574enum ModifyOutcome {
6575 Applied,
6576 Rejected,
6577}
6578
6579#[derive(Debug)]
6580enum PostMatchOrderAction {
6581 RemoveClosed,
6582 Expire(OrderAny),
6583 UpdateTrailing(OrderAny),
6584 NoMaintenance,
6585}
6586
6587fn post_match_order_action<F>(
6588 order: &OrderAny,
6589 support_gtd_orders: bool,
6590 timestamp_ns: UnixNanos,
6591 clone_order: F,
6592) -> PostMatchOrderAction
6593where
6594 F: FnOnce(&OrderAny) -> OrderAny,
6595{
6596 if order.is_closed() {
6597 PostMatchOrderAction::RemoveClosed
6598 } else if support_gtd_orders
6599 && order
6600 .expire_time()
6601 .is_some_and(|expire_ns| timestamp_ns >= expire_ns)
6602 {
6603 PostMatchOrderAction::Expire(clone_order(order))
6604 } else if matches!(
6605 order.order_type(),
6606 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
6607 ) {
6608 PostMatchOrderAction::UpdateTrailing(clone_order(order))
6609 } else {
6610 PostMatchOrderAction::NoMaintenance
6611 }
6612}
6613
6614fn write_filled_qty(order: &mut OrderAny, filled_qty: Quantity) {
6619 match order {
6620 OrderAny::Limit(o) => o.filled_qty = filled_qty,
6621 OrderAny::LimitIfTouched(o) => o.filled_qty = filled_qty,
6622 OrderAny::Market(o) => o.filled_qty = filled_qty,
6623 OrderAny::MarketIfTouched(o) => o.filled_qty = filled_qty,
6624 OrderAny::MarketToLimit(o) => o.filled_qty = filled_qty,
6625 OrderAny::StopLimit(o) => o.filled_qty = filled_qty,
6626 OrderAny::StopMarket(o) => o.filled_qty = filled_qty,
6627 OrderAny::TrailingStopLimit(o) => o.filled_qty = filled_qty,
6628 OrderAny::TrailingStopMarket(o) => o.filled_qty = filled_qty,
6629 }
6630}
6631
6632#[derive(Debug, Clone, Copy)]
6633struct BarTickSizes {
6634 open: Quantity,
6635 high: Quantity,
6636 low: Quantity,
6637 close: Quantity,
6638}
6639
6640impl BarTickSizes {
6641 fn from_volume(volume: Quantity, size_increment: Quantity) -> Self {
6642 let precision_diff = FIXED_PRECISION.saturating_sub(volume.precision);
6643 let scale = QuantityRaw::pow(10, u32::from(precision_diff));
6644 let units = volume.raw / scale;
6645 let increment_units = (size_increment.raw / scale).max(1);
6646 let rounded_units = (units / increment_units) * increment_units;
6647 let increments = rounded_units / increment_units;
6648 let zero = Quantity::zero(volume.precision);
6649 let size =
6650 |increments| Quantity::from_raw(increments * increment_units * scale, volume.precision);
6651
6652 match increments {
6653 0 => Self {
6654 open: zero,
6655 high: zero,
6656 low: zero,
6657 close: zero,
6658 },
6659 1 => Self {
6661 open: zero,
6662 high: zero,
6663 low: zero,
6664 close: size(1),
6665 },
6666 2 => Self {
6667 open: zero,
6668 high: size(1),
6669 low: size(1),
6670 close: zero,
6671 },
6672 3 => {
6673 let path_size = size(1);
6674
6675 Self {
6676 open: path_size,
6677 high: path_size,
6678 low: path_size,
6679 close: zero,
6680 }
6681 }
6682 _ => {
6683 let path_increments = increments / 4;
6684 let close_increments = increments - (path_increments * 3);
6685 let path_size = size(path_increments);
6686
6687 Self {
6688 open: path_size,
6689 high: path_size,
6690 low: path_size,
6691 close: size(close_increments),
6692 }
6693 }
6694 }
6695 }
6696}
6697
6698#[cfg(test)]
6699mod tests {
6700 use std::{
6701 cell::{Cell, RefCell},
6702 collections::{HashMap, HashSet},
6703 rc::Rc,
6704 };
6705
6706 use nautilus_common::{cache::Cache, clock::TestClock};
6707 use nautilus_core::{UnixNanos, correctness::CorrectnessError};
6708 use nautilus_model::{
6709 data::{
6710 DEPTH10_LEN, OrderBookDelta, OrderBookDepth10, QuoteTick, TradeTick,
6711 option_chain::OptionGreeks,
6712 order::{BookOrder, OrderId},
6713 },
6714 enums::{
6715 AccountType, AggressorSide, BookAction, BookType, LiquiditySide, OmsType, OrderSide,
6716 OrderType, RecordFlag, TimeInForce, TrailingOffsetType, TriggerType,
6717 },
6718 events::OrderEventAny,
6719 identifiers::{AccountId, ClientOrderId, TradeId, VenueOrderId},
6720 instruments::{
6721 Instrument, InstrumentAny,
6722 stubs::{crypto_option_btc_deribit, crypto_perpetual_ethusdt, futures_contract_es},
6723 },
6724 orderbook::OrderBook,
6725 orders::{Order, OrderAny, OrderTestBuilder, stubs::TestOrderEventStubs},
6726 types::{Money, Price, Quantity, fixed::FIXED_PRECISION, quantity::QuantityRaw},
6727 };
6728 use proptest::prelude::*;
6729 use rstest::rstest;
6730 use rust_decimal::Decimal;
6731
6732 use super::{BarTickSizes, OrderMatchingEngine, PostMatchOrderAction, post_match_order_action};
6733 use crate::{
6734 matching_engine::config::OrderMatchingEngineConfig,
6735 models::{
6736 fee::{FeeModel, FeeModelAny, FeeModelHandle},
6737 fill::{FillModel, FillModelHandle},
6738 },
6739 };
6740
6741 fn assert_valid_bar_tick_sizes(volume: Quantity, size_increment: Quantity) {
6742 let sizes = BarTickSizes::from_volume(volume, size_increment);
6743 let total_raw = sizes.open.raw + sizes.high.raw + sizes.low.raw + sizes.close.raw;
6744 assert!(total_raw <= volume.raw);
6745
6746 for quantity in [sizes.open, sizes.high, sizes.low, sizes.close] {
6747 assert_eq!(quantity.precision, volume.precision);
6748 assert!(
6749 OrderMatchingEngine::quantity_matches_precision(quantity, volume.precision),
6750 "bar tick quantity {quantity} not aligned to precision {}",
6751 volume.precision,
6752 );
6753 assert!(
6754 size_increment.raw == 0 || quantity.raw.is_multiple_of(size_increment.raw),
6755 "bar tick quantity {quantity} not aligned to increment {size_increment}",
6756 );
6757 }
6758
6759 if size_increment.raw > 0 {
6760 assert!(
6761 volume.raw - total_raw < size_increment.raw,
6762 "bar tick split left {} raw units from volume {volume} and increment {size_increment}",
6763 volume.raw - total_raw,
6764 );
6765 }
6766 }
6767
6768 #[rstest]
6769 fn test_post_match_order_action_does_not_clone_no_maintenance_order() {
6770 let order = post_match_limit_order();
6771 let clone_count = Cell::new(0);
6772
6773 let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
6774 clone_count.set(clone_count.get() + 1);
6775 order.clone()
6776 });
6777
6778 assert!(matches!(action, PostMatchOrderAction::NoMaintenance));
6779 assert_eq!(clone_count.get(), 0);
6780 }
6781
6782 #[rstest]
6783 fn test_post_match_order_action_does_not_clone_closed_order() {
6784 let order = post_match_closed_limit_order();
6785 let clone_count = Cell::new(0);
6786
6787 let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
6788 clone_count.set(clone_count.get() + 1);
6789 order.clone()
6790 });
6791
6792 assert!(matches!(action, PostMatchOrderAction::RemoveClosed));
6793 assert_eq!(clone_count.get(), 0);
6794 }
6795
6796 #[rstest]
6797 fn test_post_match_order_action_clones_expired_gtd_order_once() {
6798 let order = post_match_gtd_limit_order();
6799 let clone_count = Cell::new(0);
6800
6801 let action = post_match_order_action(&order, true, UnixNanos::from(10_u64), |order| {
6802 clone_count.set(clone_count.get() + 1);
6803 order.clone()
6804 });
6805
6806 let PostMatchOrderAction::Expire(cloned) = action else {
6807 panic!("Expected expired action, was {action:?}");
6808 };
6809 assert_eq!(cloned.client_order_id(), order.client_order_id());
6810 assert_eq!(clone_count.get(), 1);
6811 }
6812
6813 #[rstest]
6814 fn test_post_match_order_action_clones_trailing_order_once() {
6815 let order = post_match_trailing_stop_order();
6816 let clone_count = Cell::new(0);
6817
6818 let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
6819 clone_count.set(clone_count.get() + 1);
6820 order.clone()
6821 });
6822
6823 let PostMatchOrderAction::UpdateTrailing(cloned) = action else {
6824 panic!("Expected trailing update action, was {action:?}");
6825 };
6826 assert_eq!(cloned.client_order_id(), order.client_order_id());
6827 assert_eq!(clone_count.get(), 1);
6828 }
6829
6830 fn post_match_limit_order() -> OrderAny {
6831 OrderTestBuilder::new(OrderType::Limit)
6832 .instrument_id(crypto_perpetual_ethusdt().id())
6833 .side(OrderSide::Buy)
6834 .price(Price::from("1500.00"))
6835 .quantity(Quantity::from("1.000"))
6836 .client_order_id(ClientOrderId::from("POST-MATCH-LIMIT"))
6837 .submit(true)
6838 .build()
6839 }
6840
6841 fn post_match_closed_limit_order() -> OrderAny {
6842 let account_id = AccountId::from("SIM-001");
6843 let venue_order_id = VenueOrderId::from("V-001");
6844 let mut order = post_match_limit_order();
6845 order
6846 .apply(TestOrderEventStubs::accepted(
6847 &order,
6848 account_id,
6849 venue_order_id,
6850 ))
6851 .unwrap();
6852 order
6853 .apply(TestOrderEventStubs::canceled(
6854 &order,
6855 account_id,
6856 Some(venue_order_id),
6857 ))
6858 .unwrap();
6859 order
6860 }
6861
6862 fn post_match_gtd_limit_order() -> OrderAny {
6863 OrderTestBuilder::new(OrderType::Limit)
6864 .instrument_id(crypto_perpetual_ethusdt().id())
6865 .side(OrderSide::Buy)
6866 .price(Price::from("1500.00"))
6867 .quantity(Quantity::from("1.000"))
6868 .time_in_force(TimeInForce::Gtd)
6869 .expire_time(UnixNanos::from(10_u64))
6870 .client_order_id(ClientOrderId::from("POST-MATCH-GTD"))
6871 .submit(true)
6872 .build()
6873 }
6874
6875 fn post_match_trailing_stop_order() -> OrderAny {
6876 OrderTestBuilder::new(OrderType::TrailingStopMarket)
6877 .instrument_id(crypto_perpetual_ethusdt().id())
6878 .side(OrderSide::Buy)
6879 .quantity(Quantity::from("1.000"))
6880 .trigger_price(Price::from("1510.00"))
6881 .trigger_type(TriggerType::BidAsk)
6882 .trailing_offset(Decimal::new(5, 0))
6883 .trailing_offset_type(TrailingOffsetType::Price)
6884 .client_order_id(ClientOrderId::from("POST-MATCH-TRAIL"))
6885 .submit(true)
6886 .build()
6887 }
6888
6889 #[rstest]
6890 fn test_fill_order_calculates_commission_from_fill_liquidity_side() {
6891 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
6892 let cache = Rc::new(RefCell::new(Cache::default()));
6893 let clock = Rc::new(RefCell::new(TestClock::new()));
6894 let mut engine = OrderMatchingEngine::new(
6895 instrument.clone(),
6896 1,
6897 FillModelHandle::default(),
6898 FeeModelAny::default().into(),
6899 BookType::L1_MBP,
6900 OmsType::Netting,
6901 AccountType::Margin,
6902 clock,
6903 cache,
6904 Default::default(),
6905 );
6906 let events = Rc::new(RefCell::new(Vec::new()));
6907 let events_handler = Rc::clone(&events);
6908 engine.set_event_handler(Rc::new(move |event| {
6909 events_handler.borrow_mut().push(event);
6910 }));
6911
6912 let mut order = OrderTestBuilder::new(OrderType::Market)
6913 .instrument_id(instrument.id())
6914 .side(OrderSide::Buy)
6915 .quantity(Quantity::from("1.000"))
6916 .submit(true)
6917 .build();
6918 order.set_liquidity_side(LiquiditySide::Maker);
6919 engine
6920 .account_ids
6921 .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
6922
6923 engine
6924 .fill_order(
6925 &order,
6926 Price::from("1500.00"),
6927 Quantity::from("1.000"),
6928 LiquiditySide::Taker,
6929 None,
6930 None,
6931 )
6932 .unwrap();
6933
6934 let events = events.borrow();
6935 assert_eq!(events.len(), 1);
6936 let fill = match &events[0] {
6937 OrderEventAny::Filled(fill) => fill,
6938 event => panic!("Expected OrderFilled, was {event:?}"),
6939 };
6940 let commission = fill.commission.expect("expected commission");
6941 let expected_commission =
6942 fill.last_qty.as_decimal() * fill.last_px.as_decimal() * instrument.taker_fee();
6943
6944 assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
6945 assert_eq!(commission.currency, instrument.quote_currency());
6946 assert_eq!(commission.as_decimal(), expected_commission);
6947 }
6948
6949 #[rstest]
6950 fn test_custom_fee_model_handle_is_called_by_fill_order() {
6951 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
6952 let cache = Rc::new(RefCell::new(Cache::default()));
6953 let clock = Rc::new(RefCell::new(TestClock::new()));
6954 let calls = Rc::new(Cell::new(0));
6955 let expected_commission = Money::from("1.23 USDT");
6956 let fee_model = FeeModelHandle::new(RecordingFeeModel {
6957 calls: Rc::clone(&calls),
6958 commission: expected_commission,
6959 });
6960 let cloned_fee_model = fee_model.clone();
6961 drop(fee_model);
6962 let mut engine = OrderMatchingEngine::new(
6963 instrument.clone(),
6964 1,
6965 FillModelHandle::default(),
6966 cloned_fee_model,
6967 BookType::L1_MBP,
6968 OmsType::Netting,
6969 AccountType::Margin,
6970 clock,
6971 cache,
6972 Default::default(),
6973 );
6974 let events = Rc::new(RefCell::new(Vec::new()));
6975 let events_handler = Rc::clone(&events);
6976 engine.set_event_handler(Rc::new(move |event| {
6977 events_handler.borrow_mut().push(event);
6978 }));
6979
6980 let order = OrderTestBuilder::new(OrderType::Market)
6981 .instrument_id(instrument.id())
6982 .side(OrderSide::Buy)
6983 .quantity(Quantity::from("1.000"))
6984 .submit(true)
6985 .build();
6986 engine
6987 .account_ids
6988 .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
6989
6990 engine
6991 .fill_order(
6992 &order,
6993 Price::from("1500.00"),
6994 Quantity::from("1.000"),
6995 LiquiditySide::Taker,
6996 None,
6997 None,
6998 )
6999 .unwrap();
7000
7001 let events = events.borrow();
7002 assert_eq!(events.len(), 1);
7003 let fill = match &events[0] {
7004 OrderEventAny::Filled(fill) => fill,
7005 event => panic!("Expected OrderFilled, was {event:?}"),
7006 };
7007
7008 assert_eq!(calls.get(), 1);
7009 assert_eq!(fill.commission, Some(expected_commission));
7010 }
7011
7012 #[rstest]
7013 fn test_fill_order_does_not_cache_filled_qty_when_fee_model_fails() {
7014 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7015 let cache = Rc::new(RefCell::new(Cache::default()));
7016 let clock = Rc::new(RefCell::new(TestClock::new()));
7017 let mut engine = OrderMatchingEngine::new(
7018 instrument.clone(),
7019 1,
7020 FillModelHandle::default(),
7021 FeeModelHandle::new(FailingFeeModel),
7022 BookType::L1_MBP,
7023 OmsType::Netting,
7024 AccountType::Margin,
7025 clock,
7026 cache,
7027 Default::default(),
7028 );
7029 let events = Rc::new(RefCell::new(Vec::new()));
7030 let events_handler = Rc::clone(&events);
7031 engine.set_event_handler(Rc::new(move |event| {
7032 events_handler.borrow_mut().push(event);
7033 }));
7034
7035 let order = OrderTestBuilder::new(OrderType::Market)
7036 .instrument_id(instrument.id())
7037 .side(OrderSide::Buy)
7038 .quantity(Quantity::from("1.000"))
7039 .submit(true)
7040 .build();
7041 engine
7042 .account_ids
7043 .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
7044
7045 let result = engine.fill_order(
7046 &order,
7047 Price::from("1500.00"),
7048 Quantity::from("1.000"),
7049 LiquiditySide::Taker,
7050 None,
7051 None,
7052 );
7053
7054 assert!(result.is_err());
7055 assert_eq!(engine.cached_filled_qty_len(), 0);
7056 assert!(events.borrow().is_empty());
7057 }
7058
7059 fn collision_engine() -> (OrderMatchingEngine, Rc<RefCell<Cache>>, VenueOrderId) {
7060 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7061 let cache = Rc::new(RefCell::new(Cache::default()));
7062 let venue_order_id = VenueOrderId::from(format!("{}-1-1", instrument.id().venue));
7063 cache
7064 .borrow_mut()
7065 .add_venue_order_id(&ClientOrderId::from("O-OWNER"), &venue_order_id, false)
7066 .unwrap();
7067 let engine = OrderMatchingEngine::new(
7068 instrument,
7069 1,
7070 FillModelHandle::default(),
7071 FeeModelAny::default().into(),
7072 BookType::L1_MBP,
7073 OmsType::Netting,
7074 AccountType::Margin,
7075 Rc::new(RefCell::new(TestClock::new())),
7076 Rc::clone(&cache),
7077 Default::default(),
7078 );
7079
7080 (engine, cache, venue_order_id)
7081 }
7082
7083 #[rstest]
7084 #[case(OrderType::Market)]
7085 #[case(OrderType::MarketToLimit)]
7086 fn test_market_collision_probes_and_fills_with_default_ack_config(
7087 #[case] order_type: OrderType,
7088 ) {
7089 let (mut engine, cache, venue_order_id) = collision_engine();
7090 assert!(!engine.config.use_market_order_acks);
7091 let quote = QuoteTick::new(
7092 engine.instrument.id(),
7093 Price::from("1499.00"),
7094 Price::from("1500.00"),
7095 Quantity::from("10.000"),
7096 Quantity::from("10.000"),
7097 UnixNanos::default(),
7098 UnixNanos::default(),
7099 );
7100 engine.process_quote_tick("e);
7101 let events = Rc::new(RefCell::new(Vec::new()));
7102 let events_handler = Rc::clone(&events);
7103 engine.set_event_handler(Rc::new(move |event| {
7104 events_handler.borrow_mut().push(event);
7105 }));
7106 let mut order = OrderTestBuilder::new(order_type)
7107 .instrument_id(engine.instrument.id())
7108 .client_order_id(ClientOrderId::from("O-CLAIMANT"))
7109 .side(OrderSide::Buy)
7110 .quantity(Quantity::from("1.000"))
7111 .submit(true)
7112 .build();
7113
7114 engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
7115
7116 assert!(
7117 !events
7118 .borrow()
7119 .iter()
7120 .any(|event| matches!(event, OrderEventAny::Rejected(_)))
7121 );
7122 assert!(
7123 events
7124 .borrow()
7125 .iter()
7126 .any(|event| matches!(event, OrderEventAny::Filled(_)))
7127 );
7128 assert!(cache.borrow().order_exists(&order.client_order_id()));
7129 assert_eq!(
7130 cache.borrow().client_order_id(&venue_order_id),
7131 Some(&ClientOrderId::from("O-OWNER"))
7132 );
7133 assert_eq!(
7134 cache.borrow().venue_order_id(&order.client_order_id()),
7135 Some(&VenueOrderId::from(format!("{}-1-2", engine.venue)))
7136 );
7137 }
7138
7139 struct RecordingFeeModel {
7140 calls: Rc<Cell<u32>>,
7141 commission: Money,
7142 }
7143
7144 impl FeeModel for RecordingFeeModel {
7145 fn get_commission(
7146 &self,
7147 _order: &OrderAny,
7148 _fill_quantity: Quantity,
7149 _fill_px: Price,
7150 _instrument: &InstrumentAny,
7151 ) -> anyhow::Result<Money> {
7152 self.calls.set(self.calls.get() + 1);
7153 Ok(self.commission)
7154 }
7155 }
7156
7157 struct FailingFeeModel;
7158
7159 impl FeeModel for FailingFeeModel {
7160 fn get_commission(
7161 &self,
7162 _order: &OrderAny,
7163 _fill_quantity: Quantity,
7164 _fill_px: Price,
7165 _instrument: &InstrumentAny,
7166 ) -> anyhow::Result<Money> {
7167 Err(anyhow::anyhow!("fee model failed"))
7168 }
7169 }
7170
7171 #[rstest]
7172 fn test_custom_fill_model_handle_is_called_by_market_fill() {
7173 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7174 let cache = Rc::new(RefCell::new(Cache::default()));
7175 let clock = Rc::new(RefCell::new(TestClock::new()));
7176 let calls = Rc::new(Cell::new(0));
7177 let fill_model = FillModelHandle::new(RecordingFillModel {
7178 calls: Rc::clone(&calls),
7179 });
7180 let mut engine = OrderMatchingEngine::new(
7181 instrument.clone(),
7182 1,
7183 fill_model,
7184 FeeModelAny::default().into(),
7185 BookType::L1_MBP,
7186 OmsType::Netting,
7187 AccountType::Margin,
7188 clock,
7189 cache,
7190 Default::default(),
7191 );
7192 let quote = QuoteTick::new(
7193 instrument.id(),
7194 Price::from("1500.00"),
7195 Price::from("1501.00"),
7196 Quantity::from("10.000"),
7197 Quantity::from("10.000"),
7198 UnixNanos::default(),
7199 UnixNanos::default(),
7200 );
7201 engine.process_quote_tick("e);
7202
7203 let mut order = OrderTestBuilder::new(OrderType::Market)
7204 .instrument_id(instrument.id())
7205 .side(OrderSide::Buy)
7206 .quantity(Quantity::from("1.000"))
7207 .submit(true)
7208 .build();
7209 engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
7210
7211 assert_eq!(calls.get(), 1);
7212 }
7213
7214 #[rstest]
7215 fn test_l1_depth10_skips_padding_for_last_quote_tracking() {
7216 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7217 let cache = Rc::new(RefCell::new(Cache::default()));
7218 let clock = Rc::new(RefCell::new(TestClock::new()));
7219 let mut engine = OrderMatchingEngine::new(
7220 instrument.clone(),
7221 1,
7222 FillModelHandle::default(),
7223 FeeModelAny::default().into(),
7224 BookType::L1_MBP,
7225 OmsType::Netting,
7226 AccountType::Margin,
7227 clock,
7228 cache,
7229 Default::default(),
7230 );
7231 let mut bids = [BookOrder::default(); DEPTH10_LEN];
7232 let mut asks = [BookOrder::default(); DEPTH10_LEN];
7233 bids[1] = BookOrder::new(
7234 OrderSide::Buy,
7235 Price::from("1499.00"),
7236 Quantity::from("1.000"),
7237 1,
7238 );
7239 asks[0] = BookOrder::new(
7240 OrderSide::Sell,
7241 Price::from("1500.00"),
7242 Quantity::from("1.000"),
7243 2,
7244 );
7245
7246 let depth = OrderBookDepth10::new(
7247 instrument.id(),
7248 bids,
7249 asks,
7250 [0; DEPTH10_LEN],
7251 [0; DEPTH10_LEN],
7252 0,
7253 0,
7254 UnixNanos::from(1_u64),
7255 UnixNanos::from(1_u64),
7256 );
7257 engine.process_order_book_depth10(&depth).unwrap();
7258
7259 assert_eq!(engine.last_quote_bid, Some(Price::from("1499.00")));
7260 assert_eq!(engine.last_quote_ask, Some(Price::from("1500.00")));
7261
7262 let depth_without_bid = OrderBookDepth10::new(
7263 instrument.id(),
7264 [BookOrder::default(); DEPTH10_LEN],
7265 asks,
7266 [0; DEPTH10_LEN],
7267 [0; DEPTH10_LEN],
7268 0,
7269 1,
7270 UnixNanos::from(2_u64),
7271 UnixNanos::from(2_u64),
7272 );
7273 engine
7274 .process_order_book_depth10(&depth_without_bid)
7275 .unwrap();
7276
7277 assert_eq!(engine.last_quote_bid, None);
7278 assert_eq!(engine.last_quote_ask, Some(Price::from("1500.00")));
7279 }
7280
7281 struct RecordingFillModel {
7282 calls: Rc<Cell<u32>>,
7283 }
7284
7285 impl FillModel for RecordingFillModel {
7286 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
7287 Ok(true)
7288 }
7289
7290 fn is_slipped(&mut self) -> anyhow::Result<bool> {
7291 Ok(false)
7292 }
7293
7294 fn get_orderbook_for_fill_simulation(
7295 &mut self,
7296 _instrument: &InstrumentAny,
7297 _order: &OrderAny,
7298 _best_bid: Price,
7299 _best_ask: Price,
7300 ) -> anyhow::Result<Option<OrderBook>> {
7301 self.calls.set(self.calls.get() + 1);
7302 Ok(None)
7303 }
7304 }
7305
7306 #[rstest]
7307 fn test_fee_underlying_price_uses_valid_cached_greeks_price() {
7308 let instrument = InstrumentAny::CryptoOption(crypto_option_btc_deribit(
7309 3,
7310 1,
7311 Price::from("0.001"),
7312 Quantity::from("0.1"),
7313 ));
7314 let cache = Rc::new(RefCell::new(Cache::default()));
7315 cache.borrow_mut().add_option_greeks(OptionGreeks {
7316 instrument_id: instrument.id(),
7317 underlying_price: Some(50_000.0),
7318 ..Default::default()
7319 });
7320 let clock = Rc::new(RefCell::new(TestClock::new()));
7321 let engine = OrderMatchingEngine::new(
7322 instrument,
7323 1,
7324 FillModelHandle::default(),
7325 FeeModelAny::default().into(),
7326 BookType::L1_MBP,
7327 OmsType::Netting,
7328 AccountType::Margin,
7329 clock,
7330 cache,
7331 Default::default(),
7332 );
7333
7334 let price = engine
7335 .fee_underlying_price()
7336 .unwrap()
7337 .expect("expected underlying price");
7338
7339 assert_eq!(price.precision, FIXED_PRECISION);
7340 assert_eq!(price.as_decimal(), Decimal::from(50_000));
7341 }
7342
7343 #[rstest]
7344 fn test_fee_underlying_price_rejects_invalid_cached_greeks_price() {
7345 let instrument = InstrumentAny::CryptoOption(crypto_option_btc_deribit(
7346 3,
7347 1,
7348 Price::from("0.001"),
7349 Quantity::from("0.1"),
7350 ));
7351 let cache = Rc::new(RefCell::new(Cache::default()));
7352 cache.borrow_mut().add_option_greeks(OptionGreeks {
7353 instrument_id: instrument.id(),
7354 underlying_price: Some(f64::NAN),
7355 ..Default::default()
7356 });
7357 let clock = Rc::new(RefCell::new(TestClock::new()));
7358 let engine = OrderMatchingEngine::new(
7359 instrument,
7360 1,
7361 FillModelHandle::default(),
7362 FeeModelAny::default().into(),
7363 BookType::L1_MBP,
7364 OmsType::Netting,
7365 AccountType::Margin,
7366 clock,
7367 cache,
7368 Default::default(),
7369 );
7370
7371 let error = engine.fee_underlying_price().unwrap_err();
7372
7373 assert_eq!(
7374 error,
7375 CorrectnessError::InvalidValue {
7376 param: "value".to_string(),
7377 value: "NaN".to_string(),
7378 type_name: "f64",
7379 }
7380 );
7381 }
7382
7383 #[rstest]
7384 fn test_bar_tick_sizes_divisible() {
7385 let volume = Quantity::from("100.000");
7387 let increment = Quantity::from("0.001");
7388 let sizes = BarTickSizes::from_volume(volume, increment);
7389 assert_eq!(sizes.open, Quantity::from("25.000"));
7390 assert_eq!(sizes.high, Quantity::from("25.000"));
7391 assert_eq!(sizes.low, Quantity::from("25.000"));
7392 assert_eq!(sizes.close, Quantity::from("25.000"));
7393 assert_valid_bar_tick_sizes(volume, increment);
7394 }
7395
7396 #[rstest]
7397 fn test_bar_tick_sizes_indivisible_with_remainder() {
7398 let volume = Quantity::from("0.05");
7400 let increment = Quantity::from("0.01");
7401 let sizes = BarTickSizes::from_volume(volume, increment);
7402 assert_eq!(sizes.open, Quantity::from("0.01"));
7403 assert_eq!(sizes.high, Quantity::from("0.01"));
7404 assert_eq!(sizes.low, Quantity::from("0.01"));
7405 assert_eq!(sizes.close, Quantity::from("0.02"));
7406 assert_valid_bar_tick_sizes(volume, increment);
7407 assert_eq!(
7408 sizes.open.raw + sizes.high.raw + sizes.low.raw + sizes.close.raw,
7409 volume.raw
7410 );
7411 }
7412
7413 #[rstest]
7414 #[case("1", "0", "0", "0", "1")]
7415 #[case("2", "0", "1", "1", "0")]
7416 #[case("3", "1", "1", "1", "0")]
7417 fn test_bar_tick_sizes_units_less_than_four_preserves_volume(
7418 #[case] volume: &str,
7419 #[case] open_size: &str,
7420 #[case] high_size: &str,
7421 #[case] low_size: &str,
7422 #[case] close_size: &str,
7423 ) {
7424 let volume = Quantity::from(volume);
7425 let increment = Quantity::from("1");
7426 let sizes = BarTickSizes::from_volume(volume, increment);
7427
7428 assert_eq!(sizes.open, Quantity::from(open_size));
7429 assert_eq!(sizes.high, Quantity::from(high_size));
7430 assert_eq!(sizes.low, Quantity::from(low_size));
7431 assert_eq!(sizes.close, Quantity::from(close_size));
7432 assert_valid_bar_tick_sizes(volume, increment);
7433 assert_eq!(
7434 sizes.open.raw + sizes.high.raw + sizes.low.raw + sizes.close.raw,
7435 volume.raw
7436 );
7437 }
7438
7439 #[rstest]
7440 fn test_bar_tick_sizes_zero_volume_remains_zero() {
7441 let volume = Quantity::zero(3);
7442 let increment = Quantity::from("0.001");
7443 let sizes = BarTickSizes::from_volume(volume, increment);
7444 assert_eq!(sizes.open, Quantity::zero(3));
7445 assert_eq!(sizes.high, Quantity::zero(3));
7446 assert_eq!(sizes.low, Quantity::zero(3));
7447 assert_eq!(sizes.close, Quantity::zero(3));
7448 assert_valid_bar_tick_sizes(volume, increment);
7449 }
7450
7451 #[rstest]
7452 fn test_bar_tick_sizes_rounds_down_to_size_increment() {
7453 let volume = Quantity::from("1.07");
7454 let increment = Quantity::from("0.10");
7455 let sizes = BarTickSizes::from_volume(volume, increment);
7456 assert_eq!(sizes.open, Quantity::from("0.20"));
7457 assert_eq!(sizes.high, Quantity::from("0.20"));
7458 assert_eq!(sizes.low, Quantity::from("0.20"));
7459 assert_eq!(sizes.close, Quantity::from("0.40"));
7460 assert_valid_bar_tick_sizes(volume, increment);
7461 }
7462
7463 #[rstest]
7464 fn test_bar_tick_sizes_at_fixed_precision() {
7465 let units: QuantityRaw = 17;
7468 let volume = Quantity::from_raw(units, FIXED_PRECISION);
7469 let increment = Quantity::from_raw(1, FIXED_PRECISION);
7470 let sizes = BarTickSizes::from_volume(volume, increment);
7471 assert_eq!(sizes.open.raw, 4);
7472 assert_eq!(sizes.high.raw, 4);
7473 assert_eq!(sizes.low.raw, 4);
7474 assert_eq!(sizes.close.raw, 5);
7475 assert_valid_bar_tick_sizes(volume, increment);
7476 }
7477
7478 fn get_l3_queue_engine(instrument: InstrumentAny) -> (OrderMatchingEngine, Rc<RefCell<Cache>>) {
7479 let clock = Rc::new(RefCell::new(TestClock::new()));
7480 let cache = Rc::new(RefCell::new(Cache::default()));
7481 let config = OrderMatchingEngineConfig {
7482 trade_execution: true,
7483 queue_position: true,
7484 ..Default::default()
7485 };
7486
7487 let mut engine = OrderMatchingEngine::new(
7488 instrument,
7489 1,
7490 FillModelHandle::default(),
7491 FeeModelAny::default().into(),
7492 BookType::L3_MBO,
7493 OmsType::Netting,
7494 AccountType::Margin,
7495 clock,
7496 Rc::clone(&cache),
7497 config,
7498 );
7499
7500 let handler_cache = Rc::clone(&cache);
7501 engine.set_event_handler(Rc::new(move |event: OrderEventAny| {
7502 if let Ok(mut cache) = handler_cache.try_borrow_mut() {
7503 let _ = cache.update_order(&event);
7504 }
7505 }));
7506
7507 (engine, cache)
7508 }
7509
7510 fn assert_l3_queue_synced(engine: &OrderMatchingEngine) {
7511 for (client_order_id, orders_ahead) in &engine.queue_ahead_orders {
7512 let set_sum: QuantityRaw = orders_ahead.values().sum();
7513 let counter = engine
7514 .queue_ahead_total
7515 .get(client_order_id)
7516 .map_or(0, |&(_, ahead_raw)| ahead_raw);
7517 assert_eq!(
7518 set_sum, counter,
7519 "tracked orders out of sync with quantity-ahead counter for {client_order_id}",
7520 );
7521 }
7522 }
7523
7524 #[derive(Debug, Clone, Copy)]
7525 enum QueueEvent {
7526 Add { id: OrderId, size: u64 },
7527 Update { id: OrderId, size: u64 },
7528 MoveAway { id: OrderId },
7529 Delete { id: OrderId },
7530 Trade { size: u64, aggressor: u8 },
7531 AggregateCap { size: u64 },
7532 AggregateDelete,
7533 RestOrder,
7534 }
7535
7536 fn granular_queue_event() -> impl Strategy<Value = QueueEvent> {
7537 prop_oneof![
7538 3 => (1u64..=6, 1u64..=9).prop_map(|(id, size)| QueueEvent::Add { id, size }),
7539 3 => (1u64..=6, 1u64..=9).prop_map(|(id, size)| QueueEvent::Update { id, size }),
7540 1 => (1u64..=6).prop_map(|id| QueueEvent::MoveAway { id }),
7541 2 => (1u64..=6).prop_map(|id| QueueEvent::Delete { id }),
7542 2 => Just(QueueEvent::RestOrder),
7543 ]
7544 }
7545
7546 fn any_queue_event() -> impl Strategy<Value = QueueEvent> {
7547 prop_oneof![
7548 5 => granular_queue_event(),
7549 3 => (1u64..=9, 0u8..3).prop_map(|(size, aggressor)| QueueEvent::Trade {
7550 size,
7551 aggressor,
7552 }),
7553 1 => (1u64..=9).prop_map(|size| QueueEvent::AggregateCap { size }),
7554 1 => Just(QueueEvent::AggregateDelete),
7555 ]
7556 }
7557
7558 struct L3QueueSim {
7561 engine: OrderMatchingEngine,
7562 account_id: AccountId,
7563 live_main: HashMap<OrderId, u64>,
7564 live_away: HashSet<OrderId>,
7565 rest_snapshots: HashMap<ClientOrderId, HashSet<OrderId>>,
7566 rested: usize,
7567 sequence: u64,
7568 }
7569
7570 impl L3QueueSim {
7571 const MAIN_PRICE: &'static str = "100.00";
7572 const AWAY_PRICE: &'static str = "101.00";
7573
7574 fn new() -> Self {
7575 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7576 let (engine, _cache) = get_l3_queue_engine(instrument);
7577
7578 Self {
7579 engine,
7580 account_id: AccountId::from("SIM-001"),
7581 live_main: HashMap::new(),
7582 live_away: HashSet::new(),
7583 rest_snapshots: HashMap::new(),
7584 rested: 0,
7585 sequence: 0,
7586 }
7587 }
7588
7589 fn quantity(size: u64) -> Quantity {
7590 Quantity::from(format!("{size}.000").as_str())
7591 }
7592
7593 fn process_delta(
7594 &mut self,
7595 action: BookAction,
7596 price: &str,
7597 size: u64,
7598 order_id: OrderId,
7599 flags: u8,
7600 ) {
7601 self.sequence += 1;
7602 let delta = OrderBookDelta::new(
7603 self.engine.instrument.id(),
7604 action,
7605 BookOrder::new(
7606 OrderSide::Sell,
7607 Price::from(price),
7608 Self::quantity(size),
7609 order_id,
7610 ),
7611 flags,
7612 self.sequence,
7613 UnixNanos::from(self.sequence),
7614 UnixNanos::from(self.sequence),
7615 );
7616 self.engine.process_order_book_delta(&delta).unwrap();
7617 }
7618
7619 fn apply(&mut self, event: QueueEvent) {
7620 match event {
7621 QueueEvent::Add { id, size } => {
7622 if self.live_main.contains_key(&id) || self.live_away.contains(&id) {
7623 return;
7624 }
7625 self.process_delta(BookAction::Add, Self::MAIN_PRICE, size, id, 0);
7626 self.live_main.insert(id, size);
7627 }
7628 QueueEvent::Update { id, size } => {
7629 if !self.live_main.contains_key(&id) {
7630 return;
7631 }
7632 self.process_delta(BookAction::Update, Self::MAIN_PRICE, size, id, 0);
7633 self.live_main.insert(id, size);
7634 }
7635 QueueEvent::MoveAway { id } => {
7636 let Some(size) = self.live_main.remove(&id) else {
7637 return;
7638 };
7639 self.process_delta(BookAction::Update, Self::AWAY_PRICE, size, id, 0);
7640 self.live_away.insert(id);
7641 }
7642 QueueEvent::Delete { id } => {
7643 if let Some(size) = self.live_main.remove(&id) {
7644 self.process_delta(BookAction::Delete, Self::MAIN_PRICE, size, id, 0);
7645 } else if self.live_away.remove(&id) {
7646 self.process_delta(BookAction::Delete, Self::AWAY_PRICE, 1, id, 0);
7647 } else {
7648 self.process_delta(BookAction::Delete, Self::MAIN_PRICE, 1, id, 0);
7650 }
7651
7652 for snapshot_ids in self.rest_snapshots.values_mut() {
7655 snapshot_ids.remove(&id);
7656 }
7657 }
7658 QueueEvent::Trade { size, aggressor } => {
7659 self.sequence += 1;
7660 let aggressor_side = match aggressor {
7661 0 => AggressorSide::Buyer,
7662 1 => AggressorSide::Seller,
7663 _ => AggressorSide::NoAggressor,
7664 };
7665 let trade = TradeTick::new(
7666 self.engine.instrument.id(),
7667 Price::from(Self::MAIN_PRICE),
7668 Self::quantity(size),
7669 aggressor_side,
7670 TradeId::new(format!("T-{}", self.sequence).as_str()),
7671 UnixNanos::from(self.sequence),
7672 UnixNanos::from(self.sequence),
7673 );
7674 self.engine.process_trade_tick(&trade);
7675 }
7676 QueueEvent::AggregateCap { size } => {
7677 self.process_delta(
7678 BookAction::Update,
7679 Self::MAIN_PRICE,
7680 size,
7681 0,
7682 RecordFlag::F_MBP as u8,
7683 );
7684 }
7685 QueueEvent::AggregateDelete => {
7686 self.process_delta(
7687 BookAction::Delete,
7688 Self::MAIN_PRICE,
7689 1,
7690 0,
7691 RecordFlag::F_MBP as u8,
7692 );
7693 }
7694 QueueEvent::RestOrder => {
7695 if self.rested >= 3 {
7696 return;
7697 }
7698 self.rested += 1;
7699 let mut order = OrderTestBuilder::new(OrderType::Limit)
7700 .instrument_id(self.engine.instrument.id())
7701 .side(OrderSide::Sell)
7702 .price(Price::from(Self::MAIN_PRICE))
7703 .quantity(Self::quantity(5))
7704 .client_order_id(ClientOrderId::from(
7705 format!("O-PROP-{}", self.rested).as_str(),
7706 ))
7707 .submit(true)
7708 .build();
7709 self.engine.process_order(&mut order, self.account_id);
7710
7711 assert!(
7712 self.engine
7713 .queue_ahead_orders
7714 .contains_key(&order.client_order_id()),
7715 "L3 snapshot must track the resting order",
7716 );
7717
7718 self.rest_snapshots.insert(
7719 order.client_order_id(),
7720 self.live_main.keys().copied().collect(),
7721 );
7722 }
7723 }
7724 }
7725
7726 fn assert_tracked_orders_match_book(&self) {
7729 let level: HashMap<OrderId, QuantityRaw> = self
7730 .engine
7731 .book
7732 .get_orders_at_level(Price::from(Self::MAIN_PRICE), OrderSide::Buy)
7733 .iter()
7734 .map(|order| (order.order_id, order.size.raw))
7735 .collect();
7736
7737 for (client_order_id, orders_ahead) in &self.engine.queue_ahead_orders {
7738 for (order_id, size_raw) in orders_ahead {
7739 let book_size = level.get(order_id).copied().unwrap_or_else(|| {
7740 panic!("tracked order {order_id} for {client_order_id} not in book level")
7741 });
7742 assert_eq!(
7743 book_size, *size_raw,
7744 "tracked size diverged from book for order {order_id}",
7745 );
7746 }
7747
7748 let tracked: HashSet<OrderId> = orders_ahead.keys().copied().collect();
7749 let expected: HashSet<OrderId> = self.rest_snapshots[client_order_id]
7750 .iter()
7751 .filter(|id| self.live_main.contains_key(id))
7752 .copied()
7753 .collect();
7754 assert_eq!(
7755 tracked, expected,
7756 "tracked set incomplete or stale for {client_order_id}",
7757 );
7758 }
7759 }
7760 }
7761
7762 #[rstest]
7763 fn prop_test_l3_queue_tracking_stays_synced_with_counter() {
7764 proptest!(|(events in prop::collection::vec(any_queue_event(), 1..=80))| {
7765 let mut sim = L3QueueSim::new();
7766 for event in events {
7767 sim.apply(event);
7768 assert_l3_queue_synced(&sim.engine);
7769 }
7770 });
7771 }
7772
7773 #[rstest]
7774 fn prop_test_l3_queue_tracking_mirrors_book_without_trades() {
7775 proptest!(|(events in prop::collection::vec(granular_queue_event(), 1..=80))| {
7776 let mut sim = L3QueueSim::new();
7777 for event in events {
7778 sim.apply(event);
7779 assert_l3_queue_synced(&sim.engine);
7780 sim.assert_tracked_orders_match_book();
7781 }
7782 });
7783 }
7784
7785 #[rstest]
7790 fn test_l3_queue_position_replay_databento_mbo_stays_synced() {
7791 let json = include_str!("../../test_data/databento_esh4_mbo.json");
7792 let records: Vec<serde_json::Value> = serde_json::from_str(json).unwrap();
7793 assert!(records.len() > 1000);
7794
7795 let instrument = InstrumentAny::FuturesContract(futures_contract_es(None, None));
7796 let instrument_id = instrument.id();
7797 let (mut engine, cache) = get_l3_queue_engine(instrument);
7798 let account_id = AccountId::from("SIM-001");
7799
7800 let mut rested = 0usize;
7801 let mut trades = 0usize;
7802
7803 for (index, record) in records.iter().enumerate() {
7804 match record.get("type").and_then(serde_json::Value::as_str) {
7805 Some("OrderBookDelta") => {
7806 let mut delta: OrderBookDelta = serde_json::from_value(record.clone()).unwrap();
7807 delta.instrument_id = instrument_id;
7808 engine.process_order_book_delta(&delta).unwrap();
7809 }
7810 Some("TradeTick") => {
7811 let mut trade: TradeTick = serde_json::from_value(record.clone()).unwrap();
7812 trade.instrument_id = instrument_id;
7813 engine.process_trade_tick(&trade);
7814 trades += 1;
7815 }
7816 other => panic!("unexpected record type {other:?}"),
7817 }
7818
7819 if index % 150 == 100 {
7820 let (side, price) = if rested.is_multiple_of(2) {
7821 (OrderSide::Sell, engine.book.best_ask_price())
7822 } else {
7823 (OrderSide::Buy, engine.book.best_bid_price())
7824 };
7825
7826 if let Some(price) = price {
7827 rested += 1;
7828 let mut order = OrderTestBuilder::new(OrderType::Limit)
7829 .instrument_id(instrument_id)
7830 .side(side)
7831 .price(price)
7832 .quantity(Quantity::from("1"))
7833 .client_order_id(ClientOrderId::from(format!("O-MBO-{rested}").as_str()))
7834 .submit(true)
7835 .build();
7836 engine.process_order(&mut order, account_id);
7837
7838 let is_open = cache
7841 .borrow()
7842 .order(&order.client_order_id())
7843 .is_some_and(|order| order.is_open());
7844 if is_open {
7845 assert!(
7846 engine
7847 .queue_ahead_orders
7848 .contains_key(&order.client_order_id()),
7849 "L3 snapshot must track the resting order",
7850 );
7851 }
7852 }
7853 }
7854
7855 assert_l3_queue_synced(&engine);
7856 }
7857
7858 assert!(rested >= 5, "replay must exercise resting orders");
7859 assert!(trades >= 50, "replay must exercise trade interleavings");
7860 }
7861}