1pub mod config;
19pub mod ids_generator;
20pub mod inflight;
21
22mod settlement;
23
24use std::{
25 cell::RefCell,
26 cmp::min,
27 fmt::Debug,
28 mem,
29 ops::{Add, Sub},
30 rc::Rc,
31};
32
33use indexmap::{IndexMap, IndexSet};
34use jiff::SignedDuration;
35use nautilus_common::{
36 cache::Cache,
37 clock::Clock,
38 messages::execution::{
39 BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, ModifyOrder,
40 },
41 msgbus::{self, MessagingSwitchboard},
42};
43use nautilus_core::{UUID4, UnixNanos, correctness::CorrectnessResult};
44use nautilus_model::{
45 data::{
46 Bar, BarType, InstrumentClose, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick,
47 TradeTick,
48 order::{BookOrder, OrderId},
49 },
50 enums::{
51 AccountType, AggregationSource, AggressorSide, BookAction, BookType, ContingencyType,
52 InstrumentCloseType, LiquiditySide, MarketStatus, MarketStatusAction, OmsType, OrderSide,
53 OrderStatus, OrderType, PositionSide, PriceType, RecordFlag, TimeInForce, TriggerType,
54 },
55 events::{
56 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderEventAny, OrderExpired,
57 OrderFilled, OrderModifyRejected, OrderRejected, OrderSubmitted, OrderTriggered,
58 OrderUpdated,
59 },
60 identifiers::{
61 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId, Venue,
62 VenueOrderId,
63 },
64 instruments::{Instrument, InstrumentAny},
65 orderbook::{BookLevel, OrderBook},
66 orders::{MarketOrder, Order, OrderAny, OrderCore},
67 position::{Position, PositionReplayEvent},
68 types::{
69 Currency, Money, Price, Quantity,
70 fixed::{FIXED_PRECISION, raw_scales_match},
71 price::PriceRaw,
72 quantity::QuantityRaw,
73 },
74};
75use rust_decimal::Decimal;
76use ustr::Ustr;
77
78use self::{
79 config::OrderMatchingEngineConfig, ids_generator::IdsGenerator, inflight::InflightOrders,
80};
81use crate::{
82 matching_core::{MatchAction, OrderMatchingCore, RestingOrder},
83 models::{
84 fee::{FeeModel, FeeModelHandle},
85 fill::{FillModel, FillModelHandle},
86 },
87 protection::protection_price_calculate,
88 trailing::trailing_stop_calculate,
89};
90
91pub struct OrderMatchingEngine {
93 pub venue: Venue,
95 pub instrument: InstrumentAny,
97 pub raw_id: u32,
99 pub book_type: BookType,
101 pub oms_type: OmsType,
103 pub account_type: AccountType,
105 pub market_status: MarketStatus,
107 pub config: OrderMatchingEngineConfig,
109 core: OrderMatchingCore,
110 clock: Rc<RefCell<dyn Clock>>,
111 cache: Rc<RefCell<Cache>>,
112 book: OrderBook,
113 fill_model: FillModelHandle,
114 fee_model: FeeModelHandle,
115 event_handler: Option<Rc<dyn Fn(OrderEventAny)>>,
116 inflight_orders: InflightOrders,
117 target_bid: Option<Price>,
118 target_ask: Option<Price>,
119 target_last: Option<Price>,
120 last_bar_bid: Option<Bar>,
121 last_bar_ask: Option<Bar>,
122 fill_at_market: bool,
123 execution_bar_types: IndexMap<InstrumentId, BarType>,
124 execution_bar_deltas: IndexMap<BarType, SignedDuration>,
125 account_ids: IndexMap<TraderId, AccountId>,
126 cached_filled_qty: IndexMap<ClientOrderId, Quantity>,
127 pending_order_updates: RefCell<IndexMap<ClientOrderId, Vec<OrderUpdated>>>,
128 pending_fills: IndexMap<TradeId, PendingFill>,
129 post_match_order_ids: IndexSet<ClientOrderId>,
130 ids_generator: IdsGenerator,
131 last_trade_size: Option<Quantity>,
132 trade_consumption: QuantityRaw,
133 bid_consumption: IndexMap<PriceRaw, (QuantityRaw, QuantityRaw)>,
134 ask_consumption: IndexMap<PriceRaw, (QuantityRaw, QuantityRaw)>,
135 queue_pending: IndexMap<ClientOrderId, PriceRaw>,
136 queue_ahead_orders: IndexMap<ClientOrderId, IndexMap<OrderId, QuantityRaw>>,
137 queue_ahead_total: IndexMap<ClientOrderId, (PriceRaw, QuantityRaw)>,
138 queue_snapshot_in_progress: bool,
139 queue_ids_by_price: IndexMap<PriceRaw, IndexSet<ClientOrderId>>,
140 queue_excess: IndexMap<ClientOrderId, QuantityRaw>,
141 queue_id_scratch: Vec<ClientOrderId>,
142 queue_pending_scratch: Vec<(ClientOrderId, PriceRaw)>,
143 queue_stale_scratch: Vec<ClientOrderId>,
144 queue_entry_scratch: Vec<(ClientOrderId, QuantityRaw, QuantityRaw)>,
145 prev_bid_price_raw: PriceRaw,
146 prev_ask_price_raw: PriceRaw,
147 tob_initialized: bool,
148 last_quote_bid: Option<Price>,
149 last_quote_ask: Option<Price>,
150 precision_mismatch_streak: u32,
151 instrument_close: Option<InstrumentClose>,
152 pending_resolution: bool,
153 expiration_processed: bool,
154 option_settlement_failed: bool,
155 option_settlement_warning: Option<&'static str>,
156 option_expiration_orders_canceled: bool,
157}
158
159impl Debug for OrderMatchingEngine {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 f.debug_struct(stringify!(OrderMatchingEngine))
162 .field("venue", &self.venue)
163 .field("instrument", &self.instrument.id())
164 .finish()
165 }
166}
167
168impl OrderMatchingEngine {
169 #[expect(clippy::too_many_arguments)]
171 pub fn new(
172 instrument: InstrumentAny,
173 raw_id: u32,
174 fill_model: FillModelHandle,
175 fee_model: FeeModelHandle,
176 book_type: BookType,
177 oms_type: OmsType,
178 account_type: AccountType,
179 clock: Rc<RefCell<dyn Clock>>,
180 cache: Rc<RefCell<Cache>>,
181 config: OrderMatchingEngineConfig,
182 ) -> Self {
183 let book = OrderBook::new(instrument.id(), book_type);
184 let mut core = OrderMatchingCore::new(instrument.id(), instrument.price_increment());
185 core.set_fill_limit_inside_spread(Self::fill_limit_inside_spread_or_false(&fill_model));
186 let ids_generator = IdsGenerator::new(
187 instrument.id().venue,
188 oms_type,
189 raw_id,
190 config.use_random_ids,
191 config.use_position_ids,
192 cache.clone(),
193 );
194
195 Self {
196 venue: instrument.id().venue,
197 instrument,
198 raw_id,
199 fill_model,
200 fee_model,
201 event_handler: None,
202 inflight_orders: InflightOrders::default(),
203 book_type,
204 oms_type,
205 account_type,
206 clock,
207 cache,
208 book,
209 market_status: MarketStatus::Open,
210 config,
211 core,
212 target_bid: None,
213 target_ask: None,
214 target_last: None,
215 last_bar_bid: None,
216 last_bar_ask: None,
217 fill_at_market: true,
218 execution_bar_types: IndexMap::new(),
219 execution_bar_deltas: IndexMap::new(),
220 account_ids: IndexMap::new(),
221 cached_filled_qty: IndexMap::new(),
222 pending_order_updates: RefCell::new(IndexMap::new()),
223 pending_fills: IndexMap::new(),
224 post_match_order_ids: IndexSet::new(),
225 ids_generator,
226 last_trade_size: None,
227 trade_consumption: 0,
228 bid_consumption: IndexMap::new(),
229 ask_consumption: IndexMap::new(),
230 queue_pending: IndexMap::new(),
231 queue_ahead_orders: IndexMap::new(),
232 queue_ahead_total: IndexMap::new(),
233 queue_snapshot_in_progress: false,
234 queue_ids_by_price: IndexMap::new(),
235 queue_excess: IndexMap::new(),
236 queue_id_scratch: Vec::new(),
237 queue_pending_scratch: Vec::new(),
238 queue_stale_scratch: Vec::new(),
239 queue_entry_scratch: Vec::new(),
240 prev_bid_price_raw: 0,
241 prev_ask_price_raw: 0,
242 tob_initialized: false,
243 last_quote_bid: None,
244 last_quote_ask: None,
245 precision_mismatch_streak: 0,
246 instrument_close: None,
247 pending_resolution: false,
248 expiration_processed: false,
249 option_settlement_failed: false,
250 option_settlement_warning: None,
251 option_expiration_orders_canceled: false,
252 }
253 }
254
255 pub fn set_event_handler(&mut self, handler: Rc<dyn Fn(OrderEventAny)>) {
262 self.event_handler = Some(handler);
263 }
264
265 pub fn set_inflight_orders(&mut self, orders: InflightOrders) {
267 self.inflight_orders = orders;
268 }
269
270 fn dispatch_order_event(&self, event: OrderEventAny) {
271 if let Some(handler) = &self.event_handler {
272 handler(event);
273 } else {
274 let endpoint = MessagingSwitchboard::exec_engine_process();
275 msgbus::send_order_event(endpoint, event);
276 }
277 }
278
279 pub fn reset(&mut self) {
285 self.book.reset();
286 self.execution_bar_types.clear();
287 self.execution_bar_deltas.clear();
288 self.account_ids.clear();
289 self.cached_filled_qty.clear();
290 self.pending_order_updates.get_mut().clear();
291 self.pending_fills.clear();
292 self.post_match_order_ids.clear();
293 self.core.reset();
294 self.target_bid = None;
295 self.target_ask = None;
296 self.target_last = None;
297 self.last_trade_size = None;
298 self.trade_consumption = 0;
299 self.bid_consumption.clear();
300 self.ask_consumption.clear();
301 self.queue_pending.clear();
302 self.queue_ahead_orders.clear();
303 self.queue_ahead_total.clear();
304 self.queue_snapshot_in_progress = false;
305 self.queue_ids_by_price.clear();
306 self.queue_excess.clear();
307 self.queue_id_scratch.clear();
308 self.queue_pending_scratch.clear();
309 self.queue_stale_scratch.clear();
310 self.queue_entry_scratch.clear();
311 self.prev_bid_price_raw = 0;
312 self.prev_ask_price_raw = 0;
313 self.tob_initialized = false;
314 self.last_quote_bid = None;
315 self.last_quote_ask = None;
316 self.last_bar_bid = None;
317 self.last_bar_ask = None;
318 self.precision_mismatch_streak = 0;
319 self.instrument_close = None;
320 self.market_status = MarketStatus::Open;
321 self.pending_resolution = false;
322 self.expiration_processed = false;
323 self.option_settlement_failed = false;
324 self.option_settlement_warning = None;
325 self.option_expiration_orders_canceled = false;
326 self.fill_at_market = true;
327 self.ids_generator.reset();
328
329 log::info!("Reset {}", self.instrument.id());
330 }
331
332 fn apply_liquidity_consumption(
333 &mut self,
334 mut fills: Vec<(Price, Quantity)>,
335 order_side: OrderSide,
336 leaves_qty: Quantity,
337 book_prices: Option<&[Price]>,
338 ) -> Vec<(Price, Quantity)> {
339 if !self.config.liquidity_consumption {
340 return fills;
341 }
342
343 let consumption = match order_side {
344 OrderSide::Buy => &mut self.ask_consumption,
345 OrderSide::Sell => &mut self.bid_consumption,
346 };
347
348 let mut adjusted_len = 0;
349 let mut remaining_qty = leaves_qty.raw();
350
351 for fill_idx in 0..fills.len() {
352 if remaining_qty == 0 {
353 break;
354 }
355
356 let (price, qty) = fills[fill_idx];
357
358 let book_price = book_prices
361 .and_then(|bp| bp.get(fill_idx).copied())
362 .unwrap_or(price);
363
364 let book_price_raw = book_price.raw();
365 let level_size = self
366 .book
367 .get_quantity_at_level(book_price, order_side, qty.precision);
368
369 let (original_size, consumed) = consumption
370 .entry(book_price_raw)
371 .or_insert((level_size.raw(), 0));
372
373 if *original_size != level_size.raw() {
375 *original_size = level_size.raw();
376 *consumed = 0;
377 }
378
379 let available = original_size.saturating_sub(*consumed);
380 if available == 0 {
381 continue;
382 }
383
384 let adjusted_qty_raw = min(min(qty.raw(), available), remaining_qty);
385 if adjusted_qty_raw == 0 {
386 continue;
387 }
388
389 *consumed += adjusted_qty_raw;
390 remaining_qty -= adjusted_qty_raw;
391
392 let adjusted_qty = Quantity::from_raw(adjusted_qty_raw, qty.precision);
393 fills[adjusted_len] = (price, adjusted_qty);
394 adjusted_len += 1;
395 }
396
397 fills.truncate(adjusted_len);
398 fills
399 }
400
401 fn seed_trade_consumption(
402 &mut self,
403 trade_price_raw: PriceRaw,
404 trade_size_raw: QuantityRaw,
405 trade_ts_event: UnixNanos,
406 aggressor_side: AggressorSide,
407 ) {
408 if trade_size_raw == 0 {
409 return;
410 }
411
412 if self.book.ts_last > trade_ts_event {
415 return;
416 }
417
418 let book = &self.book;
419 let consumption = match aggressor_side {
420 AggressorSide::Buy => &mut self.ask_consumption,
421 AggressorSide::Sell => &mut self.bid_consumption,
422 AggressorSide::NoAggressor => return,
423 };
424
425 let mut remaining = trade_size_raw;
426
427 match aggressor_side {
428 AggressorSide::Buy => {
429 for level in book
430 .asks(None)
431 .take_while(|level| level.price.value.raw() <= trade_price_raw)
432 {
433 Self::consume_trade_level(consumption, &mut remaining, level);
434 if remaining == 0 {
435 break;
436 }
437 }
438 }
439 AggressorSide::Sell => {
440 for level in book
441 .bids(None)
442 .take_while(|level| level.price.value.raw() >= trade_price_raw)
443 {
444 Self::consume_trade_level(consumption, &mut remaining, level);
445 if remaining == 0 {
446 break;
447 }
448 }
449 }
450 AggressorSide::NoAggressor => unreachable!(),
451 }
452 }
453
454 fn consume_trade_level(
455 consumption: &mut IndexMap<PriceRaw, (QuantityRaw, QuantityRaw)>,
456 remaining: &mut QuantityRaw,
457 level: &BookLevel,
458 ) {
459 let level_size = level.size_raw();
460 let entry = consumption
461 .entry(level.price.value.raw())
462 .or_insert((level_size, 0));
463
464 if entry.0 != level_size {
466 entry.0 = level_size;
467 entry.1 = 0;
468 }
469
470 let available = level_size.saturating_sub(entry.1);
471 let consume = min(*remaining, available);
472 entry.1 += consume;
473 *remaining -= consume;
474 }
475
476 pub fn set_fill_model(&mut self, fill_model: FillModelHandle) {
478 self.core
479 .set_fill_limit_inside_spread(Self::fill_limit_inside_spread_or_false(&fill_model));
480 self.fill_model = fill_model;
481 }
482
483 fn fill_limit_inside_spread_or_false(fill_model: &FillModelHandle) -> bool {
484 fill_model.fill_limit_inside_spread().unwrap_or_else(|e| {
485 log::error!("Failed to query fill model spread behavior: {e}");
486 false
487 })
488 }
489
490 fn snapshot_queue_position(&mut self, order: &OrderAny, price: Price) {
491 if !self.config.queue_position {
492 return;
493 }
494 let size_prec = self.instrument.size_precision();
495
496 let qty_ahead = self.book.get_quantity_at_level(
499 price,
500 OrderCore::opposite_side(order.order_side()),
501 size_prec,
502 );
503
504 let client_order_id = order.client_order_id();
505
506 self.remove_queue_position(client_order_id);
507 self.queue_ids_by_price
508 .entry(price.raw())
509 .or_default()
510 .insert(client_order_id);
511
512 if self.book_type == BookType::L1_MBP && qty_ahead.is_zero() {
517 let behind_bbo = match order.order_side() {
518 OrderSide::Buy => self.book.best_bid_price().is_some_and(|bid| price < bid),
519 OrderSide::Sell => self.book.best_ask_price().is_some_and(|ask| price > ask),
520 };
521
522 if behind_bbo {
523 self.queue_pending.insert(client_order_id, price.raw());
524 return;
525 }
526 }
527
528 self.queue_ahead_total
529 .insert(client_order_id, (price.raw(), qty_ahead.raw()));
530
531 if self.book_type == BookType::L3_MBO {
533 let orders_ahead: IndexMap<OrderId, QuantityRaw> = self
534 .book
535 .get_orders_at_level(price, OrderCore::opposite_side(order.order_side()))
536 .iter()
537 .map(|book_order| (book_order.order_id, book_order.size.raw()))
538 .collect();
539 self.queue_ahead_orders
540 .insert(client_order_id, orders_ahead);
541 }
542 }
543
544 fn remove_queue_position(&mut self, client_order_id: ClientOrderId) {
545 let pending_price = self.queue_pending.shift_remove(&client_order_id);
546 let ahead_price = self
547 .queue_ahead_total
548 .shift_remove(&client_order_id)
549 .map(|(price_raw, _)| price_raw);
550 self.queue_ahead_orders.shift_remove(&client_order_id);
551 self.queue_excess.shift_remove(&client_order_id);
552
553 for price_raw in [pending_price, ahead_price].into_iter().flatten() {
554 let remove_price = self
555 .queue_ids_by_price
556 .get_mut(&price_raw)
557 .is_some_and(|ids| {
558 ids.shift_remove(&client_order_id);
559 ids.is_empty()
560 });
561
562 if remove_price {
563 self.queue_ids_by_price.shift_remove(&price_raw);
564 }
565 }
566 }
567
568 fn take_queue_ids_at_price(&mut self, price_raw: PriceRaw) -> Vec<ClientOrderId> {
569 let mut ids = Self::take_cleared(&mut self.queue_id_scratch);
570 if let Some(tracked_ids) = self.queue_ids_by_price.get(&price_raw) {
571 ids.extend(tracked_ids.iter().copied());
572 }
573
574 ids
575 }
576
577 fn decrement_queue_on_trade(
578 &mut self,
579 price_raw: PriceRaw,
580 trade_size_raw: QuantityRaw,
581 aggressor_side: AggressorSide,
582 ) {
583 if !self.config.queue_position {
584 return;
585 }
586
587 self.queue_excess.clear();
588
589 let keys = self.take_queue_ids_at_price(price_raw);
590 let mut entries = Self::take_cleared(&mut self.queue_entry_scratch);
591 let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
592
593 for client_order_id in keys.iter().copied() {
594 let (order_price_raw, ahead_raw) =
595 match self.queue_ahead_total.get(&client_order_id).copied() {
596 Some(v) => v,
597 None => continue,
598 };
599
600 let cache = self.cache.borrow();
601 let order_info = cache.order(&client_order_id).and_then(|order| {
602 if order.is_closed() {
603 return None;
604 }
605 let has_pending_updates = self
606 .pending_order_updates
607 .borrow()
608 .contains_key(&client_order_id);
609 let has_pending_fills = self
610 .cached_filled_qty
611 .get(&client_order_id)
612 .is_some_and(|filled_qty| *filled_qty != order.filled_qty());
613 let snapshot;
614 let order = if has_pending_updates || has_pending_fills {
615 snapshot = self.order_snapshot(client_order_id)?;
616 &snapshot
617 } else {
618 &order
619 };
620
621 Some((order.order_side(), order.leaves_qty().raw()))
622 });
623 drop(cache);
624
625 let Some((order_side, leaves_raw)) = order_info else {
626 stale.push(client_order_id);
627 continue;
628 };
629
630 if order_price_raw != price_raw || ahead_raw == 0 {
631 continue;
632 }
633
634 let should_decrement = matches!(aggressor_side, AggressorSide::NoAggressor)
635 || (aggressor_side == AggressorSide::Buy && order_side == OrderSide::Sell)
636 || (aggressor_side == AggressorSide::Sell && order_side == OrderSide::Buy);
637
638 if should_decrement {
639 entries.push((client_order_id, ahead_raw, leaves_raw));
640 }
641 }
642
643 for id in stale.drain(..) {
644 self.remove_queue_position(id);
645 }
646
647 entries.sort_by_key(|&(_, ahead, _)| ahead);
649
650 let mut remaining = trade_size_raw;
651 let mut prev_position: QuantityRaw = 0;
652
653 for (client_order_id, ahead_raw, leaves_raw) in &entries {
654 if remaining == 0 {
655 let new_ahead = ahead_raw.saturating_sub(trade_size_raw);
656 self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, new_ahead);
657 if new_ahead == 0 {
658 self.queue_excess.insert(*client_order_id, 0);
660 }
661 continue;
662 }
663
664 let gap = ahead_raw.saturating_sub(prev_position);
666 let queue_consumed = remaining.min(gap);
667 remaining -= queue_consumed;
668
669 if remaining == 0 && queue_consumed < gap {
670 let new_ahead = ahead_raw.saturating_sub(trade_size_raw);
671 self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, new_ahead);
672 continue;
673 }
674
675 self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, 0);
676 let excess = remaining.min(*leaves_raw);
677 self.queue_excess.insert(*client_order_id, excess);
678 remaining -= excess;
679 prev_position = ahead_raw + excess;
680 }
681
682 self.queue_id_scratch = keys;
683 self.queue_entry_scratch = entries;
684 self.queue_stale_scratch = stale;
685 }
686
687 fn reduce_queue_ahead(
691 &mut self,
692 client_order_id: ClientOrderId,
693 price_raw: PriceRaw,
694 ahead_raw: QuantityRaw,
695 new_ahead_raw: QuantityRaw,
696 ) {
697 self.queue_ahead_total
698 .insert(client_order_id, (price_raw, new_ahead_raw));
699 self.consume_queue_ahead_orders(client_order_id, ahead_raw.saturating_sub(new_ahead_raw));
700 }
701
702 fn consume_queue_ahead_orders(
704 &mut self,
705 client_order_id: ClientOrderId,
706 mut amount_raw: QuantityRaw,
707 ) {
708 let Some(orders_ahead) = self.queue_ahead_orders.get_mut(&client_order_id) else {
709 return;
710 };
711
712 while amount_raw > 0 {
713 let Some((&book_order_id, &size_raw)) = orders_ahead.get_index(0) else {
714 break;
715 };
716
717 if size_raw <= amount_raw {
718 orders_ahead.shift_remove(&book_order_id);
719 amount_raw -= size_raw;
720 } else {
721 orders_ahead.insert(book_order_id, size_raw - amount_raw);
722 amount_raw = 0;
723 }
724 }
725 }
726
727 fn determine_trade_fill_qty(&self, order: &OrderAny) -> Option<QuantityRaw> {
728 if !self.config.queue_position {
729 return Some(order.leaves_qty().raw());
730 }
731
732 let client_order_id = order.client_order_id();
733
734 if self.queue_pending.contains_key(&client_order_id) {
736 return None;
737 }
738
739 if let Some(&(tracked_price_raw, ahead_raw)) = self.queue_ahead_total.get(&client_order_id)
740 && let Some(order_price) = order.price()
741 && order_price.raw() == tracked_price_raw
742 && ahead_raw > 0
743 {
744 return None;
745 }
746
747 let leaves_raw = order.leaves_qty().raw();
748 if leaves_raw == 0 {
749 return None;
750 }
751
752 let mut available_raw = leaves_raw;
753
754 if let Some(trade_size) = self.last_trade_size {
756 let remaining = trade_size.raw().saturating_sub(self.trade_consumption);
757 available_raw = available_raw.min(remaining);
758
759 if let Some(&excess_raw) = self.queue_excess.get(&client_order_id) {
760 if excess_raw == 0 {
761 return None;
762 }
763 available_raw = available_raw.min(excess_raw);
764 }
765 }
766
767 if available_raw == 0 {
768 return None;
769 }
770
771 Some(available_raw)
772 }
773
774 fn rebase_queue_positions(&mut self) {
781 if !self.config.queue_position {
782 return;
783 }
784
785 let tracked: Vec<_> = self
786 .queue_ahead_total
787 .iter()
788 .map(|(&client_order_id, &(price_raw, ahead_raw))| {
789 (client_order_id, price_raw, ahead_raw)
790 })
791 .collect();
792 let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
793 let size_precision = self.instrument.size_precision();
794 let price_precision = self.instrument.price_precision();
795
796 for (client_order_id, price_raw, ahead_raw) in tracked {
797 let order_side = self
798 .cache
799 .borrow()
800 .order(&client_order_id)
801 .and_then(|order| {
802 if order.is_closed() {
803 None
804 } else {
805 Some(order.order_side())
806 }
807 });
808
809 let Some(order_side) = order_side else {
810 stale.push(client_order_id);
811 continue;
812 };
813
814 let price = Price::from_raw(price_raw, price_precision);
815 let visible_raw = self
816 .book
817 .get_quantity_at_level(price, OrderCore::opposite_side(order_side), size_precision)
818 .raw();
819 let rebased_raw = ahead_raw.min(visible_raw);
820
821 if self.book_type == BookType::L3_MBO {
822 let previous_orders = self
823 .queue_ahead_orders
824 .get(&client_order_id)
825 .cloned()
826 .unwrap_or_default();
827 let mut orders_ahead = IndexMap::new();
828 let mut total_raw = 0;
829
830 for book_order in self
831 .book
832 .get_orders_at_level(price, OrderCore::opposite_side(order_side))
833 {
834 if !previous_orders.contains_key(&book_order.order_id) {
835 continue;
836 }
837
838 let previous_size_raw = previous_orders[&book_order.order_id];
839 let size_raw = previous_size_raw.min(book_order.size.raw());
840 orders_ahead.insert(book_order.order_id, size_raw);
841 total_raw += size_raw;
842 }
843
844 self.queue_ahead_orders
845 .insert(client_order_id, orders_ahead);
846 self.queue_ahead_total
847 .insert(client_order_id, (price_raw, total_raw));
848 } else {
849 self.queue_ahead_total
850 .insert(client_order_id, (price_raw, rebased_raw));
851 }
852 }
853
854 for client_order_id in stale.drain(..) {
855 self.remove_queue_position(client_order_id);
856 }
857
858 self.queue_stale_scratch = stale;
859 }
860
861 fn adjust_queue_for_delta(&mut self, delta: &OrderBookDelta) {
862 if delta.action == BookAction::Delete {
863 if self.is_order_granular_delta(delta.flags) {
864 self.advance_l3_queue_on_delete(delta.order.order_id);
865 } else {
866 self.clear_queue_on_delete(delta.order.price.raw(), delta.order.side);
867 }
868 } else if delta.action == BookAction::Update {
869 if self.is_order_granular_delta(delta.flags) {
870 self.adjust_l3_queue_on_update(&delta.order);
871 } else {
872 self.cap_queue_ahead(
873 delta.order.price.raw(),
874 delta.order.size.raw(),
875 delta.order.side,
876 );
877 }
878 }
879 }
880
881 fn clear_queue_on_delete(
882 &mut self,
883 deleted_price_raw: PriceRaw,
884 deleted_side: Option<OrderSide>,
885 ) {
886 let keys = self.take_queue_ids_at_price(deleted_price_raw);
887 for client_order_id in keys.iter().copied() {
888 if let Some(&(order_price_raw, ahead_raw)) =
889 self.queue_ahead_total.get(&client_order_id)
890 && order_price_raw == deleted_price_raw
891 {
892 let matches_side = self
893 .cache
894 .borrow()
895 .order(&client_order_id)
896 .is_some_and(|o| Some(o.order_side()) == deleted_side);
897
898 if matches_side {
899 self.reduce_queue_ahead(client_order_id, order_price_raw, ahead_raw, 0);
900 }
901 }
902 }
903
904 self.queue_id_scratch = keys;
905 }
906
907 fn is_order_granular_delta(&self, flags: u8) -> bool {
910 self.book_type == BookType::L3_MBO
911 && !RecordFlag::F_TOB.matches(flags)
912 && !RecordFlag::F_MBP.matches(flags)
913 }
914
915 fn advance_l3_queue_on_delete(&mut self, book_order_id: OrderId) {
916 for (client_order_id, orders_ahead) in &mut self.queue_ahead_orders {
917 let Some(size_raw) = orders_ahead.shift_remove(&book_order_id) else {
918 continue;
919 };
920
921 if let Some((_, ahead_raw)) = self.queue_ahead_total.get_mut(client_order_id) {
922 *ahead_raw = ahead_raw.saturating_sub(size_raw);
923 }
924 }
925 }
926
927 fn adjust_l3_queue_on_update(&mut self, book_order: &BookOrder) {
933 for (client_order_id, orders_ahead) in &mut self.queue_ahead_orders {
934 let Some(&tracked_size_raw) = orders_ahead.get(&book_order.order_id) else {
935 continue;
936 };
937 let Some((tracked_price_raw, ahead_raw)) =
938 self.queue_ahead_total.get_mut(client_order_id)
939 else {
940 continue;
941 };
942
943 if book_order.price.raw() != *tracked_price_raw {
944 *ahead_raw = ahead_raw.saturating_sub(tracked_size_raw);
945 orders_ahead.shift_remove(&book_order.order_id);
946 } else if book_order.size.raw() < tracked_size_raw {
947 *ahead_raw = ahead_raw.saturating_sub(tracked_size_raw - book_order.size.raw());
949 orders_ahead.insert(book_order.order_id, book_order.size.raw());
950 } else if book_order.size.raw() > tracked_size_raw {
951 *ahead_raw = ahead_raw.saturating_add(book_order.size.raw() - tracked_size_raw);
952 orders_ahead.insert(book_order.order_id, book_order.size.raw());
953 }
954 }
955 }
956
957 fn cap_queue_ahead(
958 &mut self,
959 price_raw: PriceRaw,
960 size_raw: QuantityRaw,
961 order_side: Option<OrderSide>,
962 ) {
963 let keys = self.take_queue_ids_at_price(price_raw);
964 let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
965
966 for client_order_id in keys.iter().copied() {
967 let (order_price_raw, ahead_raw) =
968 match self.queue_ahead_total.get(&client_order_id).copied() {
969 Some(v) => v,
970 None => continue,
971 };
972
973 if order_price_raw != price_raw || ahead_raw <= size_raw {
974 continue;
975 }
976
977 let cache = self.cache.borrow();
978 let order_info = cache.order(&client_order_id).and_then(|order| {
979 if order.is_closed() {
980 None
981 } else {
982 Some(order.order_side())
983 }
984 });
985 drop(cache);
986
987 let Some(side) = order_info else {
988 stale.push(client_order_id);
989 continue;
990 };
991
992 if Some(side) != order_side {
993 continue;
994 }
995
996 self.reduce_queue_ahead(client_order_id, order_price_raw, ahead_raw, size_raw);
997 }
998
999 for id in stale.drain(..) {
1000 self.remove_queue_position(id);
1001 }
1002
1003 self.queue_id_scratch = keys;
1004 self.queue_stale_scratch = stale;
1005 }
1006
1007 fn seed_tob_baseline(&mut self) {
1008 let bid = self.book.best_bid_price();
1009 let ask = self.book.best_ask_price();
1010 self.prev_bid_price_raw = bid.map_or(0, |p| p.raw());
1011 self.prev_ask_price_raw = ask.map_or(0, |p| p.raw());
1012 self.tob_initialized = bid.is_some() || ask.is_some();
1013 }
1014
1015 fn decrement_l1_queue_on_quote(
1016 &mut self,
1017 bid_price_raw: PriceRaw,
1018 bid_size_raw: QuantityRaw,
1019 ask_price_raw: PriceRaw,
1020 ask_size_raw: QuantityRaw,
1021 ) {
1022 if !self.config.queue_position {
1023 return;
1024 }
1025
1026 if self.tob_initialized {
1028 if bid_price_raw < self.prev_bid_price_raw {
1030 self.adjust_l1_queue_on_price_move(bid_price_raw, bid_size_raw, OrderSide::Buy);
1031 }
1032
1033 if ask_price_raw > self.prev_ask_price_raw {
1035 self.adjust_l1_queue_on_price_move(ask_price_raw, ask_size_raw, OrderSide::Sell);
1036 }
1037 }
1038
1039 self.resolve_pending_l1_snapshots(bid_price_raw, bid_size_raw, ask_price_raw, ask_size_raw);
1041 self.cap_queue_ahead(bid_price_raw, bid_size_raw, Some(OrderSide::Buy));
1042 self.cap_queue_ahead(ask_price_raw, ask_size_raw, Some(OrderSide::Sell));
1043 }
1044
1045 fn adjust_l1_queue_on_price_move(
1046 &mut self,
1047 new_price_raw: PriceRaw,
1048 new_size_raw: QuantityRaw,
1049 order_side: OrderSide,
1050 ) {
1051 let mut keys = Self::take_cleared(&mut self.queue_id_scratch);
1052 keys.extend(self.queue_ahead_total.keys().copied());
1053 let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
1054
1055 for client_order_id in keys.iter().copied() {
1056 let Some(&(order_price_raw, ahead_raw)) = self.queue_ahead_total.get(&client_order_id)
1057 else {
1058 continue;
1059 };
1060
1061 let cache = self.cache.borrow();
1062 let order_info = cache.order(&client_order_id).and_then(|order| {
1063 if order.is_closed() {
1064 None
1065 } else {
1066 Some(order.order_side())
1067 }
1068 });
1069 drop(cache);
1070
1071 let Some(side) = order_info else {
1072 stale.push(client_order_id);
1073 continue;
1074 };
1075
1076 if side != order_side {
1077 continue;
1078 }
1079
1080 let crossed = match order_side {
1083 OrderSide::Buy => order_price_raw > new_price_raw,
1084 _ => order_price_raw < new_price_raw,
1085 };
1086
1087 if crossed {
1088 self.queue_ahead_total
1089 .insert(client_order_id, (order_price_raw, 0));
1090 } else if order_price_raw == new_price_raw && ahead_raw > new_size_raw {
1091 self.queue_ahead_total
1092 .insert(client_order_id, (order_price_raw, new_size_raw));
1093 }
1094 }
1095
1096 for id in stale.drain(..) {
1097 self.remove_queue_position(id);
1098 }
1099
1100 let mut pending = Self::take_cleared(&mut self.queue_pending_scratch);
1101 pending.extend(
1102 self.queue_pending
1103 .iter()
1104 .map(|(&client_order_id, &price_raw)| (client_order_id, price_raw)),
1105 );
1106
1107 for (client_order_id, order_price_raw) in pending.iter().copied() {
1108 let cache = self.cache.borrow();
1109 let order_info = cache.order(&client_order_id).and_then(|order| {
1110 if order.is_closed() {
1111 None
1112 } else {
1113 Some(order.order_side())
1114 }
1115 });
1116 drop(cache);
1117
1118 let Some(side) = order_info else {
1119 stale.push(client_order_id);
1120 continue;
1121 };
1122
1123 if side != order_side {
1124 continue;
1125 }
1126
1127 let crossed = match order_side {
1128 OrderSide::Buy => order_price_raw > new_price_raw,
1129 _ => order_price_raw < new_price_raw,
1130 };
1131
1132 if crossed {
1133 self.queue_pending.shift_remove(&client_order_id);
1134 self.queue_ahead_total
1135 .insert(client_order_id, (order_price_raw, 0));
1136 } else if order_price_raw == new_price_raw {
1137 self.queue_pending.shift_remove(&client_order_id);
1138 self.queue_ahead_total
1139 .insert(client_order_id, (order_price_raw, new_size_raw));
1140 }
1141 }
1142
1143 for id in stale.drain(..) {
1144 self.remove_queue_position(id);
1145 }
1146
1147 self.queue_id_scratch = keys;
1148 self.queue_pending_scratch = pending;
1149 self.queue_stale_scratch = stale;
1150 }
1151
1152 fn resolve_pending_l1_snapshots(
1153 &mut self,
1154 bid_price_raw: PriceRaw,
1155 bid_size_raw: QuantityRaw,
1156 ask_price_raw: PriceRaw,
1157 ask_size_raw: QuantityRaw,
1158 ) {
1159 let mut keys = self.take_queue_ids_at_price(bid_price_raw);
1160 if ask_price_raw != bid_price_raw
1161 && let Some(ask_ids) = self.queue_ids_by_price.get(&ask_price_raw)
1162 {
1163 keys.extend(ask_ids.iter().copied());
1164 }
1165
1166 let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
1167
1168 for client_order_id in keys.iter().copied() {
1169 let Some(&order_price_raw) = self.queue_pending.get(&client_order_id) else {
1170 continue;
1171 };
1172
1173 let cache = self.cache.borrow();
1174 let order_info = cache.order(&client_order_id).and_then(|order| {
1175 if order.is_closed() {
1176 None
1177 } else {
1178 Some(order.order_side())
1179 }
1180 });
1181 drop(cache);
1182
1183 let Some(side) = order_info else {
1184 stale.push(client_order_id);
1185 continue;
1186 };
1187
1188 let matched_size = match side {
1190 OrderSide::Buy if order_price_raw == bid_price_raw => Some(bid_size_raw),
1191 OrderSide::Sell if order_price_raw == ask_price_raw => Some(ask_size_raw),
1192 _ => None,
1193 };
1194
1195 if let Some(size) = matched_size {
1196 self.queue_pending.shift_remove(&client_order_id);
1197 self.queue_ahead_total
1198 .insert(client_order_id, (order_price_raw, size));
1199 }
1200 }
1201
1202 for id in stale.drain(..) {
1203 self.remove_queue_position(id);
1204 }
1205
1206 self.queue_id_scratch = keys;
1207 self.queue_stale_scratch = stale;
1208 }
1209
1210 fn resolve_pending_on_trade(&mut self, trade_price_raw: PriceRaw) {
1211 let mut keys = Self::take_cleared(&mut self.queue_id_scratch);
1212 keys.extend(self.queue_pending.keys().copied());
1213 let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
1214
1215 for client_order_id in keys.iter().copied() {
1216 let Some(&order_price_raw) = self.queue_pending.get(&client_order_id) else {
1217 continue;
1218 };
1219
1220 let cache = self.cache.borrow();
1221 let order_side = cache.order(&client_order_id).and_then(|order| {
1222 if order.is_closed() {
1223 None
1224 } else {
1225 Some(order.order_side())
1226 }
1227 });
1228 drop(cache);
1229
1230 let Some(side) = order_side else {
1231 stale.push(client_order_id);
1232 continue;
1233 };
1234
1235 let crossed = match side {
1237 OrderSide::Buy => trade_price_raw < order_price_raw,
1238 OrderSide::Sell => trade_price_raw > order_price_raw,
1239 };
1240
1241 if crossed {
1242 self.queue_pending.shift_remove(&client_order_id);
1243 self.queue_ahead_total
1244 .insert(client_order_id, (order_price_raw, 0));
1245 }
1246 }
1247
1248 for id in stale.drain(..) {
1249 self.remove_queue_position(id);
1250 }
1251
1252 self.queue_id_scratch = keys;
1253 self.queue_stale_scratch = stale;
1254 }
1255
1256 fn take_cleared<T>(buf: &mut Vec<T>) -> Vec<T> {
1257 let mut items = mem::take(buf);
1258 items.clear();
1259 items
1260 }
1261
1262 #[must_use]
1263 pub fn best_bid_price(&self) -> Option<Price> {
1265 self.book.best_bid_price()
1266 }
1267
1268 #[must_use]
1269 pub fn best_ask_price(&self) -> Option<Price> {
1271 self.book.best_ask_price()
1272 }
1273
1274 #[must_use]
1275 pub const fn get_book(&self) -> &OrderBook {
1277 &self.book
1278 }
1279
1280 #[must_use]
1281 pub fn get_open_bid_orders(&self) -> Vec<RestingOrder> {
1283 self.core.get_orders_bid()
1284 }
1285
1286 #[must_use]
1287 pub fn get_open_ask_orders(&self) -> Vec<RestingOrder> {
1289 self.core.get_orders_ask()
1290 }
1291
1292 #[must_use]
1293 pub fn get_open_orders(&self) -> Vec<RestingOrder> {
1295 self.core.get_orders()
1296 }
1297
1298 #[must_use]
1299 pub fn order_exists(&self, client_order_id: ClientOrderId) -> bool {
1301 self.core.order_exists(client_order_id)
1302 }
1303
1304 #[must_use]
1305 pub fn cached_filled_qty_len(&self) -> usize {
1307 self.cached_filled_qty.len()
1308 }
1309
1310 #[must_use]
1311 pub const fn get_core(&self) -> &OrderMatchingCore {
1312 &self.core
1313 }
1314
1315 pub fn set_fill_at_market(&mut self, value: bool) {
1316 self.fill_at_market = value;
1317 }
1318
1319 pub fn update_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> {
1325 if instrument.id() != self.instrument.id() {
1326 anyhow::bail!(
1327 "Cannot update instrument {} with {}",
1328 self.instrument.id(),
1329 instrument.id()
1330 );
1331 }
1332
1333 let changed = instrument.price_increment() != self.instrument.price_increment()
1334 || instrument.price_precision() != self.instrument.price_precision()
1335 || instrument.size_precision() != self.instrument.size_precision();
1336
1337 if changed {
1338 self.core
1339 .update_price_increment(instrument.price_increment());
1340 self.book.reset();
1341 self.trade_consumption = 0;
1342 self.bid_consumption.clear();
1343 self.ask_consumption.clear();
1344 self.queue_pending.clear();
1345 self.queue_ahead_orders.clear();
1346 self.queue_ahead_total.clear();
1347 self.queue_ids_by_price.clear();
1348 self.queue_excess.clear();
1349 self.prev_bid_price_raw = 0;
1350 self.prev_ask_price_raw = 0;
1351 self.tob_initialized = false;
1352 self.last_quote_bid = None;
1353 self.last_quote_ask = None;
1354 self.precision_mismatch_streak = 0;
1355 self.target_bid = None;
1356 self.target_ask = None;
1357 self.target_last = None;
1358 self.last_bar_bid = None;
1359 self.last_bar_ask = None;
1360 self.core.bid = None;
1361 self.core.ask = None;
1362 self.core.last = None;
1363 log::info!(
1364 "Updated instrument {} (price_precision={} size_precision={})",
1365 instrument.id(),
1366 instrument.price_precision(),
1367 instrument.size_precision()
1368 );
1369 }
1370
1371 self.instrument = instrument;
1372
1373 if changed {
1374 self.drop_incompatible_core_orders();
1375 }
1376
1377 Ok(())
1378 }
1379
1380 fn check_price_precision(&self, actual: u8, field: &str) -> anyhow::Result<()> {
1381 let expected = self.instrument.price_precision();
1382 if actual != expected {
1383 anyhow::bail!(
1384 "Invalid {field} precision {actual}, expected {expected} for {}",
1385 self.instrument.id()
1386 );
1387 }
1388 Ok(())
1389 }
1390
1391 fn check_size_precision(&self, actual: u8, field: &str) -> anyhow::Result<()> {
1392 let expected = self.instrument.size_precision();
1393 if actual != expected {
1394 anyhow::bail!(
1395 "Invalid {field} precision {actual}, expected {expected} for {}",
1396 self.instrument.id()
1397 );
1398 }
1399 Ok(())
1400 }
1401
1402 fn log_precision_mismatch(
1403 &mut self,
1404 data_type: &str,
1405 instrument_id: InstrumentId,
1406 err: &anyhow::Error,
1407 ) {
1408 self.precision_mismatch_streak = self.precision_mismatch_streak.saturating_add(1);
1409 let streak = self.precision_mismatch_streak;
1410
1411 if streak <= 3 || streak.is_multiple_of(100) {
1412 log::warn!(
1413 "Skipping {data_type} for {instrument_id}: {err} \
1414 (consecutive_precision_mismatches={streak})"
1415 );
1416 }
1417
1418 if streak == 20 {
1419 log::error!(
1420 "Precision mismatches reached {streak} consecutive events for \
1421 {instrument_id}; check instrument update flow and upstream market data"
1422 );
1423 }
1424 }
1425
1426 fn drop_incompatible_core_orders(&mut self) {
1427 let client_order_ids: Vec<ClientOrderId> = self
1428 .core
1429 .iter_orders()
1430 .filter(|order| {
1431 !self.resting_order_matches_current_instrument(order)
1432 || !self.cached_order_matches_current_instrument(order.client_order_id)
1433 })
1434 .map(|order| order.client_order_id)
1435 .collect();
1436
1437 for client_order_id in client_order_ids {
1438 let order = self
1439 .cache
1440 .borrow()
1441 .order(&client_order_id)
1442 .map(|o| o.clone());
1443
1444 if let Some(order) = order
1445 && (order.is_inflight() || order.is_open())
1446 {
1447 log::warn!(
1448 "Canceling order {client_order_id} after instrument update: \
1449 price, trigger price, or quantity is not compatible with {}",
1450 self.instrument.id()
1451 );
1452 self.cancel_order(&order, None);
1453 } else {
1454 self.delete_core_order(client_order_id);
1455 self.cached_filled_qty.swap_remove(&client_order_id);
1456 }
1457 }
1458 }
1459
1460 fn cached_order_matches_current_instrument(&self, client_order_id: ClientOrderId) -> bool {
1461 self.cache
1462 .borrow()
1463 .order(&client_order_id)
1464 .is_none_or(|order| {
1465 Self::quantity_matches_precision(order.quantity(), self.instrument.size_precision())
1466 })
1467 }
1468
1469 fn resting_order_matches_current_instrument(&self, order: &RestingOrder) -> bool {
1470 order
1471 .limit_price
1472 .is_none_or(|price| self.price_matches_current_instrument(price))
1473 && order
1474 .trigger_price
1475 .is_none_or(|price| self.price_matches_current_instrument(price))
1476 }
1477
1478 fn price_matches_current_instrument(&self, price: Price) -> bool {
1479 Self::price_matches_precision(price, self.instrument.price_precision())
1480 && Self::price_matches_tick(price, self.instrument.price_increment())
1481 }
1482
1483 fn price_matches_precision(price: Price, precision: u8) -> bool {
1484 let precision_diff = FIXED_PRECISION.saturating_sub(precision);
1485 let scale = PriceRaw::pow(10, u32::from(precision_diff));
1486 price.raw() % scale == 0
1487 }
1488
1489 fn price_matches_tick(price: Price, increment: Price) -> bool {
1490 let increment_raw = increment.raw().abs();
1491 increment_raw == 0 || price.raw() % increment_raw == 0
1492 }
1493
1494 fn quantity_matches_precision(quantity: Quantity, precision: u8) -> bool {
1495 let precision_diff = FIXED_PRECISION.saturating_sub(precision);
1496 let scale = QuantityRaw::pow(10, u32::from(precision_diff));
1497 quantity.raw().is_multiple_of(scale)
1498 }
1499
1500 fn normalize_price_for_current_instrument(&self, price: Price) -> Option<Price> {
1501 if !self.price_matches_current_instrument(price) {
1502 return None;
1503 }
1504
1505 Some(Price::from_raw(
1506 price.raw(),
1507 self.instrument.price_precision(),
1508 ))
1509 }
1510
1511 fn normalize_quantity_for_current_instrument(&self, quantity: Quantity) -> Option<Quantity> {
1512 let precision = self.instrument.size_precision();
1513 if !Self::quantity_matches_precision(quantity, precision) {
1514 return None;
1515 }
1516
1517 Some(Quantity::from_raw(quantity.raw(), precision))
1518 }
1519
1520 pub fn process_order_book_delta(&mut self, delta: &OrderBookDelta) -> anyhow::Result<()> {
1528 log::debug!("Processing {delta}");
1529
1530 if matches!(delta.action, BookAction::Add | BookAction::Update) {
1532 self.check_price_precision(delta.order.price.precision, "delta order price")?;
1533 self.check_size_precision(delta.order.size.precision, "delta order size")?;
1534 }
1535
1536 if self.book_type == BookType::L1_MBP {
1538 self.iterate(delta.ts_init, AggressorSide::NoAggressor);
1539 return Ok(());
1540 }
1541
1542 self.book.apply_delta(delta)?;
1543
1544 let is_snapshot = RecordFlag::F_SNAPSHOT.matches(delta.flags);
1545 let is_last = RecordFlag::F_LAST.matches(delta.flags);
1546 let is_clear = delta.action == BookAction::Clear;
1547 let snapshot_complete = is_last && (is_snapshot || self.queue_snapshot_in_progress);
1548
1549 if self.config.queue_position {
1550 if is_snapshot && !is_last {
1551 self.queue_snapshot_in_progress = true;
1555 }
1556
1557 if snapshot_complete {
1558 self.queue_snapshot_in_progress = false;
1559 self.rebase_queue_positions();
1560 } else if is_clear && !is_snapshot {
1561 self.rebase_queue_positions();
1562 } else if !self.queue_snapshot_in_progress {
1563 self.adjust_queue_for_delta(delta);
1564 }
1565 }
1566
1567 if self.config.queue_position && (snapshot_complete || (is_clear && !is_snapshot)) {
1568 self.seed_tob_baseline();
1569 }
1570
1571 self.iterate(delta.ts_init, AggressorSide::NoAggressor);
1572 Ok(())
1573 }
1574
1575 pub fn process_order_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
1583 log::debug!("Processing {deltas}");
1584
1585 for delta in &deltas.deltas {
1587 if matches!(delta.action, BookAction::Add | BookAction::Update) {
1588 self.check_price_precision(delta.order.price.precision, "delta order price")?;
1589 self.check_size_precision(delta.order.size.precision, "delta order size")?;
1590 }
1591 }
1592
1593 if self.book_type == BookType::L1_MBP {
1595 self.iterate(deltas.ts_init, AggressorSide::NoAggressor);
1596 return Ok(());
1597 }
1598
1599 self.book.apply_deltas(deltas)?;
1600
1601 let mut has_snapshot_or_clear = false;
1602
1603 if self.config.queue_position {
1604 for delta in &deltas.deltas {
1605 if RecordFlag::F_SNAPSHOT.matches(delta.flags) || delta.action == BookAction::Clear
1606 {
1607 has_snapshot_or_clear = true;
1608 break;
1609 }
1610 self.adjust_queue_for_delta(delta);
1611 }
1612 }
1613
1614 if self.config.queue_position && has_snapshot_or_clear {
1615 self.queue_snapshot_in_progress = false;
1616 self.rebase_queue_positions();
1617 self.seed_tob_baseline();
1618 }
1619
1620 self.iterate(deltas.ts_init, AggressorSide::NoAggressor);
1621 Ok(())
1622 }
1623
1624 pub fn process_order_book_depth(&mut self, depth: &OrderBookDepth) -> anyhow::Result<()> {
1633 log::debug!("Processing OrderBookDepth for {}", depth.instrument_id);
1634
1635 for order in &depth.bids {
1637 if order.side.is_none() || !order.size.is_positive() {
1638 continue;
1639 }
1640 self.check_price_precision(order.price.precision, "bid price")?;
1641 self.check_size_precision(order.size.precision, "bid size")?;
1642 }
1643
1644 for order in &depth.asks {
1645 if order.side.is_none() || !order.size.is_positive() {
1646 continue;
1647 }
1648 self.check_price_precision(order.price.precision, "ask price")?;
1649 self.check_size_precision(order.size.precision, "ask size")?;
1650 }
1651
1652 let top_bid = Self::first_valid_depth_order(&depth.bids, OrderSide::Buy);
1653 let top_ask = Self::first_valid_depth_order(&depth.asks, OrderSide::Sell);
1654
1655 if self.book_type == BookType::L1_MBP {
1658 let quote = QuoteTick::new(
1659 depth.instrument_id,
1660 Self::depth_quote_price(top_bid, self.instrument.price_precision()),
1661 Self::depth_quote_price(top_ask, self.instrument.price_precision()),
1662 Self::depth_quote_size(top_bid, self.instrument.size_precision()),
1663 Self::depth_quote_size(top_ask, self.instrument.size_precision()),
1664 depth.ts_event,
1665 depth.ts_init,
1666 );
1667 self.book.update_quote_tick("e)?;
1668 self.last_quote_bid = top_bid.map(|order| order.price);
1669 self.last_quote_ask = top_ask.map(|order| order.price);
1670 } else {
1671 self.book.apply_depth(depth)?;
1672 }
1673
1674 if self.config.queue_position {
1676 self.rebase_queue_positions();
1677 let bid_price_raw = top_bid.map_or(0, |order| order.price.raw());
1678 let bid_size_raw = top_bid.map_or(0, |order| order.size.raw());
1679 let ask_price_raw = top_ask.map_or(0, |order| order.price.raw());
1680 let ask_size_raw = top_ask.map_or(0, |order| order.size.raw());
1681
1682 self.decrement_l1_queue_on_quote(
1683 bid_price_raw,
1684 bid_size_raw,
1685 ask_price_raw,
1686 ask_size_raw,
1687 );
1688
1689 self.prev_bid_price_raw = bid_price_raw;
1690 self.prev_ask_price_raw = ask_price_raw;
1691 self.tob_initialized = true;
1692 }
1693
1694 self.iterate(depth.ts_init, AggressorSide::NoAggressor);
1695 Ok(())
1696 }
1697
1698 fn first_valid_depth_order(orders: &[BookOrder], side: OrderSide) -> Option<BookOrder> {
1699 orders
1700 .iter()
1701 .copied()
1702 .find(|order| order.side == Some(side) && order.size.is_positive())
1703 }
1704
1705 fn depth_quote_price(order: Option<BookOrder>, price_precision: u8) -> Price {
1706 order.map_or_else(|| Price::zero(price_precision), |order| order.price)
1707 }
1708
1709 fn depth_quote_size(order: Option<BookOrder>, size_precision: u8) -> Quantity {
1710 order.map_or_else(|| Quantity::zero(size_precision), |order| order.size)
1711 }
1712
1713 pub fn process_quote_tick(&mut self, quote: &QuoteTick) {
1715 log::debug!("Processing {quote}");
1716
1717 if let Err(e) = self.check_price_precision(quote.bid_price.precision, "bid_price") {
1718 self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1719 return;
1720 }
1721
1722 if let Err(e) = self.check_price_precision(quote.ask_price.precision, "ask_price") {
1723 self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1724 return;
1725 }
1726
1727 if let Err(e) = self.check_size_precision(quote.bid_size.precision, "bid_size") {
1728 self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1729 return;
1730 }
1731
1732 if let Err(e) = self.check_size_precision(quote.ask_size.precision, "ask_size") {
1733 self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1734 return;
1735 }
1736
1737 self.precision_mismatch_streak = 0;
1738
1739 if self.book_type == BookType::L1_MBP {
1740 if quote.ts_event < self.book.ts_last {
1742 log::warn!(
1743 "Skipping stale quote: ts_event {} < book.ts_last {} for {}",
1744 quote.ts_event,
1745 self.book.ts_last,
1746 self.book.instrument_id,
1747 );
1748 self.iterate(quote.ts_init, AggressorSide::NoAggressor);
1749 return;
1750 }
1751
1752 if !self.update_quote_tick_or_skip(quote, "quote tick") {
1753 return;
1754 }
1755
1756 if self.config.queue_position {
1757 self.decrement_l1_queue_on_quote(
1758 quote.bid_price.raw(),
1759 quote.bid_size.raw(),
1760 quote.ask_price.raw(),
1761 quote.ask_size.raw(),
1762 );
1763 self.prev_bid_price_raw = quote.bid_price.raw();
1764 self.prev_ask_price_raw = quote.ask_price.raw();
1765 self.tob_initialized = true;
1766 }
1767 self.last_quote_bid = Some(quote.bid_price);
1768 self.last_quote_ask = Some(quote.ask_price);
1769 }
1770
1771 self.iterate(quote.ts_init, AggressorSide::NoAggressor);
1772 }
1773
1774 pub fn process_bar(&mut self, bar: &Bar) {
1783 log::debug!("Processing {bar}");
1784
1785 debug_assert!(
1786 bar.high >= bar.open
1787 && bar.high >= bar.low
1788 && bar.high >= bar.close
1789 && bar.low <= bar.open
1790 && bar.low <= bar.close,
1791 "OHLC invariant violated for {bar}"
1792 );
1793
1794 if !self.config.bar_execution || self.book_type != BookType::L1_MBP {
1796 return;
1797 }
1798
1799 let bar_type = bar.bar_type;
1800
1801 if bar_type.aggregation_source() == AggregationSource::Internal {
1803 return;
1804 }
1805
1806 if let Err(e) = self.check_price_precision(bar.open.precision, "bar open") {
1807 self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1808 return;
1809 }
1810
1811 if let Err(e) = self.check_price_precision(bar.high.precision, "bar high") {
1812 self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1813 return;
1814 }
1815
1816 if let Err(e) = self.check_price_precision(bar.low.precision, "bar low") {
1817 self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1818 return;
1819 }
1820
1821 if let Err(e) = self.check_price_precision(bar.close.precision, "bar close") {
1822 self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1823 return;
1824 }
1825
1826 if let Err(e) = self.check_size_precision(bar.volume.precision, "bar volume") {
1827 self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1828 return;
1829 }
1830
1831 self.precision_mismatch_streak = 0;
1832
1833 let price_type = bar_type.spec().price_type;
1834 if price_type == PriceType::Mark {
1835 log::warn!(
1836 "Cannot process bar for {} with `PriceType::Mark`, mark price bars are not supported for bar execution",
1837 bar.instrument_id(),
1838 );
1839 return;
1840 }
1841
1842 let execution_bar_type =
1843 if let Some(execution_bar_type) = self.execution_bar_types.get(&bar.instrument_id()) {
1844 execution_bar_type.to_owned()
1845 } else {
1846 self.execution_bar_types
1847 .insert(bar.instrument_id(), bar_type);
1848 self.execution_bar_deltas
1849 .insert(bar_type, bar_type.spec().timedelta());
1850 bar_type
1851 };
1852
1853 if execution_bar_type != bar_type {
1854 let mut bar_type_timedelta = self.execution_bar_deltas.get(&bar_type).copied();
1855 if bar_type_timedelta.is_none() {
1856 bar_type_timedelta = Some(bar_type.spec().timedelta());
1857 self.execution_bar_deltas
1858 .insert(bar_type, bar_type_timedelta.unwrap());
1859 }
1860
1861 if self.execution_bar_deltas.get(&execution_bar_type).unwrap()
1862 >= &bar_type_timedelta.unwrap()
1863 {
1864 self.execution_bar_types
1865 .insert(bar_type.instrument_id(), bar_type);
1866 } else {
1867 return;
1868 }
1869 }
1870
1871 match price_type {
1872 PriceType::Last | PriceType::Mid => self.process_trade_ticks_from_bar(bar),
1873 PriceType::Bid => {
1874 self.last_bar_bid = Some(bar.to_owned());
1875 self.process_quote_ticks_from_bar();
1876 }
1877 PriceType::Ask => {
1878 self.last_bar_ask = Some(bar.to_owned());
1879 self.process_quote_ticks_from_bar();
1880 }
1881 PriceType::Mark => {
1882 unreachable!("PriceType::Mark bars return before execution bar state updates")
1883 }
1884 }
1885 }
1886
1887 fn process_trade_ticks_from_bar(&mut self, bar: &Bar) {
1888 let sizes = BarTickSizes::from_volume(bar.volume, self.instrument.size_increment());
1889
1890 let aggressor_side = if self.core.last.is_none_or(|last| bar.open > last) {
1891 AggressorSide::Buy
1892 } else {
1893 AggressorSide::Sell
1894 };
1895
1896 if self.core.last.is_none() {
1898 self.fill_at_market = true;
1899
1900 if !self.process_bar_trade_tick(
1901 bar,
1902 bar.open,
1903 sizes.open,
1904 aggressor_side,
1905 "bar open trade tick",
1906 ) {
1907 return;
1908 }
1909 self.core.set_last_raw(bar.open);
1910 } else if self.core.last.is_some_and(|last| bar.open != last) {
1911 self.fill_at_market = true;
1913
1914 if !self.process_bar_trade_tick(
1915 bar,
1916 bar.open,
1917 sizes.open,
1918 aggressor_side,
1919 "bar gap-open trade tick",
1920 ) {
1921 return;
1922 }
1923 self.core.set_last_raw(bar.open);
1924 }
1925
1926 let high_first = self.bar_high_first(bar);
1929
1930 if high_first {
1931 self.process_bar_high(bar, sizes.high);
1932 self.process_bar_low(bar, sizes.low);
1933 } else {
1934 self.process_bar_low(bar, sizes.low);
1935 self.process_bar_high(bar, sizes.high);
1936 }
1937
1938 if self.core.last.is_some_and(|last| bar.close != last) {
1940 self.fill_at_market = false;
1941
1942 let aggressor_side = if bar.close > self.core.last.unwrap() {
1943 AggressorSide::Buy
1944 } else {
1945 AggressorSide::Sell
1946 };
1947
1948 if !self.process_bar_trade_tick(
1949 bar,
1950 bar.close,
1951 sizes.close,
1952 aggressor_side,
1953 "bar close trade tick",
1954 ) {
1955 return;
1956 }
1957
1958 self.core.set_last_raw(bar.close);
1959 }
1960
1961 self.fill_at_market = true;
1962 }
1963
1964 fn process_bar_high(&mut self, bar: &Bar, size: Quantity) {
1965 if self.core.last.is_some_and(|last| bar.high > last) {
1966 self.fill_at_market = false;
1967
1968 if !self.process_bar_trade_tick(
1969 bar,
1970 bar.high,
1971 size,
1972 AggressorSide::Buy,
1973 "bar high trade tick",
1974 ) {
1975 return;
1976 }
1977
1978 self.core.set_last_raw(bar.high);
1979 }
1980 }
1981
1982 fn process_bar_low(&mut self, bar: &Bar, size: Quantity) {
1983 if self.core.last.is_some_and(|last| bar.low < last) {
1984 self.fill_at_market = false;
1985
1986 if !self.process_bar_trade_tick(
1987 bar,
1988 bar.low,
1989 size,
1990 AggressorSide::Sell,
1991 "bar low trade tick",
1992 ) {
1993 return;
1994 }
1995
1996 self.core.set_last_raw(bar.low);
1997 }
1998 }
1999
2000 fn process_bar_trade_tick(
2001 &mut self,
2002 bar: &Bar,
2003 price: Price,
2004 size: Quantity,
2005 aggressor_side: AggressorSide,
2006 context: &str,
2007 ) -> bool {
2008 if size.is_zero() {
2009 return true;
2010 }
2011
2012 let trade_tick = TradeTick::new(
2013 bar.instrument_id(),
2014 price,
2015 size,
2016 aggressor_side,
2017 self.ids_generator.generate_trade_id(bar.ts_init),
2018 bar.ts_init,
2019 bar.ts_init,
2020 );
2021
2022 if !self.update_trade_tick_or_skip(&trade_tick, context) {
2023 return false;
2024 }
2025
2026 self.iterate(trade_tick.ts_init, AggressorSide::NoAggressor);
2027 true
2028 }
2029
2030 fn process_quote_ticks_from_bar(&mut self) {
2031 if self.last_bar_bid.is_none()
2033 || self.last_bar_ask.is_none()
2034 || self.last_bar_bid.unwrap().ts_init != self.last_bar_ask.unwrap().ts_init
2035 {
2036 return;
2037 }
2038 let bid_bar = self.last_bar_bid.unwrap();
2039 let ask_bar = self.last_bar_ask.unwrap();
2040
2041 let size_increment = self.instrument.size_increment();
2042 let bid_sizes = BarTickSizes::from_volume(bid_bar.volume, size_increment);
2043 let ask_sizes = BarTickSizes::from_volume(ask_bar.volume, size_increment);
2044 let mut has_current_bid = false;
2045 let mut has_current_ask = false;
2046
2047 let mut quote_tick = QuoteTick::new(
2048 self.book.instrument_id,
2049 bid_bar.open,
2050 ask_bar.open,
2051 bid_sizes.open,
2052 ask_sizes.open,
2053 bid_bar.ts_init,
2054 bid_bar.ts_init,
2055 );
2056
2057 self.fill_at_market = true;
2059
2060 if !self.process_bar_quote_tick(
2061 "e_tick,
2062 "bar open quote tick",
2063 &mut has_current_bid,
2064 &mut has_current_ask,
2065 ) {
2066 return;
2067 }
2068
2069 let high_first = self.bar_high_first(&bid_bar);
2072
2073 let high_leg = (
2074 bid_bar.high,
2075 ask_bar.high,
2076 bid_sizes.high,
2077 ask_sizes.high,
2078 "bar high quote tick",
2079 );
2080 let low_leg = (
2081 bid_bar.low,
2082 ask_bar.low,
2083 bid_sizes.low,
2084 ask_sizes.low,
2085 "bar low quote tick",
2086 );
2087 let legs = if high_first {
2088 [high_leg, low_leg]
2089 } else {
2090 [low_leg, high_leg]
2091 };
2092
2093 for (bid_price, ask_price, bid_size, ask_size, context) in legs {
2095 self.fill_at_market = false;
2096 quote_tick.bid_price = bid_price;
2097 quote_tick.ask_price = ask_price;
2098 quote_tick.bid_size = bid_size;
2099 quote_tick.ask_size = ask_size;
2100
2101 if !self.process_bar_quote_tick(
2102 "e_tick,
2103 context,
2104 &mut has_current_bid,
2105 &mut has_current_ask,
2106 ) {
2107 return;
2108 }
2109 }
2110
2111 self.fill_at_market = false;
2113 quote_tick.bid_price = bid_bar.close;
2114 quote_tick.ask_price = ask_bar.close;
2115 quote_tick.bid_size = bid_sizes.close;
2116 quote_tick.ask_size = ask_sizes.close;
2117
2118 if !self.process_bar_quote_tick(
2119 "e_tick,
2120 "bar close quote tick",
2121 &mut has_current_bid,
2122 &mut has_current_ask,
2123 ) {
2124 return;
2125 }
2126
2127 self.last_bar_bid = None;
2128 self.last_bar_ask = None;
2129 self.fill_at_market = true;
2130 }
2131
2132 fn process_bar_quote_tick(
2133 &mut self,
2134 quote: &QuoteTick,
2135 context: &str,
2136 has_current_bid: &mut bool,
2137 has_current_ask: &mut bool,
2138 ) -> bool {
2139 let has_bid_size = quote.bid_size.non_zero();
2140 let has_ask_size = quote.ask_size.non_zero();
2141 let mut book_changed = false;
2142 let mut bid_cleared = false;
2143 let mut ask_cleared = false;
2144
2145 match (has_bid_size, has_ask_size) {
2146 (true, true) => {
2147 if !self.update_quote_tick_or_skip(quote, context) {
2148 return false;
2149 }
2150 *has_current_bid = true;
2151 *has_current_ask = true;
2152 book_changed = true;
2153 }
2154 _ => {
2155 if has_bid_size {
2156 self.update_bar_quote_bid(quote);
2157 *has_current_bid = true;
2158 book_changed = true;
2159 } else if !*has_current_bid {
2160 self.clear_bar_quote_bid(quote);
2161 *has_current_bid = true;
2162 book_changed = true;
2163 bid_cleared = true;
2164 }
2165
2166 if has_ask_size {
2167 self.update_bar_quote_ask(quote);
2168 *has_current_ask = true;
2169 book_changed = true;
2170 } else if !*has_current_ask {
2171 self.clear_bar_quote_ask(quote);
2172 *has_current_ask = true;
2173 book_changed = true;
2174 ask_cleared = true;
2175 }
2176 }
2177 }
2178
2179 if book_changed
2180 && let (Some(best_bid), Some(best_ask)) =
2181 (self.book.best_bid_price(), self.book.best_ask_price())
2182 && best_bid > best_ask
2183 {
2184 if has_bid_size && !has_ask_size {
2185 self.clear_bar_quote_ask(quote);
2186 ask_cleared = true;
2187 } else if has_ask_size && !has_bid_size {
2188 self.clear_bar_quote_bid(quote);
2189 bid_cleared = true;
2190 }
2191 }
2192
2193 if has_bid_size {
2194 self.last_quote_bid = Some(quote.bid_price);
2195 } else if bid_cleared {
2196 self.last_quote_bid = None;
2197 }
2198
2199 if has_ask_size {
2200 self.last_quote_ask = Some(quote.ask_price);
2201 } else if ask_cleared {
2202 self.last_quote_ask = None;
2203 }
2204
2205 if !book_changed {
2206 return true;
2207 }
2208
2209 self.iterate(quote.ts_init, AggressorSide::NoAggressor);
2210 true
2211 }
2212
2213 fn bar_high_first(&self, bar: &Bar) -> bool {
2214 !self.config.bar_adaptive_high_low_ordering || bar.high - bar.open < bar.open - bar.low
2215 }
2216
2217 fn update_bar_quote_bid(&mut self, quote: &QuoteTick) {
2218 let bid = BookOrder::new(
2219 OrderSide::Buy,
2220 quote.bid_price,
2221 quote.bid_size,
2222 OrderSide::Buy as u64,
2223 );
2224 self.book
2225 .add(bid, 0, self.book.sequence.saturating_add(1), quote.ts_event);
2226 }
2227
2228 fn clear_bar_quote_bid(&mut self, quote: &QuoteTick) {
2229 self.book
2230 .clear_bids(self.book.sequence.saturating_add(1), quote.ts_event);
2231 }
2232
2233 fn update_bar_quote_ask(&mut self, quote: &QuoteTick) {
2234 let ask = BookOrder::new(
2235 OrderSide::Sell,
2236 quote.ask_price,
2237 quote.ask_size,
2238 OrderSide::Sell as u64,
2239 );
2240 self.book
2241 .add(ask, 0, self.book.sequence.saturating_add(1), quote.ts_event);
2242 }
2243
2244 fn clear_bar_quote_ask(&mut self, quote: &QuoteTick) {
2245 self.book
2246 .clear_asks(self.book.sequence.saturating_add(1), quote.ts_event);
2247 }
2248
2249 pub fn process_trade_tick(&mut self, trade: &TradeTick) {
2260 log::debug!("Processing {trade}");
2261
2262 if let Err(e) = self.check_price_precision(trade.price.precision, "trade price") {
2263 self.log_precision_mismatch("trade tick", trade.instrument_id, &e);
2264 return;
2265 }
2266
2267 if let Err(e) = self.check_size_precision(trade.size.precision, "trade size") {
2268 self.log_precision_mismatch("trade tick", trade.instrument_id, &e);
2269 return;
2270 }
2271
2272 self.precision_mismatch_streak = 0;
2273
2274 let price_raw = trade.price.raw();
2275
2276 if self.book_type == BookType::L1_MBP {
2277 if trade.ts_event < self.book.ts_last {
2279 log::warn!(
2280 "Skipping stale trade: ts_event {} < book.ts_last {} for {}",
2281 trade.ts_event,
2282 self.book.ts_last,
2283 self.book.instrument_id,
2284 );
2285 self.iterate(trade.ts_init, AggressorSide::NoAggressor);
2286 return;
2287 }
2288
2289 if !self.update_trade_tick_or_skip(trade, "trade tick") {
2290 return;
2291 }
2292 }
2293
2294 self.core.set_last_raw(trade.price);
2295
2296 if !self.config.trade_execution {
2297 if self.book_type == BookType::L1_MBP {
2298 if let Some(bid) = self.book.best_bid_price() {
2299 self.core.set_bid_raw(bid);
2300 }
2301
2302 if let Some(ask) = self.book.best_ask_price() {
2303 self.core.set_ask_raw(ask);
2304 }
2305 } else {
2306 self.iterate_with_mode(
2307 trade.ts_init,
2308 AggressorSide::NoAggressor,
2309 OrderMatchMode::LastPriceStopTriggers,
2310 );
2311 }
2312 return;
2313 }
2314
2315 let aggressor_side = trade.aggressor_side;
2316
2317 match aggressor_side {
2318 AggressorSide::Buy => {
2319 if self.core.ask.is_none_or(|ask| trade.price > ask) {
2322 self.core.set_ask_raw(trade.price);
2323 }
2324
2325 if self.core.bid.is_none() {
2327 self.core.set_bid_raw(trade.price);
2328 }
2329 }
2330 AggressorSide::Sell => {
2331 if self.core.bid.is_none_or(|bid| trade.price < bid) {
2334 self.core.set_bid_raw(trade.price);
2335 }
2336
2337 if self.core.ask.is_none() {
2339 self.core.set_ask_raw(trade.price);
2340 }
2341 }
2342 AggressorSide::NoAggressor => {
2343 if self.core.bid.is_none_or(|bid| trade.price <= bid) {
2344 self.core.set_bid_raw(trade.price);
2345 }
2346
2347 if self.core.ask.is_none_or(|ask| trade.price >= ask) {
2348 self.core.set_ask_raw(trade.price);
2349 }
2350 }
2351 }
2352
2353 let original_bid = self.core.bid;
2354 let original_ask = self.core.ask;
2355
2356 match aggressor_side {
2357 AggressorSide::Sell => {
2358 if original_ask.is_some_and(|ask| trade.price < ask) {
2359 self.core.set_ask_raw(trade.price);
2360 }
2361 }
2362 AggressorSide::Buy => {
2363 if original_bid.is_some_and(|bid| trade.price > bid) {
2364 self.core.set_bid_raw(trade.price);
2365 }
2366 }
2367 AggressorSide::NoAggressor => {
2368 self.core.set_bid_raw(trade.price);
2370 self.core.set_ask_raw(trade.price);
2371 }
2372 }
2373
2374 self.last_trade_size = Some(trade.size);
2375 self.trade_consumption = 0;
2376
2377 if self.config.liquidity_consumption && self.book_type != BookType::L1_MBP {
2378 self.seed_trade_consumption(
2379 price_raw,
2380 trade.size.raw(),
2381 trade.ts_event,
2382 aggressor_side,
2383 );
2384 }
2385
2386 self.resolve_pending_on_trade(price_raw);
2387 self.decrement_queue_on_trade(price_raw, trade.size.raw(), aggressor_side);
2388
2389 self.iterate(trade.ts_init, aggressor_side);
2390
2391 self.last_trade_size = None;
2392 self.trade_consumption = 0;
2393
2394 if self.book_type == BookType::L1_MBP {
2400 match aggressor_side {
2401 AggressorSide::Sell => {
2402 if let Some(ask) = self.last_quote_ask {
2403 self.core.ask = Some(ask);
2404 }
2405 }
2406 AggressorSide::Buy => {
2407 if let Some(bid) = self.last_quote_bid {
2408 self.core.bid = Some(bid);
2409 }
2410 }
2411 AggressorSide::NoAggressor => {}
2412 }
2413 } else {
2414 match aggressor_side {
2415 AggressorSide::Sell => {
2416 if let Some(ask) = original_ask
2417 && trade.price < ask
2418 {
2419 self.core.ask = Some(ask);
2420 }
2421 }
2422 AggressorSide::Buy => {
2423 if let Some(bid) = original_bid
2424 && trade.price > bid
2425 {
2426 self.core.bid = Some(bid);
2427 }
2428 }
2429 AggressorSide::NoAggressor => {}
2430 }
2431 }
2432 }
2433
2434 fn update_quote_tick_or_skip(&mut self, quote: &QuoteTick, context: &str) -> bool {
2435 if let Err(e) = self.book.update_quote_tick(quote) {
2436 log::warn!(
2437 "Skipping {context} for {}: update_quote_tick failed: {e}",
2438 quote.instrument_id,
2439 );
2440 return false;
2441 }
2442 true
2443 }
2444
2445 fn update_trade_tick_or_skip(&mut self, trade: &TradeTick, context: &str) -> bool {
2446 if let Err(e) = self.book.update_trade_tick(trade) {
2447 log::warn!(
2448 "Skipping {context} for {}: update_trade_tick failed: {e}",
2449 trade.instrument_id,
2450 );
2451 return false;
2452 }
2453 true
2454 }
2455
2456 pub fn process_status(&mut self, action: MarketStatusAction) {
2458 log::debug!("Processing {action}");
2459
2460 match action {
2461 MarketStatusAction::Trading | MarketStatusAction::PreOpen
2462 if matches!(
2463 self.market_status,
2464 MarketStatus::Closed | MarketStatus::Paused | MarketStatus::Suspended
2465 ) =>
2466 {
2467 self.market_status = MarketStatus::Open;
2468 }
2469 MarketStatusAction::Pause if self.market_status == MarketStatus::Open => {
2470 self.market_status = MarketStatus::Paused;
2471 }
2472 MarketStatusAction::Suspend if self.market_status == MarketStatus::Open => {
2473 self.market_status = MarketStatus::Suspended;
2474 }
2475 MarketStatusAction::Halt | MarketStatusAction::Close
2476 if self.market_status == MarketStatus::Open =>
2477 {
2478 self.market_status = MarketStatus::Closed;
2479 }
2480 _ => {}
2481 }
2482 }
2483
2484 pub fn process_instrument_close(&mut self, close: InstrumentClose) {
2489 if close.instrument_id != self.instrument.id() {
2490 log::warn!(
2491 "Received instrument close for unknown instrument_id: {}",
2492 close.instrument_id
2493 );
2494 return;
2495 }
2496
2497 if close.close_type == InstrumentCloseType::ContractExpired {
2498 self.instrument_close = Some(close);
2499 self.iterate(close.ts_init, AggressorSide::NoAggressor);
2500 }
2501 }
2502
2503 pub fn process_instrument_expiration(&mut self, timestamp_ns: UnixNanos) {
2505 self.check_instrument_expiration(timestamp_ns, false);
2506 }
2507
2508 #[must_use]
2510 pub const fn is_expiration_processed(&self) -> bool {
2511 self.expiration_processed
2512 }
2513
2514 fn requires_pending_resolution(&self) -> bool {
2515 matches!(self.instrument, InstrumentAny::BinaryOption(_))
2516 }
2517
2518 fn cancel_open_orders_for_expiration(&mut self) {
2519 let instrument_id = self.instrument.id();
2524 let expiration_order_ids: IndexSet<ClientOrderId> = {
2525 let cache = self.cache.borrow();
2526 let mut order_ids = IndexSet::new();
2527
2528 for order_info in self.get_open_orders() {
2529 order_ids.insert(order_info.client_order_id);
2530 }
2531
2532 for order in cache.orders(None, Some(&instrument_id), None, None, None) {
2533 if order.is_open() || order.is_inflight() {
2534 order_ids.insert(order.client_order_id());
2535 }
2536 }
2537
2538 order_ids
2539 };
2540
2541 for client_order_id in expiration_order_ids {
2542 let order = {
2543 let cache = self.cache.borrow();
2544 cache.order(&client_order_id).map(|order| order.clone())
2545 };
2546
2547 if let Some(order) = order {
2548 self.cancel_order(&order, None);
2549 }
2550 }
2551 }
2552
2553 fn enter_pending_resolution(&mut self) {
2554 if self.pending_resolution {
2555 return;
2556 }
2557
2558 self.pending_resolution = true;
2559 self.market_status = MarketStatus::Closed;
2560 self.cancel_open_orders_for_expiration();
2561 log::info!(
2562 "{} expired and is now pending resolution; open orders canceled and new orders blocked",
2563 self.instrument.id()
2564 );
2565 }
2566
2567 fn check_instrument_expiration(&mut self, timestamp_ns: UnixNanos, defer_settlement: bool) {
2568 if self.expiration_processed || self.option_settlement_failed {
2569 return;
2570 }
2571
2572 let timestamp_triggered = self
2573 .instrument
2574 .expiration_ns()
2575 .is_some_and(|ns| timestamp_ns >= ns);
2576
2577 if !timestamp_triggered && self.instrument_close.is_none() {
2578 return;
2579 }
2580
2581 if self.instrument_close.is_none()
2582 && timestamp_triggered
2583 && self.requires_pending_resolution()
2584 {
2585 self.enter_pending_resolution();
2586 return;
2587 }
2588
2589 if matches!(
2590 self.instrument,
2591 InstrumentAny::OptionContract(_) | InstrumentAny::CryptoOption(_)
2592 ) {
2593 if !self.option_expiration_orders_canceled {
2597 self.option_expiration_orders_canceled = true;
2598 self.enter_pending_resolution();
2599 }
2600
2601 if defer_settlement
2604 && self.instrument_close.is_none()
2605 && self.instrument.expiration_ns() == Some(timestamp_ns)
2606 {
2607 return;
2608 }
2609
2610 match self.process_option_expiry(timestamp_ns) {
2611 Ok(true) => {
2612 self.expiration_processed = true;
2613 self.pending_resolution = false;
2614 self.instrument_close.take();
2615 self.option_settlement_warning = None;
2616 log::info!("{} reached expiration", self.instrument.id());
2617 }
2618 Ok(false) => {}
2619 Err(e) => {
2620 self.option_settlement_failed = true;
2621 log::error!(
2622 "Option settlement failed terminally for {}: {e}",
2623 self.instrument.id()
2624 );
2625 }
2626 }
2627 return;
2628 }
2629
2630 self.expiration_processed = true;
2631 self.pending_resolution = false;
2632 let close = self.instrument_close.take();
2633 log::info!("{} reached expiration", self.instrument.id());
2634 self.cancel_open_orders_for_expiration();
2635
2636 let instrument_id = self.instrument.id();
2637 let positions: Vec<(
2638 TraderId,
2639 StrategyId,
2640 AccountId,
2641 PositionId,
2642 OrderSide,
2643 Quantity,
2644 )> = {
2645 let cache = self.cache.borrow();
2646 cache
2647 .positions_open(None, Some(&instrument_id), None, None, None)
2648 .into_iter()
2649 .filter_map(|pos| {
2650 OrderCore::closing_side(pos.side).map(|closing_side| {
2651 (
2652 pos.trader_id,
2653 pos.strategy_id,
2654 pos.account_id,
2655 pos.id,
2656 closing_side,
2657 pos.quantity,
2658 )
2659 })
2660 })
2661 .collect()
2662 };
2663
2664 let ts_now = self.clock.borrow().timestamp_ns();
2665 let close_price = close.as_ref().map(|close| close.close_price);
2666
2667 for (trader_id, strategy_id, account_id, position_id, closing_side, quantity) in positions {
2668 let client_order_id =
2669 ClientOrderId::from(format!("EXPIRATION-{}-{}", self.venue, UUID4::new()).as_str());
2670 let mut order = OrderAny::Market(MarketOrder::new(
2671 trader_id,
2672 strategy_id,
2673 instrument_id,
2674 client_order_id,
2675 closing_side,
2676 quantity,
2677 TimeInForce::Gtc,
2678 UUID4::new(),
2679 ts_now,
2680 true, false,
2682 None,
2683 None,
2684 None,
2685 None,
2686 None,
2687 None,
2688 None,
2689 Some(vec![Ustr::from(&format!(
2690 "EXPIRATION_{}_CLOSE",
2691 self.venue
2692 ))]),
2693 ));
2694 order.set_liquidity_side(LiquiditySide::Taker);
2695
2696 let add_result =
2697 self.cache
2698 .borrow_mut()
2699 .add_order(order.clone(), Some(position_id), None, false);
2700 if add_result.is_err() {
2701 log::debug!("Expiration order already in cache: {client_order_id}");
2702 } else {
2703 self.publish_order_initialized(&order);
2704 }
2705
2706 let venue_order_id = self.ids_generator.get_venue_order_id(&order).unwrap();
2707
2708 self.account_ids.insert(trader_id, account_id);
2711 self.generate_order_accepted(&order, venue_order_id);
2712
2713 if let Some(fill_price) = close_price {
2714 if let Err(e) = self.apply_fills(
2715 &order,
2716 &[(fill_price, quantity)],
2717 LiquiditySide::Taker,
2718 Some(position_id),
2719 None,
2720 None,
2721 ) {
2722 log::error!("Cannot fill expiration order {client_order_id}: {e}");
2723 }
2724 } else {
2725 self.fill_market_order(client_order_id);
2726 }
2727 }
2728 }
2729
2730 pub fn liquidate_open_positions(
2745 &mut self,
2746 ts_now: UnixNanos,
2747 cancel_open_orders: bool,
2748 settlement_currency: Currency,
2749 ) {
2750 if self.instrument.settlement_currency() != settlement_currency {
2752 return;
2753 }
2754
2755 if cancel_open_orders {
2756 let open_orders: Vec<RestingOrder> = self.get_open_orders();
2757 for order_info in &open_orders {
2758 let order = {
2759 let cache = self.cache.borrow();
2760 cache.order_owned(&order_info.client_order_id)
2761 };
2762
2763 if let Some(order) = order {
2764 self.cancel_order(&order, None);
2765 }
2766 }
2767 }
2768
2769 let instrument_id = self.instrument.id();
2770 let positions: Vec<(
2771 TraderId,
2772 StrategyId,
2773 AccountId,
2774 PositionId,
2775 OrderSide,
2776 Quantity,
2777 )> = {
2778 let cache = self.cache.borrow();
2779 cache
2780 .positions_open(None, Some(&instrument_id), None, None, None)
2781 .into_iter()
2782 .filter_map(|pos| {
2783 OrderCore::closing_side(pos.side).map(|closing_side| {
2784 (
2785 pos.trader_id,
2786 pos.strategy_id,
2787 pos.account_id,
2788 pos.id,
2789 closing_side,
2790 pos.quantity,
2791 )
2792 })
2793 })
2794 .collect()
2795 };
2796
2797 for (trader_id, strategy_id, account_id, position_id, closing_side, quantity) in positions {
2798 let has_price = if closing_side == OrderSide::Sell {
2800 self.best_bid_price().is_some()
2801 } else {
2802 self.best_ask_price().is_some()
2803 };
2804
2805 if !has_price {
2806 log::warn!(
2807 "LIQUIDATION: no price available for {instrument_id} position {position_id}, skipping"
2808 );
2809 continue;
2810 }
2811
2812 let client_order_id = ClientOrderId::from(
2813 format!("LIQUIDATION-{}-{}", self.venue, UUID4::new()).as_str(),
2814 );
2815 let order = OrderAny::Market(MarketOrder::new(
2816 trader_id,
2817 strategy_id,
2818 instrument_id,
2819 client_order_id,
2820 closing_side,
2821 quantity,
2822 TimeInForce::Ioc,
2823 UUID4::new(),
2824 ts_now,
2825 true, false,
2827 None,
2828 None,
2829 None,
2830 None,
2831 None,
2832 None,
2833 None,
2834 Some(vec![Ustr::from(&format!(
2835 "LIQUIDATION_{}_CLOSE",
2836 self.venue
2837 ))]),
2838 ));
2839
2840 let venue_order_id = self.ids_generator.get_venue_order_id(&order).unwrap();
2841 {
2842 let mut cache = self.cache.borrow_mut();
2843 if let Err(e) = cache.add_order(order.clone(), Some(position_id), None, false) {
2844 log::debug!("Liquidation order already in cache: {e}");
2845 } else {
2846 drop(cache);
2847 self.publish_order_initialized(&order);
2848 self.cache
2849 .borrow_mut()
2850 .add_venue_order_id(&client_order_id, &venue_order_id, false)
2851 .ok();
2852 }
2853 }
2854
2855 self.account_ids.insert(trader_id, account_id);
2858 self.generate_order_submitted(&order, account_id);
2859 self.generate_order_accepted(&order, venue_order_id);
2860 self.fill_market_order(client_order_id);
2861 }
2862 }
2863
2864 pub fn process_order(&mut self, order: &mut OrderAny, account_id: AccountId) {
2873 if self.core.order_exists(order.client_order_id()) {
2875 return;
2876 }
2877
2878 let ts_now = self.clock.borrow().timestamp_ns();
2881 self.check_instrument_expiration(ts_now, self.config.defer_option_settlement);
2882
2883 let reject_reason: Option<Ustr> = 'validate: {
2888 let cache_borrow = self.cache.as_ref().borrow();
2889
2890 self.account_ids.insert(order.trader_id(), account_id);
2892
2893 if self.pending_resolution {
2894 break 'validate Some(
2895 format!(
2896 "Contract {} has expired and is pending resolution",
2897 self.instrument.id()
2898 )
2899 .into(),
2900 );
2901 }
2902
2903 if self.market_status != MarketStatus::Open {
2904 break 'validate Some(
2905 format!(
2906 "Market {} is {}, cannot accept order {}",
2907 self.instrument.id(),
2908 self.market_status,
2909 order.client_order_id()
2910 )
2911 .into(),
2912 );
2913 }
2914
2915 if self.instrument.has_expiration() {
2917 if let Some(activation_ns) = self.instrument.activation_ns()
2918 && self.clock.borrow().timestamp_ns() < activation_ns
2919 {
2920 break 'validate Some(
2921 format!(
2922 "Contract {} is not yet active, activation {activation_ns}",
2923 self.instrument.id(),
2924 )
2925 .into(),
2926 );
2927 }
2928
2929 if let Some(expiration_ns) = self.instrument.expiration_ns()
2930 && self.clock.borrow().timestamp_ns() >= expiration_ns
2931 {
2932 break 'validate Some(
2933 format!(
2934 "Contract {} has expired, expiration {expiration_ns}",
2935 self.instrument.id(),
2936 )
2937 .into(),
2938 );
2939 }
2940 }
2941
2942 if self.config.support_contingent_orders {
2944 if let Some(parent_order_id) = order.parent_order_id() {
2945 let parent_order = match self.order_snapshot(parent_order_id) {
2946 Some(o) if o.contingency_type() == Some(ContingencyType::Oto) => o,
2947 _ => panic!("OTO parent not found"),
2948 };
2949 let parent_filled_qty = parent_order.filled_qty();
2950
2951 if parent_order.status() == OrderStatus::Rejected && order.is_open() {
2952 break 'validate Some(
2953 format!("Rejected OTO order from {parent_order_id}").into(),
2954 );
2955 } else if parent_filled_qty.is_zero()
2956 || (self.config.oto_full_trigger
2957 && parent_filled_qty < parent_order.quantity())
2958 {
2959 log::info!(
2960 "Pending OTO order {} triggers from {parent_order_id}",
2961 order.client_order_id(),
2962 );
2963 return;
2964 }
2965 }
2966
2967 if let Some(linked_order_ids) = order.linked_order_ids() {
2968 let contingency_type = order.contingency_type();
2969 for client_order_id in linked_order_ids {
2970 match cache_borrow.order(client_order_id) {
2971 Some(contingent_order)
2972 if matches!(
2973 contingency_type,
2974 Some(ContingencyType::Oco | ContingencyType::Ouo)
2975 ) && !order.is_closed()
2976 && contingent_order.is_closed() =>
2977 {
2978 break 'validate Some(
2979 format!("Contingent order {client_order_id} already closed")
2980 .into(),
2981 );
2982 }
2983 None => panic!("Cannot find contingent order for {client_order_id}"),
2984 _ => {}
2985 }
2986 }
2987 }
2988 }
2989
2990 if !order_precision_valid(order.quantity().precision, self.instrument.size_precision())
2992 {
2993 break 'validate Some(
2994 format!(
2995 "Invalid order quantity precision for order {}, was {} when {} size precision is {}",
2996 order.client_order_id(),
2997 order.quantity().precision,
2998 self.instrument.id(),
2999 self.instrument.size_precision()
3000 )
3001 .into(),
3002 );
3003 }
3004
3005 if let Some(display_qty) = order.display_qty()
3007 && !order_precision_valid(display_qty.precision, self.instrument.size_precision())
3008 {
3009 break 'validate Some(
3010 format!(
3011 "Invalid order display quantity precision for order {}, was {} when {} size precision is {}",
3012 order.client_order_id(),
3013 display_qty.precision,
3014 self.instrument.id(),
3015 self.instrument.size_precision()
3016 )
3017 .into(),
3018 );
3019 }
3020
3021 if let Some(price) = order.price()
3023 && !order_precision_valid(price.precision, self.instrument.price_precision())
3024 {
3025 break 'validate Some(
3026 format!(
3027 "Invalid order price precision for order {}, was {} when {} price precision is {}",
3028 order.client_order_id(),
3029 price.precision,
3030 self.instrument.id(),
3031 self.instrument.price_precision()
3032 )
3033 .into(),
3034 );
3035 }
3036
3037 if let Some(trigger_price) = order.trigger_price()
3039 && !order_precision_valid(
3040 trigger_price.precision,
3041 self.instrument.price_precision(),
3042 )
3043 {
3044 break 'validate Some(
3045 format!(
3046 "Invalid order trigger price precision for order {}, was {} when {} price precision is {}",
3047 order.client_order_id(),
3048 trigger_price.precision,
3049 self.instrument.id(),
3050 self.instrument.price_precision()
3051 )
3052 .into(),
3053 );
3054 }
3055
3056 if order.is_reduce_only() && !self.config.use_reduce_only {
3057 break 'validate Some(
3058 "Reduce-only orders are not supported by this matching engine".into(),
3059 );
3060 }
3061
3062 let position = self.position_for_order_in_cache(&cache_borrow, order);
3063
3064 if order.order_side() == OrderSide::Sell
3066 && self.account_type != AccountType::Margin
3067 && matches!(self.instrument, InstrumentAny::Equity(_))
3068 && position
3069 .as_ref()
3070 .is_none_or(|pos| !order.would_reduce_only(pos.side, pos.quantity))
3071 {
3072 let position_string = position
3073 .as_ref()
3074 .map_or("None".to_string(), |pos| pos.id.to_string());
3075 break 'validate Some(
3076 format!(
3077 "Short selling not permitted on a CASH account with position {position_string} and order {order}",
3078 )
3079 .into(),
3080 );
3081 }
3082
3083 if self.config.use_reduce_only
3085 && order.is_reduce_only()
3086 && !order.is_closed()
3087 && position.as_ref().is_none_or(|pos| {
3088 pos.is_closed()
3089 || (order.is_buy() && pos.is_long())
3090 || (order.is_sell() && pos.is_short())
3091 })
3092 {
3093 break 'validate Some(
3094 format!(
3095 "Reduce-only order {} ({}-{}) would have increased position",
3096 order.client_order_id(),
3097 order.order_type().to_string().to_uppercase(),
3098 order.order_side().to_string().to_uppercase()
3099 )
3100 .into(),
3101 );
3102 }
3103
3104 None
3105 };
3106
3107 if let Some(reason) = reject_reason {
3108 self.generate_order_rejected(order, reason);
3109 return;
3110 }
3111
3112 if order.is_quote_quantity()
3120 && !self.instrument.is_inverse()
3121 && !matches!(
3122 order.order_type(),
3123 OrderType::TrailingStopLimit | OrderType::TrailingStopMarket,
3124 )
3125 && (order.price().is_some()
3126 || matches!(
3127 order.order_type(),
3128 OrderType::Market | OrderType::MarketToLimit,
3129 ))
3130 && !self.convert_quote_to_base_quantity(order)
3131 {
3132 return;
3133 }
3134
3135 match order.order_type() {
3136 OrderType::Market => self.process_market_order(order),
3137 OrderType::Limit => self.process_limit_order(order),
3138 OrderType::MarketToLimit => self.process_market_to_limit_order(order),
3139 OrderType::StopMarket => self.process_stop_market_order(order),
3140 OrderType::StopLimit => self.process_stop_limit_order(order),
3141 OrderType::MarketIfTouched => self.process_market_if_touched_order(order),
3142 OrderType::LimitIfTouched => self.process_limit_if_touched_order(order),
3143 OrderType::TrailingStopMarket => self.process_trailing_stop_order(order),
3144 OrderType::TrailingStopLimit => self.process_trailing_stop_order(order),
3145 }
3146 }
3147
3148 fn convert_quote_to_base_quantity(&self, order: &mut OrderAny) -> bool {
3149 let reference_price = if let Some(price) = order.price() {
3153 Some(price)
3154 } else {
3155 match order.order_side() {
3156 OrderSide::Buy => self.core.ask,
3157 OrderSide::Sell => self.core.bid,
3158 }
3159 };
3160
3161 let Some(reference_price) = reference_price else {
3162 self.generate_order_rejected(
3163 order,
3164 format!(
3165 "No market for {} to convert quote quantity to base",
3166 order.instrument_id(),
3167 )
3168 .into(),
3169 );
3170 return false;
3171 };
3172
3173 let base_quantity = self
3174 .instrument
3175 .calculate_base_quantity(order.quantity(), reference_price);
3176
3177 let ts_now = self.clock.borrow().timestamp_ns();
3178 let event = OrderEventAny::Updated(OrderUpdated::new(
3179 order.trader_id(),
3180 order.strategy_id(),
3181 order.instrument_id(),
3182 order.client_order_id(),
3183 base_quantity,
3184 UUID4::new(),
3185 ts_now,
3186 ts_now,
3187 false,
3188 order.venue_order_id(),
3189 order.account_id(),
3190 None,
3191 None,
3192 None,
3193 false,
3194 ));
3195
3196 if let Err(e) = order.apply(event.clone()) {
3200 log::error!(
3201 "Failed to apply quote-to-base update for {}: {e}",
3202 order.client_order_id(),
3203 );
3204 return false;
3205 }
3206 self.dispatch_order_event(event);
3207 true
3208 }
3209
3210 pub fn process_modify(&mut self, command: &ModifyOrder, account_id: AccountId) {
3212 if !self.core.order_exists(command.client_order_id) {
3213 self.generate_order_modify_rejected(
3214 command.trader_id,
3215 command.strategy_id,
3216 command.instrument_id,
3217 command.client_order_id,
3218 Ustr::from(format!("Order {} not found", command.client_order_id).as_str()),
3219 command.venue_order_id,
3220 Some(account_id),
3221 );
3222 return;
3223 }
3224
3225 let order = match self.order_snapshot(command.client_order_id) {
3226 Some(order) => order,
3227 None => {
3228 log::error!(
3229 "Cannot modify order: order {} not found in cache",
3230 command.client_order_id
3231 );
3232 return;
3233 }
3234 };
3235
3236 let update_success = self.update_order(
3237 &order,
3238 command.quantity,
3239 command.price,
3240 command.trigger_price,
3241 None,
3242 );
3243
3244 if !update_success {
3245 return;
3246 }
3247
3248 if !self.core.order_exists(command.client_order_id) {
3249 return;
3250 }
3251
3252 let Some(refreshed) = self.resync_core_entry(command.client_order_id) else {
3253 return;
3254 };
3255
3256 let price_changed = refreshed.price() != order.price()
3258 || refreshed.trigger_price() != order.trigger_price();
3259
3260 if price_changed
3261 && refreshed.is_open()
3262 && self.config.queue_position
3263 && let Some(new_price) = refreshed.price()
3264 {
3265 self.snapshot_queue_position(&refreshed, new_price);
3266 self.queue_excess.swap_remove(&refreshed.client_order_id());
3267 }
3268 }
3269
3270 pub fn process_cancel(&mut self, command: &CancelOrder, account_id: AccountId) {
3272 if !self.core.order_exists(command.client_order_id) {
3273 self.generate_order_cancel_rejected(
3274 command.trader_id,
3275 command.strategy_id,
3276 account_id,
3277 command.instrument_id,
3278 command.client_order_id,
3279 command.venue_order_id,
3280 Ustr::from(format!("Order {} not found", command.client_order_id).as_str()),
3281 );
3282 return;
3283 }
3284
3285 let order = match self.order_snapshot(command.client_order_id) {
3286 Some(order) => order,
3287 None => {
3288 log::error!(
3289 "Cannot cancel order: order {} not found in cache",
3290 command.client_order_id
3291 );
3292 return;
3293 }
3294 };
3295
3296 if !order.is_inflight() && !order.is_open() {
3297 self.purge_stale_core_entry(command.client_order_id);
3298 return;
3299 }
3300
3301 self.cancel_order(&order, None);
3302 }
3303
3304 pub fn process_cancel_all(&mut self, command: &CancelAllOrders, account_id: AccountId) {
3308 self.process_cancel_all_excluding(command, account_id, &[]);
3309 }
3310
3311 pub fn process_cancel_all_excluding(
3314 &mut self,
3315 command: &CancelAllOrders,
3316 account_id: AccountId,
3317 excluded: &[ClientOrderId],
3318 ) {
3319 let instrument_id = command.instrument_id;
3320 let order_side = command.order_side;
3321
3322 let mut client_order_ids: Vec<ClientOrderId> = {
3323 let cache = self.cache.borrow();
3324 cache
3325 .orders_open_refs(
3326 None,
3327 Some(&instrument_id),
3328 None,
3329 Some(&account_id),
3330 order_side,
3331 )
3332 .into_iter()
3333 .chain(cache.orders_inflight_refs(
3334 None,
3335 Some(&instrument_id),
3336 None,
3337 Some(&account_id),
3338 order_side,
3339 ))
3340 .map(|order| order.client_order_id())
3341 .filter(|client_order_id| !excluded.contains(client_order_id))
3342 .collect()
3343 };
3344 client_order_ids.sort_unstable();
3345 client_order_ids.dedup();
3346
3347 for client_order_id in client_order_ids {
3348 let order = match self
3349 .cache
3350 .borrow()
3351 .order(&client_order_id)
3352 .map(|o| o.clone())
3353 {
3354 Some(order) => order,
3355 None => continue,
3356 };
3357
3358 if !order.is_inflight() && !order.is_open() {
3359 self.purge_stale_core_entry(client_order_id);
3360 continue;
3361 }
3362
3363 self.cancel_order_excluding(&order, None, excluded);
3364 }
3365 }
3366
3367 fn purge_stale_core_entry(&mut self, client_order_id: ClientOrderId) {
3370 if self.core.order_exists(client_order_id) {
3371 self.delete_core_order(client_order_id);
3372 }
3373
3374 self.remove_queue_position(client_order_id);
3375 self.cached_filled_qty.swap_remove(&client_order_id);
3376 }
3377
3378 fn resync_core_entry(&mut self, client_order_id: ClientOrderId) -> Option<OrderAny> {
3379 let order = self.order_snapshot(client_order_id)?;
3380
3381 if order.is_closed() {
3384 self.delete_core_order(client_order_id);
3385 self.remove_queue_position(client_order_id);
3386 return Some(order);
3387 }
3388
3389 let new_match_info = Self::matching_core_entry(&order);
3390
3391 let unchanged = self
3393 .core
3394 .get_order(client_order_id)
3395 .is_some_and(|existing| *existing == new_match_info);
3396
3397 if unchanged {
3398 self.track_post_match_order(&order);
3399 return Some(order);
3400 }
3401
3402 self.delete_core_order(client_order_id);
3403 self.track_post_match_order(&order);
3404 self.core.add_order(new_match_info);
3405 Some(order)
3406 }
3407
3408 fn order_snapshot(&self, client_order_id: ClientOrderId) -> Option<OrderAny> {
3409 let mut order = self.cache.borrow().order(&client_order_id)?.clone();
3410 let mut pending = self.pending_order_updates.borrow_mut();
3411
3412 if order.is_closed() {
3413 pending.swap_remove(&client_order_id);
3414 return Some(order);
3415 }
3416
3417 if let Some(updates) = pending.get_mut(&client_order_id) {
3418 Self::retain_unapplied_order_updates(&order, updates);
3419
3420 for update in updates.iter() {
3421 if let Err(e) = order.apply(OrderEventAny::Updated(*update)) {
3422 log::error!("Cannot apply pending update for {client_order_id}: {e}");
3423 return None;
3424 }
3425 }
3426
3427 if updates.is_empty() {
3428 pending.swap_remove(&client_order_id);
3429 }
3430 }
3431
3432 if let Some(filled_qty) = self.cached_filled_qty.get(&client_order_id) {
3433 write_filled_qty(&mut order, *filled_qty);
3434 order.set_leaves_qty(order.quantity().saturating_sub(*filled_qty));
3435 }
3436
3437 Some(order)
3438 }
3439
3440 fn purge_applied_order_updates(&self) {
3441 let cache = self.cache.borrow();
3442 self.pending_order_updates
3443 .borrow_mut()
3444 .retain(|id, updates| {
3445 let Some(order) = cache.order(id) else {
3446 return false;
3447 };
3448 Self::retain_unapplied_order_updates(&order, updates);
3449 !updates.is_empty()
3450 });
3451 }
3452
3453 fn retain_unapplied_order_updates(order: &OrderAny, updates: &mut Vec<OrderUpdated>) {
3454 if order.is_closed() {
3455 updates.clear();
3456 return;
3457 }
3458
3459 let events = order.events();
3460 updates.retain(|update| {
3461 !events.iter().any(|event| {
3462 matches!(event, OrderEventAny::Updated(applied) if applied.event_id == update.event_id)
3463 })
3464 });
3465 }
3466
3467 pub fn process_batch_cancel(&mut self, command: &BatchCancelOrders, account_id: AccountId) {
3469 for order in &command.cancels {
3470 self.process_cancel(order, account_id);
3471 }
3472 }
3473
3474 pub fn process_batch_modify(&mut self, command: &BatchModifyOrders, account_id: AccountId) {
3476 for order in &command.modifies {
3477 self.process_modify(order, account_id);
3478 }
3479 }
3480
3481 fn process_market_order(&mut self, order: &OrderAny) {
3482 if order.time_in_force() == TimeInForce::AtTheOpen
3483 || order.time_in_force() == TimeInForce::AtTheClose
3484 {
3485 self.generate_order_rejected(
3486 order,
3487 format!(
3488 "time in force {} is not currently supported",
3489 order.time_in_force()
3490 )
3491 .into(),
3492 );
3493 return;
3494 }
3495
3496 if (order.order_side() == OrderSide::Buy && self.core.ask.is_none())
3498 || (order.order_side() == OrderSide::Sell && self.core.bid.is_none())
3499 {
3500 self.generate_order_rejected(
3501 order,
3502 format!("No market for {}", order.instrument_id()).into(),
3503 );
3504 return;
3505 }
3506
3507 if self.config.use_market_order_acks {
3508 let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
3509 self.generate_order_accepted(order, venue_order_id);
3510 }
3511
3512 if let Err(e) = self
3514 .cache
3515 .borrow_mut()
3516 .add_order(order.clone(), None, None, false)
3517 {
3518 log::debug!("Order already in cache: {e}");
3519 }
3520
3521 self.fill_market_order(order.client_order_id());
3522 }
3523
3524 fn process_limit_order(&mut self, order: &mut OrderAny) {
3525 if order.time_in_force() == TimeInForce::AtTheOpen
3526 || order.time_in_force() == TimeInForce::AtTheClose
3527 {
3528 self.generate_order_rejected(
3529 order,
3530 format!(
3531 "time in force {} is not currently supported",
3532 order.time_in_force()
3533 )
3534 .into(),
3535 );
3536 return;
3537 }
3538
3539 let limit_px = order.price().expect("Limit order must have a price");
3540 if order.is_post_only() && self.core.is_limit_matched(order.order_side(), limit_px) {
3541 self.generate_order_rejected(
3542 order,
3543 format!(
3544 "POST_ONLY {} {} order limit px of {} would have been a TAKER: bid={}, ask={}",
3545 order.order_type(),
3546 order.order_side(),
3547 order.price().unwrap(),
3548 self.core
3549 .bid
3550 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3551 self.core
3552 .ask
3553 .map_or_else(|| "None".to_string(), |p| p.to_string())
3554 )
3555 .into(),
3556 );
3557 return;
3558 }
3559
3560 self.accept_order(order);
3562
3563 if self.core.is_limit_matched(order.order_side(), limit_px) {
3565 order.set_liquidity_side(LiquiditySide::Taker);
3567
3568 if self
3569 .cache
3570 .borrow_mut()
3571 .add_order(order.clone(), None, None, false)
3572 .is_err()
3573 && let Err(e) = self.cache.borrow_mut().replace_order(order)
3574 {
3575 log::debug!("Failed to update order in cache: {e}");
3576 }
3577 self.fill_limit_order(order.client_order_id());
3578
3579 if self.core.order_exists(order.client_order_id())
3582 && let Some(mut order) = self.cache.borrow_mut().order_mut(&order.client_order_id())
3583 {
3584 order.set_liquidity_side(LiquiditySide::Maker);
3585 }
3586 } else if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc) {
3587 self.cancel_order(order, None);
3588 } else {
3589 order.set_liquidity_side(LiquiditySide::Maker);
3591
3592 if let Some(price) = order.price() {
3593 self.snapshot_queue_position(order, price);
3594 }
3595
3596 let add_result = self
3597 .cache
3598 .borrow_mut()
3599 .add_order(order.clone(), None, None, false);
3600
3601 if let Err(e) = add_result {
3602 log::debug!("Failed to add order to cache: {e}");
3603
3604 if let Some(mut order) = self.cache.borrow_mut().order_mut(&order.client_order_id())
3607 && !matches!(
3608 order.liquidity_side(),
3609 Some(LiquiditySide::Maker | LiquiditySide::Taker)
3610 )
3611 {
3612 order.set_liquidity_side(LiquiditySide::Maker);
3613 }
3614 }
3615 }
3616 }
3617
3618 fn process_market_to_limit_order(&mut self, order: &OrderAny) {
3619 if (order.order_side() == OrderSide::Buy && self.core.ask.is_none())
3621 || (order.order_side() == OrderSide::Sell && self.core.bid.is_none())
3622 {
3623 self.generate_order_rejected(
3624 order,
3625 format!("No market for {}", order.instrument_id()).into(),
3626 );
3627 return;
3628 }
3629
3630 if self.config.use_market_order_acks {
3631 let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
3632 self.generate_order_accepted(order, venue_order_id);
3633 }
3634
3635 if let Err(e) = self
3637 .cache
3638 .borrow_mut()
3639 .add_order(order.clone(), None, None, false)
3640 {
3641 log::debug!("Order already in cache: {e}");
3642 }
3643 let client_order_id = order.client_order_id();
3644 self.fill_market_order(client_order_id);
3645
3646 let filled_qty = self
3648 .cached_filled_qty
3649 .get(&client_order_id)
3650 .copied()
3651 .unwrap_or_default();
3652 let leaves_qty = order.quantity().saturating_sub(filled_qty);
3653 if leaves_qty.is_zero() {
3654 self.purge_cached_filled_qty_if_closed(client_order_id);
3655 return;
3656 }
3657
3658 if let Some(mut updated_order) = self.order_snapshot(client_order_id) {
3659 self.accept_order(&mut updated_order);
3660 }
3661 }
3662
3663 fn process_stop_market_order(&mut self, order: &mut OrderAny) {
3664 let stop_px = order
3665 .trigger_price()
3666 .expect("Stop order must have a trigger price");
3667
3668 if self.core.is_stop_matched_with_trigger_type(
3669 order.order_side(),
3670 stop_px,
3671 order.trigger_type().unwrap_or(TriggerType::Default),
3672 ) {
3673 if self.config.reject_stop_orders {
3674 self.generate_order_rejected(
3675 order,
3676 format!(
3677 "{} {} order stop px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3678 order.order_type(),
3679 order.order_side(),
3680 order.trigger_price().unwrap(),
3681 self.core
3682 .bid
3683 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3684 self.core
3685 .ask
3686 .map_or_else(|| "None".to_string(), |p| p.to_string())
3687 ).into(),
3688 );
3689 return;
3690 }
3691
3692 if let Err(e) = self
3693 .cache
3694 .borrow_mut()
3695 .add_order(order.clone(), None, None, false)
3696 {
3697 log::debug!("Order already in cache: {e}");
3698 }
3699 self.fill_market_order(order.client_order_id());
3700 return;
3701 }
3702
3703 self.accept_order(order);
3705
3706 order.set_liquidity_side(LiquiditySide::Maker);
3708
3709 if let Err(e) = self
3710 .cache
3711 .borrow_mut()
3712 .add_order(order.clone(), None, None, false)
3713 {
3714 log::debug!("Order already in cache: {e}");
3715 }
3716 }
3717
3718 fn process_stop_limit_order(&mut self, order: &mut OrderAny) {
3719 let stop_px = order
3720 .trigger_price()
3721 .expect("Stop order must have a trigger price");
3722
3723 if self.core.is_stop_matched_with_trigger_type(
3724 order.order_side(),
3725 stop_px,
3726 order.trigger_type().unwrap_or(TriggerType::Default),
3727 ) {
3728 if self.config.reject_stop_orders {
3729 self.generate_order_rejected(
3730 order,
3731 format!(
3732 "{} {} order stop px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3733 order.order_type(),
3734 order.order_side(),
3735 order.trigger_price().unwrap(),
3736 self.core
3737 .bid
3738 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3739 self.core
3740 .ask
3741 .map_or_else(|| "None".to_string(), |p| p.to_string())
3742 ).into(),
3743 );
3744 return;
3745 }
3746
3747 self.accept_triggered_limit_style_order(order);
3748 return;
3749 }
3750
3751 self.accept_order(order);
3752
3753 order.set_liquidity_side(LiquiditySide::Maker);
3755
3756 if let Err(e) = self
3757 .cache
3758 .borrow_mut()
3759 .add_order(order.clone(), None, None, false)
3760 {
3761 log::debug!("Order already in cache: {e}");
3762 }
3763 }
3764
3765 fn process_market_if_touched_order(&mut self, order: &mut OrderAny) {
3766 if self.core.is_touch_triggered_with_trigger_type(
3767 order.order_side(),
3768 order.trigger_price().unwrap(),
3769 order.trigger_type().unwrap_or(TriggerType::Default),
3770 ) {
3771 if self.config.reject_stop_orders {
3772 self.generate_order_rejected(
3773 order,
3774 format!(
3775 "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3776 order.order_type(),
3777 order.order_side(),
3778 order.trigger_price().unwrap(),
3779 self.core
3780 .bid
3781 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3782 self.core
3783 .ask
3784 .map_or_else(|| "None".to_string(), |p| p.to_string())
3785 ).into(),
3786 );
3787 return;
3788 }
3789
3790 if let Err(e) = self
3791 .cache
3792 .borrow_mut()
3793 .add_order(order.clone(), None, None, false)
3794 {
3795 log::debug!("Order already in cache: {e}");
3796 }
3797 self.fill_market_order(order.client_order_id());
3798 return;
3799 }
3800
3801 self.accept_order(order);
3803
3804 order.set_liquidity_side(LiquiditySide::Maker);
3806
3807 if let Err(e) = self
3808 .cache
3809 .borrow_mut()
3810 .add_order(order.clone(), None, None, false)
3811 {
3812 log::debug!("Order already in cache: {e}");
3813 }
3814 }
3815
3816 fn process_limit_if_touched_order(&mut self, order: &mut OrderAny) {
3817 if self.core.is_touch_triggered_with_trigger_type(
3818 order.order_side(),
3819 order.trigger_price().unwrap(),
3820 order.trigger_type().unwrap_or(TriggerType::Default),
3821 ) {
3822 if self.config.reject_stop_orders {
3823 self.generate_order_rejected(
3824 order,
3825 format!(
3826 "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3827 order.order_type(),
3828 order.order_side(),
3829 order.trigger_price().unwrap(),
3830 self.core
3831 .bid
3832 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3833 self.core
3834 .ask
3835 .map_or_else(|| "None".to_string(), |p| p.to_string())
3836 ).into(),
3837 );
3838 return;
3839 }
3840 self.accept_triggered_limit_style_order(order);
3841 return;
3842 }
3843
3844 self.accept_order(order);
3846
3847 order.set_liquidity_side(LiquiditySide::Maker);
3849
3850 if let Err(e) = self
3851 .cache
3852 .borrow_mut()
3853 .add_order(order.clone(), None, None, false)
3854 {
3855 log::debug!("Order already in cache: {e}");
3856 }
3857 }
3858
3859 fn accept_triggered_limit_style_order(&mut self, order: &mut OrderAny) {
3860 self.accept_order(order);
3861
3862 if let Err(e) = self
3863 .cache
3864 .borrow_mut()
3865 .add_order(order.clone(), None, None, false)
3866 {
3867 log::debug!("Order already in cache: {e}");
3868 }
3869
3870 self.trigger_limit_style_stop_order(order.client_order_id(), order.clone());
3871
3872 if let Some(cached_order) = self
3873 .cache
3874 .borrow()
3875 .order(&order.client_order_id())
3876 .map(|order| order.clone())
3877 {
3878 *order = cached_order;
3879 }
3880 }
3881
3882 fn process_trailing_stop_order(&mut self, order: &mut OrderAny) {
3883 let side = order.order_side();
3884 let trigger_type = order.trigger_type().unwrap_or(TriggerType::Default);
3885
3886 if let Some(activation_price) = order.activation_price()
3887 && self.core.is_touch_triggered(side, activation_price)
3888 && self.config.reject_stop_orders
3889 {
3890 self.generate_order_rejected(
3891 order,
3892 format!(
3893 "{} {} order activation px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3894 order.order_type(),
3895 side,
3896 activation_price,
3897 self.core
3898 .bid
3899 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3900 self.core
3901 .ask
3902 .map_or_else(|| "None".to_string(), |p| p.to_string())
3903 )
3904 .into(),
3905 );
3906 return;
3907 }
3908
3909 let activates_now = match order.activation_price() {
3910 Some(activation_price) => self.core.is_touch_triggered(side, activation_price),
3911 None => self
3912 .get_trailing_activation_price(
3913 trigger_type,
3914 side,
3915 self.core.bid,
3916 self.core.ask,
3917 self.core.last,
3918 )
3919 .is_some(),
3920 };
3921
3922 if activates_now
3923 && let Some(trigger_price) = order.trigger_price()
3924 && self
3925 .core
3926 .is_stop_matched_with_trigger_type(side, trigger_price, trigger_type)
3927 {
3928 self.generate_order_rejected(
3929 order,
3930 format!(
3931 "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3932 order.order_type(),
3933 side,
3934 trigger_price,
3935 self.core
3936 .bid
3937 .map_or_else(|| "None".to_string(), |p| p.to_string()),
3938 self.core
3939 .ask
3940 .map_or_else(|| "None".to_string(), |p| p.to_string())
3941 )
3942 .into(),
3943 );
3944 return;
3945 }
3946
3947 order.set_liquidity_side(LiquiditySide::Maker);
3951
3952 self.accept_order(order);
3953
3954 if let Err(e) = self
3955 .cache
3956 .borrow_mut()
3957 .add_order(order.clone(), None, None, false)
3958 {
3959 log::debug!("Order already in cache: {e}");
3960 }
3961 }
3962
3963 pub fn iterate(&mut self, timestamp_ns: UnixNanos, aggressor_side: AggressorSide) {
3970 self.iterate_with_mode(timestamp_ns, aggressor_side, OrderMatchMode::All);
3971 }
3972
3973 fn iterate_with_mode(
3974 &mut self,
3975 timestamp_ns: UnixNanos,
3976 aggressor_side: AggressorSide,
3977 match_mode: OrderMatchMode,
3978 ) {
3979 self.purge_closed_cached_filled_qty();
3981 self.purge_applied_order_updates();
3982 self.purge_applied_fills();
3983
3984 if aggressor_side == AggressorSide::NoAggressor && self.last_trade_size.is_none() {
3990 if self.book_type == BookType::L1_MBP {
3991 if let Some(bid) = self.book.best_bid_price() {
3992 self.core.set_bid_raw(bid);
3993 }
3994
3995 if let Some(ask) = self.book.best_ask_price() {
3996 self.core.set_ask_raw(ask);
3997 }
3998 } else {
3999 self.core.bid = self.book.best_bid_price();
4003 self.core.ask = self.book.best_ask_price();
4004 }
4005 }
4006
4007 let mut matched_order = false;
4008
4009 if self.market_status == MarketStatus::Open {
4010 for action in self.core.iterate_bids() {
4013 if !self.should_process_match_action(action, match_mode) {
4014 continue;
4015 }
4016
4017 matched_order = true;
4018
4019 match action {
4020 MatchAction::FillLimit(id) => self.fill_resting_limit_order(id),
4021 MatchAction::TriggerStop(id) => self.trigger_stop_order(id),
4022 }
4023 }
4024
4025 for action in self.core.iterate_asks() {
4026 if !self.should_process_match_action(action, match_mode) {
4027 continue;
4028 }
4029
4030 matched_order = true;
4031
4032 match action {
4033 MatchAction::FillLimit(id) => self.fill_resting_limit_order(id),
4034 MatchAction::TriggerStop(id) => self.trigger_stop_order(id),
4035 }
4036 }
4037 }
4038
4039 let order_ids: Vec<ClientOrderId> = if matched_order {
4040 self.core.iter_orders().map(|m| m.client_order_id).collect()
4041 } else if self.post_match_order_ids.is_empty() {
4042 Vec::new()
4043 } else {
4044 self.core
4045 .iter_orders()
4046 .filter_map(|order| {
4047 self.post_match_order_ids
4048 .contains(&order.client_order_id)
4049 .then_some(order.client_order_id)
4050 })
4051 .collect()
4052 };
4053
4054 let support_gtd_orders = self.config.support_gtd_orders;
4055
4056 for client_order_id in order_ids {
4057 let (action, keep_tracking) = {
4058 let cache = self.cache.borrow();
4059 let Some(order) = cache.order(&client_order_id) else {
4060 self.post_match_order_ids.swap_remove(&client_order_id);
4061 continue;
4062 };
4063
4064 (
4065 post_match_order_action(&order, support_gtd_orders, timestamp_ns, |order| {
4066 self.order_snapshot(client_order_id)
4067 .unwrap_or_else(|| order.clone())
4068 }),
4069 Self::requires_post_match_maintenance(&order),
4070 )
4071 };
4072
4073 match action {
4074 PostMatchOrderAction::RemoveClosed => {
4075 self.delete_core_order(client_order_id);
4076 self.remove_queue_position(client_order_id);
4077 self.cached_filled_qty.swap_remove(&client_order_id);
4078 continue;
4079 }
4080 PostMatchOrderAction::Expire(order) => {
4081 self.delete_core_order(client_order_id);
4082 self.cached_filled_qty.swap_remove(&client_order_id);
4083 self.expire_order(&order);
4084 continue;
4085 }
4086 PostMatchOrderAction::UpdateTrailing(mut order) => {
4087 if self.maybe_activate_trailing_stop(
4088 &mut order,
4089 self.core.bid,
4090 self.core.ask,
4091 self.core.last,
4092 ) {
4093 self.update_trailing_stop_order(&order);
4094 self.resync_core_entry(client_order_id);
4095 }
4096 }
4097 PostMatchOrderAction::NoMaintenance => {
4098 if !keep_tracking {
4099 self.post_match_order_ids.swap_remove(&client_order_id);
4100 }
4101 }
4102 }
4103
4104 if self.target_bid.is_some() || self.target_ask.is_some() || self.target_last.is_some()
4107 {
4108 if let Some(t) = self.target_bid.take() {
4109 self.core.bid = Some(t);
4110 }
4111
4112 if let Some(t) = self.target_ask.take() {
4113 self.core.ask = Some(t);
4114 }
4115
4116 if let Some(t) = self.target_last.take() {
4117 self.core.last = Some(t);
4118 }
4119 }
4120 }
4121
4122 if let Some(t) = self.target_bid.take() {
4126 self.core.bid = Some(t);
4127 }
4128
4129 if let Some(t) = self.target_ask.take() {
4130 self.core.ask = Some(t);
4131 }
4132
4133 if let Some(t) = self.target_last.take() {
4134 self.core.last = Some(t);
4135 }
4136
4137 self.core.bid = self.book.best_bid_price();
4140 self.core.ask = self.book.best_ask_price();
4141
4142 self.check_instrument_expiration(timestamp_ns, self.config.defer_option_settlement);
4145 self.purge_closed_cached_filled_qty();
4146 self.purge_applied_order_updates();
4147 self.purge_applied_fills();
4148 }
4149
4150 fn fill_resting_limit_order(&mut self, client_order_id: ClientOrderId) {
4151 if self
4153 .core
4154 .get_order(client_order_id)
4155 .is_some_and(|order| order.order_type == OrderType::MarketToLimit)
4156 && let Some(mut order) = self.cache.borrow_mut().order_mut(&client_order_id)
4157 {
4158 order.set_liquidity_side(LiquiditySide::Maker);
4159 }
4160 self.fill_limit_order(client_order_id);
4161 }
4162
4163 fn should_process_match_action(&self, action: MatchAction, match_mode: OrderMatchMode) -> bool {
4164 let client_order_id = match action {
4165 MatchAction::FillLimit(id) | MatchAction::TriggerStop(id) => id,
4166 };
4167
4168 if !self.core.order_exists(client_order_id) {
4169 return false;
4170 }
4171
4172 match match_mode {
4173 OrderMatchMode::All => true,
4174 OrderMatchMode::LastPriceStopTriggers => match action {
4175 MatchAction::TriggerStop(client_order_id) => self
4176 .core
4177 .get_order(client_order_id)
4178 .is_some_and(|order| order.trigger_type == Some(TriggerType::LastPrice)),
4179 MatchAction::FillLimit(_) => false,
4180 },
4181 }
4182 }
4183
4184 fn get_trailing_activation_price(
4185 &self,
4186 trigger_type: TriggerType,
4187 order_side: OrderSide,
4188 bid: Option<Price>,
4189 ask: Option<Price>,
4190 last: Option<Price>,
4191 ) -> Option<Price> {
4192 match trigger_type {
4193 TriggerType::LastPrice => last,
4194 TriggerType::LastOrBidAsk => last.or(match order_side {
4195 OrderSide::Buy => ask,
4196 OrderSide::Sell => bid,
4197 }),
4198
4199 _ => match order_side {
4201 OrderSide::Buy => ask,
4202 OrderSide::Sell => bid,
4203 },
4204 }
4205 }
4206
4207 fn maybe_activate_trailing_stop(
4208 &self,
4209 order: &mut OrderAny,
4210 bid: Option<Price>,
4211 ask: Option<Price>,
4212 last: Option<Price>,
4213 ) -> bool {
4214 match order {
4215 OrderAny::TrailingStopMarket(inner) => {
4216 if inner.is_activated {
4217 return true;
4218 }
4219
4220 if inner.activation_price.is_none() {
4221 let px = self.get_trailing_activation_price(
4222 inner.trigger_type,
4223 inner.order_side(),
4224 bid,
4225 ask,
4226 last,
4227 );
4228
4229 if let Some(p) = px {
4230 inner.activation_price = Some(p);
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 return true;
4237 }
4238 return false;
4239 }
4240
4241 let activation_price = inner.activation_price.unwrap();
4242 let hit = match inner.order_side() {
4243 OrderSide::Buy => ask.is_some_and(|a| a <= activation_price),
4244 OrderSide::Sell => bid.is_some_and(|b| b >= activation_price),
4245 };
4246
4247 if hit {
4248 inner.set_activated();
4249
4250 if let Err(e) = self.cache.borrow_mut().replace_order(order) {
4251 log::error!("Failed to update order: {e}");
4252 }
4253 }
4254 hit
4255 }
4256 OrderAny::TrailingStopLimit(inner) => {
4257 if inner.is_activated {
4258 return true;
4259 }
4260
4261 if inner.activation_price.is_none() {
4262 let px = self.get_trailing_activation_price(
4263 inner.trigger_type,
4264 inner.order_side(),
4265 bid,
4266 ask,
4267 last,
4268 );
4269
4270 if let Some(p) = px {
4271 inner.activation_price = Some(p);
4272 inner.set_activated();
4273
4274 if let Err(e) = self.cache.borrow_mut().replace_order(order) {
4275 log::error!("Failed to update order: {e}");
4276 }
4277 return true;
4278 }
4279 return false;
4280 }
4281
4282 let activation_price = inner.activation_price.unwrap();
4283 let hit = match inner.order_side() {
4284 OrderSide::Buy => ask.is_some_and(|a| a <= activation_price),
4285 OrderSide::Sell => bid.is_some_and(|b| b >= activation_price),
4286 };
4287
4288 if hit {
4289 inner.set_activated();
4290
4291 if let Err(e) = self.cache.borrow_mut().replace_order(order) {
4292 log::error!("Failed to update order: {e}");
4293 }
4294 }
4295 hit
4296 }
4297 _ => true,
4298 }
4299 }
4300
4301 fn determine_limit_price_and_volume(&mut self, order: &OrderAny) -> Vec<(Price, Quantity)> {
4302 match order.price() {
4303 Some(order_price) => {
4304 let mut fills = if self.config.liquidity_consumption {
4309 let size_prec = self.instrument.size_precision();
4310 self.book
4311 .get_all_crossed_levels(order.order_side(), order_price, size_prec)
4312 } else {
4313 let book_order =
4314 BookOrder::new(order.order_side(), order_price, order.quantity(), 1);
4315 self.book.simulate_fills(&book_order)
4316 };
4317
4318 if let Some(trade_size) = self.last_trade_size
4320 && let Some(trade_price) = self.core.last
4321 {
4322 let fills_at_trade_price = fills.iter().any(|(px, _)| *px == trade_price);
4323
4324 if (self.book_type == BookType::L1_MBP || !fills_at_trade_price)
4325 && self.core.is_limit_matched(order.order_side(), order_price)
4326 {
4327 let leaves_qty = order.leaves_qty();
4330 let available_qty = if self.config.liquidity_consumption {
4331 let remaining = trade_size.raw().saturating_sub(self.trade_consumption);
4332 Quantity::from_raw(remaining, trade_size.precision)
4333 } else {
4334 trade_size
4335 };
4336
4337 let fill_qty = min(leaves_qty, available_qty);
4338
4339 if fill_qty.non_zero() {
4340 let fill_price = if self.book_type == BookType::L1_MBP
4341 && fills_at_trade_price
4342 && order.liquidity_side() == Some(LiquiditySide::Taker)
4343 {
4344 trade_price
4345 } else {
4346 order_price
4347 };
4348
4349 log::debug!(
4350 "Trade execution fill: {} @ {} (trade_price={}, available: {}, book had {} fills)",
4351 fill_qty,
4352 fill_price,
4353 trade_price,
4354 available_qty,
4355 fills.len()
4356 );
4357
4358 if self.config.liquidity_consumption {
4359 self.trade_consumption += fill_qty.raw();
4360 }
4361
4362 return vec![(fill_price, fill_qty)];
4365 }
4366
4367 if self.book_type == BookType::L1_MBP {
4368 return Vec::new();
4369 }
4370 }
4371 }
4372
4373 if fills.is_empty() {
4375 return fills;
4376 }
4377
4378 let book_prices: Vec<Price> = if self.config.liquidity_consumption {
4382 fills.iter().map(|(px, _)| *px).collect()
4383 } else {
4384 Vec::new()
4385 };
4386
4387 let book_prices_ref: Option<&[Price]> = if book_prices.is_empty() {
4388 None
4389 } else {
4390 Some(&book_prices)
4391 };
4392
4393 if order
4395 .liquidity_side()
4396 .is_some_and(|liquidity_side| liquidity_side == LiquiditySide::Maker)
4397 {
4398 match order.order_side() {
4399 OrderSide::Buy => {
4400 let target_price = if order
4401 .trigger_price()
4402 .is_some_and(|trigger_price| order_price > trigger_price)
4403 {
4404 order.trigger_price().unwrap()
4405 } else {
4406 order_price
4407 };
4408
4409 for fill in &mut fills {
4410 let last_px = fill.0;
4411 if last_px < order_price {
4412 self.target_bid = self.core.bid;
4414 self.target_ask = self.core.ask;
4415 self.target_last = self.core.last;
4416 self.core.set_ask_raw(target_price);
4417 self.core.set_last_raw(target_price);
4418 fill.0 = target_price;
4419 }
4420 }
4421 }
4422 OrderSide::Sell => {
4423 let target_price = if order
4424 .trigger_price()
4425 .is_some_and(|trigger_price| order_price < trigger_price)
4426 {
4427 order.trigger_price().unwrap()
4428 } else {
4429 order_price
4430 };
4431
4432 for fill in &mut fills {
4433 let last_px = fill.0;
4434 if last_px > order_price {
4435 self.target_bid = self.core.bid;
4437 self.target_ask = self.core.ask;
4438 self.target_last = self.core.last;
4439 self.core.set_bid_raw(target_price);
4440 self.core.set_last_raw(target_price);
4441 fill.0 = target_price;
4442 }
4443 }
4444 }
4445 }
4446 }
4447
4448 self.apply_liquidity_consumption(
4449 fills,
4450 order.order_side(),
4451 order.leaves_qty(),
4452 book_prices_ref,
4453 )
4454 }
4455 None => panic!("Limit order must have a price"),
4456 }
4457 }
4458
4459 fn determine_market_price_and_volume(&self, order: &OrderAny) -> Vec<(Price, Quantity)> {
4460 let price = match order.order_side() {
4461 OrderSide::Buy => Price::max(FIXED_PRECISION),
4462 OrderSide::Sell => Price::min(FIXED_PRECISION),
4463 };
4464
4465 let mut fills = if self.config.liquidity_consumption {
4468 let size_prec = self.instrument.size_precision();
4469 self.book
4470 .get_all_crossed_levels(order.order_side(), price, size_prec)
4471 } else {
4472 let book_order = BookOrder::new(order.order_side(), price, order.quantity(), 0);
4473 self.book.simulate_fills(&book_order)
4474 };
4475
4476 if !self.fill_at_market
4479 && self.book_type == BookType::L1_MBP
4480 && !fills.is_empty()
4481 && matches!(
4482 order.order_type(),
4483 OrderType::StopMarket | OrderType::TrailingStopMarket | OrderType::MarketIfTouched
4484 )
4485 && let Some(trigger_price) = order.trigger_price()
4486 {
4487 fills[0] = (trigger_price, fills[0].1);
4488
4489 let mut remaining_qty = order.leaves_qty();
4491 let mut capped_fills = Vec::with_capacity(fills.len());
4492
4493 for (price, qty) in fills {
4494 if remaining_qty.is_zero() {
4495 break;
4496 }
4497
4498 let mut capped_qty = qty.min(remaining_qty);
4499 capped_qty.precision = qty.precision;
4500 if capped_qty.is_zero() {
4501 continue;
4502 }
4503
4504 remaining_qty = remaining_qty - capped_qty;
4505 capped_fills.push((price, capped_qty));
4506 }
4507
4508 return capped_fills;
4509 }
4510
4511 fills
4512 }
4513
4514 fn determine_market_fill_model_price_and_volume(
4515 &mut self,
4516 order: &OrderAny,
4517 ) -> anyhow::Result<(Vec<(Price, Quantity)>, bool)> {
4518 if let (Some(best_bid), Some(best_ask)) = (self.core.bid, self.core.ask)
4519 && let Some(book) = self.fill_model.get_orderbook_for_fill_simulation(
4520 &self.instrument,
4521 order,
4522 best_bid,
4523 best_ask,
4524 )?
4525 {
4526 let price = match order.order_side() {
4527 OrderSide::Buy => Price::max(FIXED_PRECISION),
4528 OrderSide::Sell => Price::min(FIXED_PRECISION),
4529 };
4530 let book_order = BookOrder::new(order.order_side(), price, order.quantity(), 0);
4531 let fills = book.simulate_fills(&book_order);
4532 if !fills.is_empty() {
4533 return Ok((fills, true));
4534 }
4535 }
4536 Ok((self.determine_market_price_and_volume(order), false))
4537 }
4538
4539 fn determine_limit_fill_model_price_and_volume(
4540 &mut self,
4541 order: &OrderAny,
4542 ) -> anyhow::Result<Vec<(Price, Quantity)>> {
4543 if let (Some(best_bid), Some(best_ask)) = (self.core.bid, self.core.ask)
4544 && let Some(book) = self.fill_model.get_orderbook_for_fill_simulation(
4545 &self.instrument,
4546 order,
4547 best_bid,
4548 best_ask,
4549 )?
4550 && let Some(limit_price) = order.price()
4551 {
4552 let book_order = BookOrder::new(order.order_side(), limit_price, order.quantity(), 0);
4553 let fills = book.simulate_fills(&book_order);
4554 if !fills.is_empty() {
4555 return Ok(fills);
4556 }
4557 }
4558 Ok(self.determine_limit_price_and_volume(order))
4559 }
4560
4561 pub fn fill_market_order(&mut self, client_order_id: ClientOrderId) {
4566 let mut order = match self.order_snapshot(client_order_id) {
4567 Some(order) => order,
4568 None => {
4569 log::error!("Cannot fill market order: order {client_order_id} not found in cache");
4570 return;
4571 }
4572 };
4573
4574 if order.is_closed() {
4575 self.purge_stale_core_entry(client_order_id);
4576 return;
4577 }
4578
4579 if order.is_quote_quantity()
4583 && !self.instrument.is_inverse()
4584 && !self.convert_quote_to_base_quantity(&mut order)
4585 {
4586 return;
4587 }
4588
4589 if let Some(filled_qty) = self.cached_filled_qty.get(&order.client_order_id())
4590 && filled_qty >= &order.quantity()
4591 {
4592 log::debug!(
4593 "Ignoring fill as already filled pending application of events: {:?}, {:?}, {:?}, {:?}",
4594 filled_qty,
4595 order.quantity(),
4596 order.filled_qty(),
4597 order.quantity()
4598 );
4599 return;
4600 }
4601
4602 let (venue_position_id, position) = self.fill_position_for_order(&order, Some(true));
4603
4604 if self.config.use_reduce_only && order.is_reduce_only() && position.is_none() {
4605 log::warn!(
4606 "Canceling REDUCE_ONLY {} as would increase position",
4607 order.order_type()
4608 );
4609 self.cancel_order(&order, None);
4610 return;
4611 }
4612
4613 order.set_liquidity_side(LiquiditySide::Taker);
4614 let (mut fills, from_synthetic) =
4615 match self.determine_market_fill_model_price_and_volume(&order) {
4616 Ok(result) => result,
4617 Err(e) => {
4618 log::error!(
4619 "Cannot fill market order {}: fill model failed: {e}",
4620 order.client_order_id()
4621 );
4622 return;
4623 }
4624 };
4625
4626 let protection_price: Option<Price> = if let Some(protection_points) =
4628 self.config.price_protection_points
4629 && matches!(
4630 order.order_type(),
4631 OrderType::Market | OrderType::StopMarket
4632 ) {
4633 protection_price_calculate(
4634 self.instrument.price_increment(),
4635 &order,
4636 protection_points,
4637 self.core.bid,
4638 self.core.ask,
4639 )
4640 .ok()
4641 } else {
4642 None
4643 };
4644
4645 if let Some(protection_price) = protection_price {
4646 fills = self.filter_fills_by_protection(fills, &order, protection_price);
4647 }
4648
4649 let is_trigger_price_fill = !self.fill_at_market
4652 && self.book_type == BookType::L1_MBP
4653 && matches!(
4654 order.order_type(),
4655 OrderType::StopMarket | OrderType::TrailingStopMarket | OrderType::MarketIfTouched
4656 )
4657 && order.trigger_price().is_some();
4658
4659 if !from_synthetic && !is_trigger_price_fill {
4660 fills = self.apply_liquidity_consumption(
4661 fills,
4662 order.order_side(),
4663 order.leaves_qty(),
4664 None,
4665 );
4666 }
4667
4668 if let Err(e) = self.apply_fills(
4669 &order,
4670 &fills,
4671 LiquiditySide::Taker,
4672 if self.config.use_reduce_only && order.is_reduce_only() {
4673 venue_position_id
4674 } else {
4675 None
4676 },
4677 position.as_ref(),
4678 protection_price,
4679 ) {
4680 log::error!("Cannot fill market order {}: {e}", order.client_order_id());
4681 }
4682 }
4683
4684 fn filter_fills_by_protection(
4685 &self,
4686 fills: Vec<(Price, Quantity)>,
4687 order: &OrderAny,
4688 protection_price: Price,
4689 ) -> Vec<(Price, Quantity)> {
4690 fills
4691 .into_iter()
4692 .filter(|(fill_price, _)| {
4693 match order.order_side() {
4694 OrderSide::Buy => *fill_price <= protection_price,
4696
4697 OrderSide::Sell => *fill_price >= protection_price,
4699 }
4700 })
4701 .collect()
4702 }
4703
4704 pub fn fill_limit_order(&mut self, client_order_id: ClientOrderId) {
4713 let mut order = match self.order_snapshot(client_order_id) {
4714 Some(order) => order,
4715 None => {
4716 log::error!("Cannot fill limit order: order {client_order_id} not found in cache");
4717 return;
4718 }
4719 };
4720
4721 if order.is_closed() {
4722 self.purge_stale_core_entry(client_order_id);
4723 return;
4724 }
4725
4726 if order.is_quote_quantity()
4730 && !self.instrument.is_inverse()
4731 && !self.convert_quote_to_base_quantity(&mut order)
4732 {
4733 return;
4734 }
4735
4736 match order.price() {
4737 Some(order_price) => {
4738 let cached_filled_qty = self.cached_filled_qty.get(&order.client_order_id());
4739 if let Some(&qty) = cached_filled_qty
4740 && qty >= order.quantity()
4741 {
4742 log::debug!(
4743 "Ignoring fill as already filled pending application of events: {}, {}, {}, {}",
4744 qty,
4745 order.quantity(),
4746 order.filled_qty(),
4747 order.leaves_qty(),
4748 );
4749 return;
4750 }
4751
4752 if order
4754 .liquidity_side()
4755 .is_some_and(|liquidity_side| liquidity_side == LiquiditySide::Maker)
4756 {
4757 let at_limit = if self.last_trade_size.is_some() && self.core.last.is_some() {
4760 self.core.last.is_some_and(|last| last == order_price)
4761 } else if order.order_side() == OrderSide::Buy {
4762 self.core.bid.is_some_and(|bid| bid == order_price)
4763 } else {
4764 self.core.ask.is_some_and(|ask| ask == order_price)
4765 };
4766
4767 if at_limit {
4768 let is_limit_filled = match self.fill_model.is_limit_filled() {
4769 Ok(value) => value,
4770 Err(e) => {
4771 log::error!(
4772 "Cannot fill limit order {}: fill model failed: {e}",
4773 order.client_order_id()
4774 );
4775 return;
4776 }
4777 };
4778
4779 if !is_limit_filled {
4780 return; }
4782 }
4783 }
4784
4785 let queue_allowed_raw = if self.config.queue_position {
4786 match self.determine_trade_fill_qty(&order) {
4787 None | Some(0) => {
4788 if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc)
4789 {
4790 self.cancel_order(&order, None);
4791 }
4792 return;
4793 }
4794 Some(allowed) => Some(allowed),
4795 }
4796 } else {
4797 None
4798 };
4799
4800 let (venue_position_id, position) = self.fill_position_for_order(&order, None);
4801
4802 if self.config.use_reduce_only && order.is_reduce_only() && position.is_none() {
4803 log::warn!(
4804 "Canceling REDUCE_ONLY {} as would increase position",
4805 order.order_type()
4806 );
4807 self.cancel_order(&order, None);
4808 return;
4809 }
4810
4811 let tc_before = self.trade_consumption;
4812 let mut fills = match self.determine_limit_fill_model_price_and_volume(&order) {
4813 Ok(fills) => fills,
4814 Err(e) => {
4815 log::error!(
4816 "Cannot fill limit order {}: fill model failed: {e}",
4817 order.client_order_id()
4818 );
4819 return;
4820 }
4821 };
4822
4823 if let Some(allowed_raw) = queue_allowed_raw {
4824 let size_prec = self.instrument.size_precision();
4825 let mut remaining = allowed_raw;
4826 fills = fills
4827 .into_iter()
4828 .filter_map(|(price, qty)| {
4829 if remaining == 0 {
4830 return None;
4831 }
4832
4833 let capped = qty.raw().min(remaining);
4834 remaining -= capped;
4835 Some((price, Quantity::from_raw(capped, size_prec)))
4836 })
4837 .collect();
4838
4839 let consumed: QuantityRaw = fills.iter().map(|(_, qty)| qty.raw()).sum();
4841
4842 if let Some(excess) = self.queue_excess.get_mut(&order.client_order_id()) {
4843 *excess = excess.saturating_sub(consumed);
4844 }
4845 self.trade_consumption = tc_before + consumed;
4846 }
4847
4848 if fills.is_empty() && self.config.liquidity_consumption {
4852 log::debug!(
4853 "Skipping fill for {}: no liquidity available after consumption",
4854 order.client_order_id()
4855 );
4856
4857 if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc) {
4858 self.cancel_order(&order, None);
4859 }
4860
4861 return;
4862 }
4863
4864 let liquidity_side = order.liquidity_side().unwrap();
4865 if let Err(e) = self.apply_fills(
4866 &order,
4867 &fills,
4868 liquidity_side,
4869 venue_position_id,
4870 position.as_ref(),
4871 None,
4872 ) {
4873 log::error!("Cannot fill limit order {}: {e}", order.client_order_id());
4874 }
4875 }
4876 None => panic!("Limit order must have a price"),
4877 }
4878 }
4879
4880 fn fill_position_for_order(
4881 &mut self,
4882 order: &OrderAny,
4883 generate: Option<bool>,
4884 ) -> (Option<PositionId>, Option<Position>) {
4885 if self.oms_type == OmsType::Hedging
4886 && self.config.use_reduce_only
4887 && order.is_reduce_only()
4888 {
4889 let cache = self.cache.as_ref().borrow();
4890
4891 if let Some(position) = cache.position_for_order(&order.client_order_id()) {
4892 let position = position.clone_without_events();
4893 return (Some(position.id), Some(position));
4894 }
4895
4896 if let Some(position) = Self::open_position_reduced_by_order(&cache, order) {
4897 return (Some(position.id), Some(position));
4898 }
4899 }
4900
4901 let venue_position_id = self.ids_generator.get_position_id(order, generate);
4902
4903 let position = {
4904 let cache = self.cache.as_ref().borrow();
4905 venue_position_id
4906 .as_ref()
4907 .and_then(|position_id| cache.position(position_id))
4908 .map(|position| position.clone_without_events())
4909 };
4910
4911 (venue_position_id, position)
4912 }
4913
4914 fn position_for_order_in_cache(&self, cache: &Cache, order: &OrderAny) -> Option<Position> {
4915 if let Some(position) = cache.position_for_order(&order.client_order_id()) {
4916 return Some(position.clone_without_events());
4917 }
4918
4919 if self.oms_type == OmsType::Netting {
4920 let position_id = PositionId::new(
4921 format!("{}-{}", order.instrument_id(), order.strategy_id()).as_str(),
4922 );
4923 return cache
4924 .position(&position_id)
4925 .map(|position| position.clone_without_events());
4926 }
4927
4928 if self.oms_type == OmsType::Hedging
4929 && self.config.use_reduce_only
4930 && order.is_reduce_only()
4931 {
4932 return Self::open_position_reduced_by_order(cache, order);
4933 }
4934
4935 None
4936 }
4937
4938 fn open_position_reduced_by_order(cache: &Cache, order: &OrderAny) -> Option<Position> {
4939 cache
4940 .positions_open(
4941 None,
4942 Some(&order.instrument_id()),
4943 Some(&order.strategy_id()),
4944 None,
4945 None,
4946 )
4947 .into_iter()
4948 .find(|position| order.would_reduce_only(position.side, position.quantity))
4949 .map(|position| position.clone_without_events())
4950 }
4951
4952 fn apply_fills(
4953 &mut self,
4954 order: &OrderAny,
4955 fills: &[(Price, Quantity)],
4956 liquidity_side: LiquiditySide,
4957 venue_position_id: Option<PositionId>,
4958 position: Option<&Position>,
4959 protection_price: Option<Price>,
4960 ) -> anyhow::Result<()> {
4961 if order.time_in_force() == TimeInForce::Fok {
4962 let mut total_size = Quantity::zero(order.quantity().precision);
4963
4964 for &(fill_px, fill_qty) in fills {
4965 if self
4966 .normalize_price_for_current_instrument(fill_px)
4967 .is_some()
4968 && let Some(fill_qty) = self.normalize_quantity_for_current_instrument(fill_qty)
4969 {
4970 total_size = total_size.add(fill_qty);
4971 }
4972 }
4973
4974 if order.leaves_qty() > total_size {
4975 self.cancel_order(order, None);
4976 return Ok(());
4977 }
4978 }
4979
4980 if fills.is_empty() {
4981 if order.status() == OrderStatus::Submitted {
4982 self.generate_order_rejected(
4983 order,
4984 format!("No market for {}", order.instrument_id()).into(),
4985 );
4986 } else {
4987 log::error!(
4988 "Cannot fill order: no fills from book when fills were expected (check size in data)"
4989 );
4990 return Ok(());
4991 }
4992 }
4993
4994 let venue_position_id = if self.oms_type == OmsType::Netting {
4996 None
4997 } else {
4998 venue_position_id
4999 };
5000
5001 let mut initial_market_to_limit_fill = false;
5002 let mut total_filled = self
5003 .cached_filled_qty
5004 .get(&order.client_order_id())
5005 .copied()
5006 .unwrap_or_else(|| order.filled_qty());
5007 let initial_total_filled = total_filled;
5008 let mut last_fill_px: Option<Price> = None;
5009 let mut reduce_only_remaining = None;
5010 let mut reduce_only_filled = None;
5011
5012 if self.config.use_reduce_only
5013 && order.is_reduce_only()
5014 && let Some(current_position) = position
5015 {
5016 let remaining = self.position_quantity_remaining(order, current_position)?;
5017 if remaining.is_zero() {
5018 self.cancel_order(order, None);
5019 return Ok(());
5020 }
5021
5022 reduce_only_remaining = Some(remaining);
5023 reduce_only_filled = Some(total_filled);
5024 }
5025
5026 for &(fill_px, fill_qty) in fills {
5027 let Some(mut fill_px) = self.normalize_fill_price(fill_px, order.client_order_id())
5028 else {
5029 continue;
5030 };
5031
5032 let Some(fill_qty) = self.normalize_fill_quantity(fill_qty, order.client_order_id())
5033 else {
5034 continue;
5035 };
5036
5037 if order.filled_qty() == Quantity::zero(order.filled_qty().precision)
5038 && order.order_type() == OrderType::MarketToLimit
5039 {
5040 self.generate_order_updated(order, order.quantity(), Some(fill_px), None, None);
5041 initial_market_to_limit_fill = true;
5042 }
5043
5044 if self.book_type == BookType::L1_MBP && self.fill_model.is_slipped()? {
5045 fill_px = match order.order_side() {
5046 OrderSide::Buy => fill_px.add(self.instrument.price_increment()),
5047 OrderSide::Sell => fill_px.sub(self.instrument.price_increment()),
5048 }
5049 }
5050
5051 let mut effective_fill_qty = fill_qty;
5052
5053 if let Some(remaining) = reduce_only_remaining {
5054 if remaining.is_zero() {
5055 return Ok(());
5056 }
5057
5058 if effective_fill_qty > remaining {
5059 let precision = effective_fill_qty.precision;
5060 effective_fill_qty = remaining;
5061 effective_fill_qty.precision = precision;
5062 }
5063 }
5064
5065 if fill_qty.is_zero() {
5066 if fills.len() == 1 && order.status() == OrderStatus::Submitted {
5067 self.generate_order_rejected(
5068 order,
5069 format!("No market for {}", order.instrument_id()).into(),
5070 );
5071 }
5072 return Ok(());
5073 }
5074
5075 let capped_fill_qty = min(
5077 effective_fill_qty,
5078 order.quantity().saturating_sub(total_filled),
5079 );
5080 let reduce_only_exhausts_position =
5081 reduce_only_remaining.is_some_and(|remaining| capped_fill_qty >= remaining);
5082
5083 if reduce_only_exhausts_position {
5084 let mut reduce_only_target = reduce_only_filled
5085 .unwrap_or(initial_total_filled)
5086 .checked_add(capped_fill_qty)
5087 .expect("Overflow occurred when adding reduce-only target quantity");
5088 reduce_only_target.precision = self.instrument.size_precision();
5089
5090 if order.quantity() != reduce_only_target {
5091 self.generate_order_updated(order, reduce_only_target, None, None, None);
5092 }
5093 }
5094
5095 total_filled = total_filled.add(capped_fill_qty);
5096
5097 if let Some(remaining) = reduce_only_remaining.as_mut() {
5098 *remaining = *remaining - capped_fill_qty.min(*remaining);
5099 }
5100
5101 if let Some(filled) = reduce_only_filled.as_mut() {
5102 *filled = filled
5103 .checked_add(capped_fill_qty)
5104 .expect("Overflow occurred when adding reduce-only filled quantity");
5105 }
5106
5107 self.fill_order(
5108 order,
5109 fill_px,
5110 effective_fill_qty,
5111 liquidity_side,
5112 venue_position_id,
5113 position,
5114 )?;
5115 last_fill_px = Some(fill_px);
5116
5117 if order.order_type() == OrderType::MarketToLimit && initial_market_to_limit_fill {
5118 return Ok(());
5120 }
5121
5122 if reduce_only_exhausts_position {
5123 self.purge_cached_filled_qty_if_closed(order.client_order_id());
5124 return Ok(());
5125 }
5126 }
5127
5128 let leaves_remaining = total_filled < order.quantity();
5129 let filled_in_loop = total_filled > initial_total_filled;
5130
5131 if order.time_in_force() == TimeInForce::Ioc && leaves_remaining {
5132 self.cancel_order(order, None);
5133 return Ok(());
5134 }
5135
5136 if leaves_remaining
5139 && (order.is_open() || filled_in_loop)
5140 && self.book_type == BookType::L1_MBP
5141 && matches!(
5142 order.order_type(),
5143 OrderType::Market
5144 | OrderType::MarketIfTouched
5145 | OrderType::StopMarket
5146 | OrderType::TrailingStopMarket
5147 )
5148 {
5149 let Some(last_fill_px) = last_fill_px else {
5151 return Ok(());
5152 };
5153
5154 let side = order.order_side();
5155 let slip_fill_px = match side {
5156 OrderSide::Buy => last_fill_px.add(self.instrument.price_increment()),
5157 OrderSide::Sell => last_fill_px.sub(self.instrument.price_increment()),
5158 };
5159
5160 if let Some(protection_price) = protection_price {
5161 let exceeds_boundary = match side {
5162 OrderSide::Buy => slip_fill_px > protection_price,
5163 OrderSide::Sell => slip_fill_px < protection_price,
5164 };
5165
5166 if exceeds_boundary {
5167 return Ok(());
5168 }
5169 }
5170
5171 let mut leaves_qty = order.quantity().saturating_sub(total_filled);
5172
5173 if let Some(remaining) = reduce_only_remaining {
5174 if remaining.is_zero() {
5175 return Ok(());
5176 }
5177
5178 if leaves_qty > remaining {
5179 let precision = leaves_qty.precision;
5180 leaves_qty = remaining;
5181 leaves_qty.precision = precision;
5182 }
5183
5184 if leaves_qty >= remaining {
5185 let mut reduce_only_target = reduce_only_filled
5186 .unwrap_or(initial_total_filled)
5187 .checked_add(leaves_qty)
5188 .expect("Overflow occurred when adding reduce-only target quantity");
5189 reduce_only_target.precision = self.instrument.size_precision();
5190
5191 if order.quantity() != reduce_only_target {
5192 self.generate_order_updated(order, reduce_only_target, None, None, None);
5193 }
5194 }
5195 }
5196
5197 if leaves_qty.is_zero() {
5198 return Ok(());
5199 }
5200
5201 self.fill_order(
5202 order,
5203 slip_fill_px,
5204 leaves_qty,
5205 liquidity_side,
5206 venue_position_id,
5207 position,
5208 )?;
5209 self.purge_cached_filled_qty_if_closed(order.client_order_id());
5210 }
5211
5212 Ok(())
5213 }
5214
5215 fn normalize_fill_price(
5216 &self,
5217 fill_px: Price,
5218 client_order_id: ClientOrderId,
5219 ) -> Option<Price> {
5220 let normalized = self.normalize_price_for_current_instrument(fill_px);
5221 if normalized.is_none() {
5222 log::warn!(
5223 "Skipping fill for {client_order_id}: fill price {fill_px} is not compatible \
5224 with {} price_precision={} price_increment={}",
5225 self.instrument.id(),
5226 self.instrument.price_precision(),
5227 self.instrument.price_increment()
5228 );
5229 }
5230 normalized
5231 }
5232
5233 fn normalize_fill_quantity(
5234 &self,
5235 fill_qty: Quantity,
5236 client_order_id: ClientOrderId,
5237 ) -> Option<Quantity> {
5238 let normalized = self.normalize_quantity_for_current_instrument(fill_qty);
5239 if normalized.is_none() {
5240 log::warn!(
5241 "Skipping fill for {client_order_id}: fill quantity {fill_qty} is not compatible \
5242 with {} size_precision={}",
5243 self.instrument.id(),
5244 self.instrument.size_precision()
5245 );
5246 }
5247 normalized
5248 }
5249
5250 fn position_quantity_remaining(
5251 &mut self,
5252 order: &OrderAny,
5253 position: &Position,
5254 ) -> anyhow::Result<Quantity> {
5255 self.purge_applied_fills();
5256 let mut quantity = match position.side {
5257 PositionSide::Long => position.quantity.as_decimal(),
5258 PositionSide::Short => -position.quantity.as_decimal(),
5259 PositionSide::Flat => Decimal::ZERO,
5260 };
5261
5262 for fill in self.pending_fills.values() {
5263 if fill.position_id == Some(position.id) {
5264 quantity = quantity
5265 .checked_add(fill.quantity_change)
5266 .ok_or_else(|| anyhow::anyhow!("Pending position quantity overflow"))?;
5267 }
5268 }
5269
5270 if (order.is_buy() && quantity >= Decimal::ZERO)
5271 || (order.is_sell() && quantity <= Decimal::ZERO)
5272 {
5273 return Ok(Quantity::zero(position.quantity.precision));
5274 }
5275 Ok(Quantity::from_decimal_dp(
5276 quantity.abs(),
5277 position.quantity.precision,
5278 )?)
5279 }
5280
5281 fn purge_applied_fills(&mut self) {
5282 let cache = self.cache.borrow();
5283 self.pending_fills.retain(|trade_id, fill| {
5284 fill.position_id = fill
5285 .position_id
5286 .or_else(|| cache.position_id(&fill.client_order_id).copied());
5287 let Some(position_id) = fill.position_id else {
5288 return cache.order_exists(&fill.client_order_id);
5289 };
5290 let Some(position) = cache.position(&position_id) else {
5291 return cache.order_exists(&fill.client_order_id);
5292 };
5293
5294 if position.trade_ids.contains(trade_id) {
5295 return false;
5296 }
5297 let opening_trade_id = position.events.first().map(|event| event.trade_id);
5298 if opening_trade_id != fill.opening_trade_id {
5299 if position.replay_events.iter().any(|event| {
5301 matches!(event, PositionReplayEvent::Filled(event) if event.trade_id == *trade_id)
5302 }) || cache.position_snapshots(Some(&position_id), None).iter()
5303 .any(|snapshot| snapshot.trade_ids.contains(trade_id))
5304 {
5305 return false;
5306 }
5307 fill.opening_trade_id = opening_trade_id;
5308 }
5309 true
5310 });
5311 }
5312
5313 fn fill_order(
5314 &mut self,
5315 order: &OrderAny,
5316 last_px: Price,
5317 last_qty: Quantity,
5318 liquidity_side: LiquiditySide,
5319 venue_position_id: Option<PositionId>,
5320 position: Option<&Position>,
5321 ) -> anyhow::Result<()> {
5322 self.check_size_precision(last_qty.precision, "fill quantity")?;
5323
5324 let (last_qty, new_filled_qty) =
5325 if let Some(filled_qty) = self.cached_filled_qty.get(&order.client_order_id()) {
5326 let leaves_qty = order.quantity().saturating_sub(*filled_qty);
5327 let last_qty = min(last_qty, leaves_qty);
5328 (last_qty, *filled_qty + last_qty)
5329 } else {
5330 let last_qty = min(last_qty, order.quantity());
5331 (last_qty, last_qty)
5332 };
5333
5334 if last_qty.is_zero() {
5335 return Ok(());
5336 }
5337
5338 let fee_order;
5339 let commission_order = {
5340 let mut cloned = order.clone();
5343 write_filled_qty(&mut cloned, new_filled_qty.saturating_sub(last_qty));
5344 if order.liquidity_side() != Some(liquidity_side) {
5345 cloned.set_liquidity_side(liquidity_side);
5346 }
5347 fee_order = cloned;
5348 &fee_order
5349 };
5350
5351 let underlying_px = self.fee_underlying_price()?;
5352 let commission = self.fee_model.get_commission_with_context(
5353 commission_order,
5354 last_qty,
5355 last_px,
5356 &self.instrument,
5357 underlying_px,
5358 )?;
5359
5360 let reduce_only_order_ids = position
5362 .map(|position| self.reduce_only_order_ids(position.id))
5363 .unwrap_or_default();
5364
5365 self.cached_filled_qty
5366 .insert(order.client_order_id(), new_filled_qty);
5367
5368 let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
5369 self.generate_order_filled(
5370 order,
5371 venue_order_id,
5372 venue_position_id,
5373 last_qty,
5374 last_px,
5375 self.instrument.quote_currency(),
5376 commission,
5377 liquidity_side,
5378 );
5379
5380 let post_fill_filled_qty = self
5381 .cached_filled_qty
5382 .get(&order.client_order_id())
5383 .copied()
5384 .unwrap_or(order.filled_qty());
5385 let post_fill_leaves_qty = order.quantity().saturating_sub(post_fill_filled_qty);
5386 let fully_filled = post_fill_leaves_qty.is_zero();
5387
5388 if order.is_closed() || fully_filled {
5389 if self.core.order_exists(order.client_order_id()) {
5390 self.delete_core_order(order.client_order_id());
5391 }
5392
5393 self.remove_queue_position(order.client_order_id());
5394
5395 if order.order_type() != OrderType::MarketToLimit {
5398 self.purge_cached_filled_qty_if_closed(order.client_order_id());
5399 }
5400 }
5401
5402 if self.config.support_contingent_orders
5403 && let Some(contingency_type) = order.contingency_type()
5404 {
5405 match contingency_type {
5406 ContingencyType::Oto => {
5407 if let Some(linked_orders_ids) = order.linked_order_ids() {
5408 for client_order_id in linked_orders_ids {
5409 let mut child_order = match self.order_snapshot(*client_order_id) {
5410 Some(child_order) => child_order,
5411 None => anyhow::bail!("Order {client_order_id} not found in cache"),
5412 };
5413
5414 if child_order.is_closed() || child_order.is_active_local() {
5415 continue;
5416 }
5417
5418 if self.inflight_orders.contains(*client_order_id) {
5419 continue;
5420 }
5421
5422 if let (None, Some(position_id)) =
5424 (child_order.position_id(), order.position_id())
5425 {
5426 self.cache
5427 .borrow_mut()
5428 .add_position_id(
5429 &position_id,
5430 &self.venue,
5431 client_order_id,
5432 &child_order.strategy_id(),
5433 )
5434 .unwrap();
5435 log::debug!(
5436 "Added position id {position_id} to cache for order {client_order_id}"
5437 );
5438 }
5439
5440 if (!child_order.is_open())
5441 || (matches!(child_order.status(), OrderStatus::PendingUpdate)
5442 && child_order
5443 .previous_status()
5444 .is_some_and(|s| matches!(s, OrderStatus::Submitted)))
5445 {
5446 let account_id = order
5447 .account_id()
5448 .or_else(|| self.account_ids.get(&order.trader_id()).copied())
5449 .ok_or_else(|| {
5450 anyhow::anyhow!(
5451 "Account ID not found for trader {}",
5452 order.trader_id()
5453 )
5454 })?;
5455 self.process_order(&mut child_order, account_id);
5456 }
5457 }
5458 } else {
5459 log::error!(
5460 "OTO order {} does not have linked orders",
5461 order.client_order_id()
5462 );
5463 }
5464 }
5465 ContingencyType::Oco => {
5466 if let Some(linked_orders_ids) = order.linked_order_ids() {
5467 for client_order_id in linked_orders_ids {
5468 let child_order = match self.order_snapshot(*client_order_id) {
5469 Some(child_order) => child_order,
5470 None => anyhow::bail!("Order {client_order_id} not found in cache"),
5471 };
5472
5473 if child_order.is_closed() || child_order.is_active_local() {
5474 continue;
5475 }
5476
5477 self.cancel_order(&child_order, Some(false));
5478 }
5479 } else {
5480 log::error!(
5481 "OCO order {} does not have linked orders",
5482 order.client_order_id()
5483 );
5484 }
5485 }
5486 ContingencyType::Ouo => {
5487 if let Some(linked_orders_ids) = order.linked_order_ids() {
5488 for client_order_id in linked_orders_ids {
5489 let child_order = match self.order_snapshot(*client_order_id) {
5490 Some(child_order) => child_order,
5491 None => anyhow::bail!("Order {client_order_id} not found in cache"),
5492 };
5493
5494 if child_order.is_active_local() {
5495 continue;
5496 }
5497
5498 let child_filled_qty = self
5499 .cached_filled_qty
5500 .get(&child_order.client_order_id())
5501 .copied()
5502 .unwrap_or(child_order.filled_qty());
5503
5504 if post_fill_leaves_qty.is_zero() && child_order.is_open() {
5505 self.cancel_order(&child_order, None);
5506 } else if child_order.is_open()
5507 && child_filled_qty >= post_fill_leaves_qty
5508 {
5509 self.cancel_order(&child_order, Some(false));
5510 } else if post_fill_leaves_qty.non_zero()
5511 && post_fill_leaves_qty != child_order.leaves_qty()
5512 {
5513 let price = child_order.price();
5514 let trigger_price = child_order.trigger_price();
5515 self.update_order(
5516 &child_order,
5517 Some(post_fill_leaves_qty),
5518 price,
5519 trigger_price,
5520 Some(false),
5521 );
5522 }
5523 }
5524 } else {
5525 log::error!(
5526 "OUO order {} does not have linked orders",
5527 order.client_order_id()
5528 );
5529 }
5530 }
5531 }
5532 }
5533
5534 if let Some(position) = position {
5535 let mut reduce_only_order_ids = reduce_only_order_ids;
5536 reduce_only_order_ids.extend(self.reduce_only_order_ids(position.id));
5537 reduce_only_order_ids.sort_unstable();
5538 reduce_only_order_ids.dedup();
5539 self.sync_reduce_only_orders(order, position, &reduce_only_order_ids)?;
5540 }
5541
5542 Ok(())
5543 }
5544
5545 fn reduce_only_order_ids(&self, position_id: PositionId) -> Vec<ClientOrderId> {
5546 if !self.config.use_reduce_only {
5547 return Vec::new();
5548 }
5549
5550 let cache = self.cache.borrow();
5551 let mut order_ids = Vec::new();
5552
5553 for resting in self.core.iter_orders() {
5554 let Some(order) = cache.order(&resting.client_order_id) else {
5555 continue;
5556 };
5557
5558 if !order.is_reduce_only() || !order.is_open() || !order.is_passive() {
5559 continue;
5560 }
5561
5562 let matches_position = match cache.position_id(&resting.client_order_id) {
5563 Some(id) => *id == position_id,
5564 None => self
5565 .position_for_order_in_cache(&cache, &order)
5566 .is_some_and(|position| position.id == position_id),
5567 };
5568
5569 if matches_position {
5570 order_ids.push(resting.client_order_id);
5571 }
5572 }
5573 order_ids.sort_unstable();
5574 order_ids
5575 }
5576
5577 fn sync_reduce_only_orders(
5578 &mut self,
5579 filled_order: &OrderAny,
5580 position: &Position,
5581 order_ids: &[ClientOrderId],
5582 ) -> anyhow::Result<()> {
5583 for &client_order_id in order_ids {
5584 if client_order_id == filled_order.client_order_id()
5586 || !self.core.order_exists(client_order_id)
5587 {
5588 continue;
5589 }
5590
5591 let Some(order) = self.order_snapshot(client_order_id) else {
5592 continue;
5593 };
5594
5595 if !order.is_reduce_only() || !order.is_open() || !order.is_passive() {
5596 continue;
5597 }
5598
5599 let position = self.cache.borrow().position(&position.id).map_or_else(
5602 || position.clone_without_events(),
5603 |position| position.clone_without_events(),
5604 );
5605
5606 let remaining = self.position_quantity_remaining(&order, &position)?;
5607 if remaining.is_zero() {
5608 self.cancel_reduce_only_order(&order, filled_order.client_order_id())?;
5609 continue;
5610 }
5611
5612 let leaves = self.parent_capped_leaves(&order, remaining);
5613 let target = order.filled_qty().checked_add(leaves).ok_or_else(|| {
5614 anyhow::anyhow!("Reduce-only quantity overflow for order {client_order_id}")
5615 })?;
5616
5617 if order.quantity() != target {
5618 self.generate_order_updated(
5620 &order,
5621 target,
5622 order.price(),
5623 order.trigger_price(),
5624 None,
5625 );
5626
5627 if target == order.filled_qty() {
5628 self.cancel_reduce_only_order(&order, filled_order.client_order_id())?;
5629 } else if self.config.support_contingent_orders
5630 && order.contingency_type() == Some(ContingencyType::Ouo)
5631 {
5632 self.sync_ouo_leaves(&order, leaves, filled_order.client_order_id())?;
5633 }
5634 }
5635 }
5636
5637 Ok(())
5638 }
5639
5640 fn cancel_reduce_only_order(
5641 &mut self,
5642 order: &OrderAny,
5643 filled_order_id: ClientOrderId,
5644 ) -> anyhow::Result<()> {
5645 let propagate = self.config.support_contingent_orders
5646 && order.contingency_type() == Some(ContingencyType::Ouo);
5647 self.cancel_order(order, Some(!propagate));
5648
5649 if propagate {
5650 self.sync_ouo_leaves(
5651 order,
5652 Quantity::zero(order.quantity().precision),
5653 filled_order_id,
5654 )?;
5655 }
5656 Ok(())
5657 }
5658
5659 fn parent_capped_leaves(&self, order: &OrderAny, leaves: Quantity) -> Quantity {
5660 let parent = if self.config.support_contingent_orders {
5661 order
5662 .parent_order_id()
5663 .and_then(|id| self.order_snapshot(id))
5664 } else {
5665 None
5666 };
5667
5668 parent.map_or(leaves, |parent| {
5669 min(
5670 leaves,
5671 parent.filled_qty().saturating_sub(order.filled_qty()),
5672 )
5673 })
5674 }
5675
5676 fn sync_ouo_leaves(
5677 &mut self,
5678 order: &OrderAny,
5679 leaves: Quantity,
5680 filled_order_id: ClientOrderId,
5681 ) -> anyhow::Result<()> {
5682 for &client_order_id in order.linked_order_ids().into_iter().flatten() {
5683 if client_order_id == filled_order_id || !self.core.order_exists(client_order_id) {
5684 continue;
5685 }
5686
5687 let Some(sibling) = self.order_snapshot(client_order_id) else {
5688 continue;
5689 };
5690
5691 if sibling.is_closed() || sibling.is_active_local() || !sibling.is_passive() {
5692 continue;
5693 }
5694
5695 if leaves.is_zero() {
5697 self.cancel_order(&sibling, Some(false));
5698 continue;
5699 }
5700
5701 if !sibling.is_open() {
5702 continue;
5703 }
5704
5705 let leaves = self.parent_capped_leaves(&sibling, leaves);
5706 let target = sibling.filled_qty().checked_add(leaves).ok_or_else(|| {
5707 anyhow::anyhow!("OUO quantity overflow for order {client_order_id}")
5708 })?;
5709
5710 if sibling.quantity() != target {
5711 self.generate_order_updated(
5712 &sibling,
5713 target,
5714 sibling.price(),
5715 sibling.trigger_price(),
5716 None,
5717 );
5718 }
5719
5720 if leaves.is_zero() {
5721 self.cancel_order(&sibling, Some(false));
5722 }
5723 }
5724 Ok(())
5725 }
5726
5727 fn fee_underlying_price(&self) -> CorrectnessResult<Option<Price>> {
5728 if !matches!(
5729 self.instrument,
5730 InstrumentAny::CryptoOption(_) | InstrumentAny::OptionContract(_)
5731 ) {
5732 return Ok(None);
5733 }
5734
5735 let Some(underlying) = self.instrument.underlying() else {
5736 return Ok(None);
5737 };
5738
5739 let underlying_id = InstrumentId::from(format!("{underlying}.{}", self.venue).as_str());
5740 let instrument_id = self.instrument.id();
5741
5742 let cache = self.cache.borrow();
5743 if let Some(price) = cache
5744 .price(&underlying_id, PriceType::Last)
5745 .or_else(|| cache.price(&underlying_id, PriceType::Mark))
5746 .or_else(|| cache.price(&underlying_id, PriceType::Mid))
5747 {
5748 return Ok(Some(price));
5749 }
5750
5751 cache
5752 .option_greeks(&instrument_id)
5753 .and_then(|greeks| greeks.underlying_price)
5754 .map(|price| Price::new_checked(price, FIXED_PRECISION))
5755 .transpose()
5756 }
5757
5758 fn cached_order_is_closed(&self, client_order_id: ClientOrderId) -> bool {
5759 self.cache
5760 .borrow()
5761 .order(&client_order_id)
5762 .is_none_or(|order| order.is_closed())
5763 }
5764
5765 fn purge_cached_filled_qty_if_closed(&mut self, client_order_id: ClientOrderId) {
5766 if self.cached_order_is_closed(client_order_id) {
5767 self.cached_filled_qty.swap_remove(&client_order_id);
5768 }
5769 }
5770
5771 fn purge_closed_cached_filled_qty(&mut self) {
5772 let client_order_ids: Vec<ClientOrderId> = self.cached_filled_qty.keys().copied().collect();
5773
5774 for client_order_id in client_order_ids {
5775 self.purge_cached_filled_qty_if_closed(client_order_id);
5776 }
5777 }
5778
5779 fn update_limit_order(
5780 &mut self,
5781 order: &OrderAny,
5782 quantity: Quantity,
5783 price: Price,
5784 ) -> ModifyOutcome {
5785 if self.core.is_limit_matched(order.order_side(), price) {
5786 if order.is_post_only() {
5787 self.generate_order_modify_rejected(
5788 order.trader_id(),
5789 order.strategy_id(),
5790 order.instrument_id(),
5791 order.client_order_id(),
5792 Ustr::from(format!(
5793 "POST_ONLY {} {} order with new limit px of {} would have been a TAKER: bid={}, ask={}",
5794 order.order_type(),
5795 order.order_side(),
5796 price,
5797 self.core.bid.map_or_else(|| "None".to_string(), |p| p.to_string()),
5798 self.core.ask.map_or_else(|| "None".to_string(), |p| p.to_string())
5799 ).as_str()),
5800 order.venue_order_id(),
5801 order.account_id(),
5802 );
5803 return ModifyOutcome::Rejected;
5804 }
5805
5806 self.generate_order_updated(order, quantity, Some(price), None, None);
5807
5808 let client_order_id = order.client_order_id();
5810 if let Some(mut order) = self.cache.borrow_mut().order_mut(&client_order_id) {
5811 order.set_liquidity_side(LiquiditySide::Taker);
5812 }
5813 self.fill_limit_order(client_order_id);
5814 return ModifyOutcome::Applied;
5815 }
5816 self.generate_order_updated(order, quantity, Some(price), None, None);
5817 ModifyOutcome::Applied
5818 }
5819
5820 fn update_stop_market_order(
5821 &self,
5822 order: &OrderAny,
5823 quantity: Quantity,
5824 trigger_price: Price,
5825 ) -> ModifyOutcome {
5826 if self.core.is_stop_matched_with_trigger_type(
5827 order.order_side(),
5828 trigger_price,
5829 order.trigger_type().unwrap_or(TriggerType::Default),
5830 ) {
5831 self.generate_order_modify_rejected(
5832 order.trader_id(),
5833 order.strategy_id(),
5834 order.instrument_id(),
5835 order.client_order_id(),
5836 Ustr::from(
5837 format!(
5838 "{} {} order new stop px of {} was in the market: bid={}, ask={}",
5839 order.order_type(),
5840 order.order_side(),
5841 trigger_price,
5842 self.core
5843 .bid
5844 .map_or_else(|| "None".to_string(), |p| p.to_string()),
5845 self.core
5846 .ask
5847 .map_or_else(|| "None".to_string(), |p| p.to_string())
5848 )
5849 .as_str(),
5850 ),
5851 order.venue_order_id(),
5852 order.account_id(),
5853 );
5854 return ModifyOutcome::Rejected;
5855 }
5856
5857 self.generate_order_updated(order, quantity, None, Some(trigger_price), None);
5858 ModifyOutcome::Applied
5859 }
5860
5861 fn update_stop_limit_order(
5862 &mut self,
5863 order: &OrderAny,
5864 quantity: Quantity,
5865 price: Price,
5866 trigger_price: Price,
5867 ) -> ModifyOutcome {
5868 if order.is_triggered().is_some_and(|t| t) {
5869 if self.core.is_limit_matched(order.order_side(), price) {
5870 return self.update_limit_order(order, quantity, price);
5871 }
5872 } else {
5873 if self.core.is_stop_matched_with_trigger_type(
5875 order.order_side(),
5876 trigger_price,
5877 order.trigger_type().unwrap_or(TriggerType::Default),
5878 ) {
5879 self.generate_order_modify_rejected(
5880 order.trader_id(),
5881 order.strategy_id(),
5882 order.instrument_id(),
5883 order.client_order_id(),
5884 Ustr::from(
5885 format!(
5886 "{} {} order new stop px of {} was in the market: bid={}, ask={}",
5887 order.order_type(),
5888 order.order_side(),
5889 trigger_price,
5890 self.core
5891 .bid
5892 .map_or_else(|| "None".to_string(), |p| p.to_string()),
5893 self.core
5894 .ask
5895 .map_or_else(|| "None".to_string(), |p| p.to_string())
5896 )
5897 .as_str(),
5898 ),
5899 order.venue_order_id(),
5900 order.account_id(),
5901 );
5902 return ModifyOutcome::Rejected;
5903 }
5904 }
5905
5906 self.generate_order_updated(order, quantity, Some(price), Some(trigger_price), None);
5907 ModifyOutcome::Applied
5908 }
5909
5910 fn update_market_if_touched_order(
5911 &self,
5912 order: &OrderAny,
5913 quantity: Quantity,
5914 trigger_price: Price,
5915 ) -> ModifyOutcome {
5916 if self.core.is_touch_triggered_with_trigger_type(
5917 order.order_side(),
5918 trigger_price,
5919 order.trigger_type().unwrap_or(TriggerType::Default),
5920 ) {
5921 self.generate_order_modify_rejected(
5922 order.trader_id(),
5923 order.strategy_id(),
5924 order.instrument_id(),
5925 order.client_order_id(),
5926 Ustr::from(
5927 format!(
5928 "{} {} order new trigger px of {} was in the market: bid={}, ask={}",
5929 order.order_type(),
5930 order.order_side(),
5931 trigger_price,
5932 self.core
5933 .bid
5934 .map_or_else(|| "None".to_string(), |p| p.to_string()),
5935 self.core
5936 .ask
5937 .map_or_else(|| "None".to_string(), |p| p.to_string())
5938 )
5939 .as_str(),
5940 ),
5941 order.venue_order_id(),
5942 order.account_id(),
5943 );
5944
5945 return ModifyOutcome::Rejected;
5947 }
5948
5949 self.generate_order_updated(order, quantity, None, Some(trigger_price), None);
5950 ModifyOutcome::Applied
5951 }
5952
5953 fn update_limit_if_touched_order(
5954 &mut self,
5955 order: &OrderAny,
5956 quantity: Quantity,
5957 price: Price,
5958 trigger_price: Price,
5959 ) -> ModifyOutcome {
5960 if order.is_triggered().is_some_and(|t| t) {
5961 if self.core.is_limit_matched(order.order_side(), price) {
5962 return self.update_limit_order(order, quantity, price);
5963 }
5964 } else {
5965 if self.core.is_touch_triggered_with_trigger_type(
5967 order.order_side(),
5968 trigger_price,
5969 order.trigger_type().unwrap_or(TriggerType::Default),
5970 ) {
5971 self.generate_order_modify_rejected(
5972 order.trader_id(),
5973 order.strategy_id(),
5974 order.instrument_id(),
5975 order.client_order_id(),
5976 Ustr::from(
5977 format!(
5978 "{} {} order new trigger px of {} was in the market: bid={}, ask={}",
5979 order.order_type(),
5980 order.order_side(),
5981 trigger_price,
5982 self.core
5983 .bid
5984 .map_or_else(|| "None".to_string(), |p| p.to_string()),
5985 self.core
5986 .ask
5987 .map_or_else(|| "None".to_string(), |p| p.to_string())
5988 )
5989 .as_str(),
5990 ),
5991 order.venue_order_id(),
5992 order.account_id(),
5993 );
5994 return ModifyOutcome::Rejected;
5995 }
5996 }
5997
5998 self.generate_order_updated(order, quantity, Some(price), Some(trigger_price), None);
5999 ModifyOutcome::Applied
6000 }
6001
6002 fn update_trailing_stop_order(&self, order: &OrderAny) {
6003 let (new_trigger_price, new_price) = match trailing_stop_calculate(
6004 self.instrument.price_increment(),
6005 order.trigger_price(),
6006 order,
6007 self.core.bid,
6008 self.core.ask,
6009 self.core.last,
6010 ) {
6011 Ok(prices) => prices,
6012 Err(e) => {
6013 log::debug!("Cannot calculate trailing-stop update: {e}");
6015 return;
6016 }
6017 };
6018
6019 if new_trigger_price.is_none() && new_price.is_none() {
6020 return;
6021 }
6022
6023 self.generate_order_updated(order, order.quantity(), new_price, new_trigger_price, None);
6024 }
6025
6026 fn accept_order(&mut self, order: &mut OrderAny) {
6027 if order.is_closed() {
6028 return;
6030 }
6031
6032 if order.status() != OrderStatus::Accepted {
6033 let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
6034 let event = self.create_order_accepted(order, venue_order_id);
6035
6036 if let Err(e) = order.apply(event.clone()) {
6039 log::warn!(
6040 "Skipping local apply of accepted event for {}: {e}",
6041 order.client_order_id(),
6042 );
6043 }
6044 self.dispatch_order_event(event);
6045
6046 if matches!(
6049 order.order_type(),
6050 OrderType::TrailingStopLimit | OrderType::TrailingStopMarket
6051 ) && order.trigger_price().is_none()
6052 && self.maybe_activate_trailing_stop(
6053 order,
6054 self.core.bid,
6055 self.core.ask,
6056 self.core.last,
6057 )
6058 {
6059 self.update_trailing_stop_order(order);
6060 }
6061 }
6062
6063 let match_info = Self::matching_core_entry(order);
6064 self.track_post_match_order(order);
6065 self.core.add_order(match_info);
6066 }
6067
6068 fn track_post_match_order(&mut self, order: &OrderAny) {
6069 self.post_match_order_ids.insert(order.client_order_id());
6070 }
6071
6072 fn delete_core_order(&mut self, client_order_id: ClientOrderId) {
6073 self.post_match_order_ids.swap_remove(&client_order_id);
6074 let _ = self.core.delete_order(client_order_id);
6075 }
6076
6077 fn requires_post_match_maintenance(order: &OrderAny) -> bool {
6078 order.expire_time().is_some()
6079 || matches!(
6080 order.order_type(),
6081 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
6082 )
6083 }
6084
6085 fn matching_core_entry(order: &OrderAny) -> RestingOrder {
6086 let triggered_limit_style = matches!(
6087 order.order_type(),
6088 OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit
6089 ) && order.is_triggered().is_some_and(|triggered| triggered);
6090
6091 RestingOrder::new_with_trigger_type(
6092 order.client_order_id(),
6093 order.order_side(),
6094 order.order_type(),
6095 Some(order.trigger_type().unwrap_or(TriggerType::Default)),
6096 if triggered_limit_style {
6097 None
6098 } else {
6099 order.trigger_price()
6100 },
6101 order.price(),
6102 match order {
6103 OrderAny::TrailingStopMarket(o) => o.is_activated,
6104 OrderAny::TrailingStopLimit(o) => o.is_activated,
6105 _ => true,
6106 },
6107 )
6108 }
6109
6110 fn expire_order(&mut self, order: &OrderAny) {
6111 self.remove_queue_position(order.client_order_id());
6112
6113 if self.config.support_contingent_orders && order.contingency_type().is_some() {
6114 self.cancel_contingent_orders(order, &[]);
6115 }
6116
6117 self.generate_order_expired(order);
6118 }
6119
6120 fn cancel_order(&mut self, order: &OrderAny, cancel_contingencies: Option<bool>) {
6121 self.cancel_order_excluding(order, cancel_contingencies, &[]);
6122 }
6123
6124 fn cancel_order_excluding(
6127 &mut self,
6128 order: &OrderAny,
6129 cancel_contingencies: Option<bool>,
6130 excluded: &[ClientOrderId],
6131 ) {
6132 if self.inflight_orders.contains(order.client_order_id()) {
6133 return;
6134 }
6135
6136 let cancel_contingencies = cancel_contingencies.unwrap_or(true);
6137
6138 if order.is_active_local()
6139 && !matches!(
6140 (order.status(), order.order_type(), order.time_in_force()),
6141 (
6142 OrderStatus::Initialized | OrderStatus::Released,
6143 OrderType::Market,
6144 TimeInForce::Ioc | TimeInForce::Fok
6145 )
6146 )
6147 {
6148 log::error!(
6149 "Cannot cancel an order with {} from the matching engine",
6150 order.status()
6151 );
6152 return;
6153 }
6154
6155 if self.core.order_exists(order.client_order_id()) {
6157 self.delete_core_order(order.client_order_id());
6158 }
6159
6160 self.remove_queue_position(order.client_order_id());
6161 self.cached_filled_qty.swap_remove(&order.client_order_id());
6162
6163 let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
6164 self.generate_order_canceled(order, venue_order_id);
6165
6166 if self.config.support_contingent_orders
6167 && order.contingency_type().is_some()
6168 && cancel_contingencies
6169 {
6170 self.cancel_contingent_orders(order, excluded);
6171 }
6172 }
6173
6174 fn update_order(
6175 &mut self,
6176 order: &OrderAny,
6177 quantity: Option<Quantity>,
6178 price: Option<Price>,
6179 trigger_price: Option<Price>,
6180 update_contingencies: Option<bool>,
6181 ) -> bool {
6182 if self.inflight_orders.contains(order.client_order_id()) {
6183 return false;
6184 }
6185
6186 let update_contingencies = update_contingencies.unwrap_or(true);
6187 let quantity = quantity.unwrap_or(order.quantity());
6188
6189 let price_prec = self.instrument.price_precision();
6190 let size_prec = self.instrument.size_precision();
6191 let instrument_id = self.instrument.id();
6192
6193 if !order_precision_valid(quantity.precision, size_prec) {
6194 self.generate_order_modify_rejected(
6195 order.trader_id(),
6196 order.strategy_id(),
6197 order.instrument_id(),
6198 order.client_order_id(),
6199 Ustr::from(&format!(
6200 "Invalid update quantity precision {}, expected {size_prec} for {instrument_id}",
6201 quantity.precision
6202 )),
6203 order.venue_order_id(),
6204 order.account_id(),
6205 );
6206 return false;
6207 }
6208
6209 if let Some(px) = price
6210 && !order_precision_valid(px.precision, price_prec)
6211 {
6212 self.generate_order_modify_rejected(
6213 order.trader_id(),
6214 order.strategy_id(),
6215 order.instrument_id(),
6216 order.client_order_id(),
6217 Ustr::from(&format!(
6218 "Invalid update price precision {}, expected {price_prec} for {instrument_id}",
6219 px.precision
6220 )),
6221 order.venue_order_id(),
6222 order.account_id(),
6223 );
6224 return false;
6225 }
6226
6227 if let Some(tp) = trigger_price
6228 && !order_precision_valid(tp.precision, price_prec)
6229 {
6230 self.generate_order_modify_rejected(
6231 order.trader_id(),
6232 order.strategy_id(),
6233 order.instrument_id(),
6234 order.client_order_id(),
6235 Ustr::from(&format!(
6236 "Invalid update trigger_price precision {}, expected {price_prec} for {instrument_id}",
6237 tp.precision
6238 )),
6239 order.venue_order_id(),
6240 order.account_id(),
6241 );
6242 return false;
6243 }
6244
6245 let filled_qty = self
6247 .cached_filled_qty
6248 .get(&order.client_order_id())
6249 .copied()
6250 .unwrap_or(order.filled_qty());
6251 if quantity < filled_qty {
6252 self.generate_order_modify_rejected(
6253 order.trader_id(),
6254 order.strategy_id(),
6255 order.instrument_id(),
6256 order.client_order_id(),
6257 Ustr::from(&format!(
6258 "Cannot reduce order quantity {quantity} below filled quantity {filled_qty}",
6259 )),
6260 order.venue_order_id(),
6261 order.account_id(),
6262 );
6263 return false;
6264 }
6265
6266 let outcome = match order {
6267 OrderAny::Limit(_) | OrderAny::MarketToLimit(_) => {
6268 let price = price.unwrap_or(order.price().unwrap());
6269 self.update_limit_order(order, quantity, price)
6270 }
6271 OrderAny::StopMarket(_) => {
6272 let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
6273 self.update_stop_market_order(order, quantity, trigger_price)
6274 }
6275 OrderAny::StopLimit(_) => {
6276 let price = price.unwrap_or(order.price().unwrap());
6277 let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
6278 self.update_stop_limit_order(order, quantity, price, trigger_price)
6279 }
6280 OrderAny::MarketIfTouched(_) => {
6281 let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
6282 self.update_market_if_touched_order(order, quantity, trigger_price)
6283 }
6284 OrderAny::LimitIfTouched(_) => {
6285 let price = price.unwrap_or(order.price().unwrap());
6286 let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
6287 self.update_limit_if_touched_order(order, quantity, price, trigger_price)
6288 }
6289 OrderAny::TrailingStopMarket(_) => {
6290 if let Some(trigger_price) = trigger_price.or(order.trigger_price()) {
6291 self.update_market_if_touched_order(order, quantity, trigger_price)
6292 } else {
6293 self.generate_order_updated(order, quantity, None, trigger_price, None);
6294 ModifyOutcome::Applied
6295 }
6296 }
6297 OrderAny::TrailingStopLimit(_) => {
6298 match (
6299 price.or(order.price()),
6300 trigger_price.or(order.trigger_price()),
6301 ) {
6302 (Some(price), Some(trigger_price)) => {
6303 self.update_limit_if_touched_order(order, quantity, price, trigger_price)
6304 }
6305 _ => {
6306 self.generate_order_updated(order, quantity, price, trigger_price, None);
6307 ModifyOutcome::Applied
6308 }
6309 }
6310 }
6311 _ => {
6312 panic!(
6313 "Unsupported order type {} for update_order",
6314 order.order_type()
6315 );
6316 }
6317 };
6318
6319 if outcome == ModifyOutcome::Rejected {
6320 return false;
6321 }
6322
6323 let new_leaves_qty = quantity.saturating_sub(filled_qty);
6325 if new_leaves_qty.is_zero() {
6326 if self.config.support_contingent_orders
6327 && order.contingency_type().is_some()
6328 && update_contingencies
6329 {
6330 self.update_contingent_order(order, quantity);
6331 }
6332
6333 self.cancel_order(order, Some(false));
6335 return true;
6336 }
6337
6338 if self.config.support_contingent_orders
6339 && order.contingency_type().is_some()
6340 && update_contingencies
6341 {
6342 self.update_contingent_order(order, quantity);
6343 }
6344
6345 true
6346 }
6347
6348 pub fn trigger_stop_order(&mut self, client_order_id: ClientOrderId) {
6350 let order = match self.order_snapshot(client_order_id) {
6351 Some(order) => order,
6352 None => {
6353 log::error!(
6354 "Cannot trigger stop order: order {client_order_id} not found in cache"
6355 );
6356 return;
6357 }
6358 };
6359
6360 if order.is_closed() {
6361 log::debug!("Cannot trigger stop order: {client_order_id} already closed");
6362 return;
6363 }
6364
6365 match order.order_type() {
6366 OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit => {
6367 self.trigger_limit_style_stop_order(client_order_id, order);
6368 }
6369 OrderType::StopMarket | OrderType::MarketIfTouched | OrderType::TrailingStopMarket => {
6370 self.fill_market_order(client_order_id);
6371 }
6372 _ => {
6373 log::error!(
6374 "Cannot trigger stop order: invalid order type {}",
6375 order.order_type()
6376 );
6377 }
6378 }
6379 }
6380
6381 fn trigger_limit_style_stop_order(&mut self, client_order_id: ClientOrderId, order: OrderAny) {
6382 if order.is_triggered().is_some_and(|triggered| triggered) {
6383 let liquidity_side = match (order.price(), order.trigger_price()) {
6384 (Some(price), Some(trigger_price)) => Self::determine_triggered_limit_liquidity(
6385 order.order_side(),
6386 price,
6387 trigger_price,
6388 ),
6389 _ => LiquiditySide::Maker,
6390 };
6391
6392 if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id)
6393 && !matches!(
6394 cached_order.liquidity_side(),
6395 Some(LiquiditySide::Maker | LiquiditySide::Taker)
6396 )
6397 {
6398 cached_order.set_liquidity_side(liquidity_side);
6399 }
6400 self.fill_limit_order(client_order_id);
6401 return;
6402 }
6403
6404 let event = self.create_order_triggered(&order);
6405 let order = match self.cache.borrow_mut().update_order(&event) {
6406 Ok(order) => order,
6407 Err(e) => {
6408 log::debug!(
6409 "Failed to apply triggered event for {} before fill: {e}",
6410 order.client_order_id(),
6411 );
6412 order
6413 }
6414 };
6415 let order = self.order_snapshot(client_order_id).unwrap_or(order);
6416 self.dispatch_order_event(event);
6417
6418 let trigger_price = order
6419 .trigger_price()
6420 .expect("Limit-style stop order must have a trigger price");
6421 let price = order
6422 .price()
6423 .expect("Limit-style stop order must have a price");
6424
6425 let maker_inside = match order.order_side() {
6426 OrderSide::Buy => self
6427 .core
6428 .ask
6429 .is_some_and(|ask| trigger_price > price && price > ask),
6430 OrderSide::Sell => self
6431 .core
6432 .bid
6433 .is_some_and(|bid| trigger_price < price && price < bid),
6434 };
6435
6436 if maker_inside {
6437 if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6438 cached_order.set_liquidity_side(LiquiditySide::Maker);
6439 }
6440 self.resync_core_entry(client_order_id);
6441 self.fill_limit_order(client_order_id);
6442 return;
6443 }
6444
6445 if self.core.is_limit_matched(order.order_side(), price) {
6446 if order.is_post_only() {
6447 self.delete_core_order(client_order_id);
6448 self.cached_filled_qty.swap_remove(&client_order_id);
6449 let event = self.create_order_rejected(
6450 &order,
6451 format!(
6452 "POST_ONLY {} {} order limit px of {} would have been a TAKER: bid={}, ask={}",
6453 order.order_type(),
6454 order.order_side(),
6455 price,
6456 self.core
6457 .bid
6458 .map_or_else(|| "None".to_string(), |p| p.to_string()),
6459 self.core
6460 .ask
6461 .map_or_else(|| "None".to_string(), |p| p.to_string())
6462 )
6463 .into(),
6464 );
6465
6466 if let Err(e) = self.cache.borrow_mut().update_order(&event) {
6467 log::debug!(
6468 "Failed to apply rejected event for {} after post-only trigger: {e}",
6469 order.client_order_id(),
6470 );
6471 }
6472 self.dispatch_order_event(event);
6473 return;
6474 }
6475
6476 if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6477 cached_order.set_liquidity_side(LiquiditySide::Taker);
6478 }
6479 self.resync_core_entry(client_order_id);
6480 self.fill_limit_order(client_order_id);
6481 return;
6482 }
6483
6484 if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6485 cached_order.set_liquidity_side(Self::determine_triggered_limit_liquidity(
6486 order.order_side(),
6487 price,
6488 trigger_price,
6489 ));
6490 }
6491 self.resync_core_entry(client_order_id);
6492 }
6493
6494 fn determine_triggered_limit_liquidity(
6495 side: OrderSide,
6496 price: Price,
6497 trigger_price: Price,
6498 ) -> LiquiditySide {
6499 if (side == OrderSide::Buy && trigger_price > price)
6500 || (side == OrderSide::Sell && trigger_price < price)
6501 {
6502 LiquiditySide::Maker
6503 } else {
6504 LiquiditySide::Taker
6505 }
6506 }
6507
6508 fn update_contingent_order(&mut self, order: &OrderAny, parent_quantity: Quantity) {
6509 log::debug!(
6510 "Updating contingent orders from {}",
6511 order.client_order_id()
6512 );
6513
6514 if let Some(linked_order_ids) = order.linked_order_ids() {
6515 let parent_filled_qty = self
6516 .cached_filled_qty
6517 .get(&order.client_order_id())
6518 .copied()
6519 .unwrap_or(order.filled_qty());
6520 let parent_leaves_qty = parent_quantity.saturating_sub(parent_filled_qty);
6521
6522 for client_order_id in linked_order_ids {
6523 let child_order = match self.order_snapshot(*client_order_id) {
6524 Some(order) => order,
6525 None => panic!("Order {client_order_id} not found in cache."),
6526 };
6527
6528 if child_order.is_active_local() {
6529 continue;
6530 }
6531
6532 let child_filled_qty = self
6533 .cached_filled_qty
6534 .get(&child_order.client_order_id())
6535 .copied()
6536 .unwrap_or(child_order.filled_qty());
6537
6538 if parent_leaves_qty.is_zero() {
6539 self.cancel_order(&child_order, Some(false));
6540 } else if child_filled_qty >= parent_leaves_qty {
6541 self.cancel_order(&child_order, Some(false));
6543 } else {
6544 let child_leaves_qty = child_order.quantity().saturating_sub(child_filled_qty);
6545 if child_leaves_qty != parent_leaves_qty {
6546 let price = child_order.price();
6547 let trigger_price = child_order.trigger_price();
6548 self.update_order(
6549 &child_order,
6550 Some(parent_leaves_qty),
6551 price,
6552 trigger_price,
6553 Some(false),
6554 );
6555 }
6556 }
6557 }
6558 }
6559 }
6560
6561 fn cancel_contingent_orders(&mut self, order: &OrderAny, excluded: &[ClientOrderId]) {
6562 if let Some(linked_order_ids) = order.linked_order_ids() {
6563 for client_order_id in linked_order_ids {
6564 if excluded.contains(client_order_id) {
6565 continue;
6567 }
6568
6569 let contingent_order = match self.order_snapshot(*client_order_id) {
6570 Some(order) => order,
6571 None => panic!("Cannot find contingent order for {client_order_id}"),
6572 };
6573
6574 if contingent_order.is_active_local() {
6575 continue;
6577 }
6578
6579 if !contingent_order.is_closed() {
6580 self.cancel_order(&contingent_order, Some(false));
6581 }
6582 }
6583 }
6584 }
6585
6586 fn generate_order_submitted(&self, order: &OrderAny, account_id: AccountId) {
6587 let ts_now = self.clock.borrow().timestamp_ns();
6588 let event = OrderEventAny::Submitted(OrderSubmitted::new(
6589 order.trader_id(),
6590 order.strategy_id(),
6591 order.instrument_id(),
6592 order.client_order_id(),
6593 account_id,
6594 UUID4::new(),
6595 ts_now,
6596 ts_now,
6597 ));
6598 self.dispatch_order_event(event);
6599 }
6600
6601 fn create_order_rejected(&self, order: &OrderAny, reason: Ustr) -> OrderEventAny {
6602 let ts_now = self.clock.borrow().timestamp_ns();
6603 let account_id = order
6604 .account_id()
6605 .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
6606
6607 let due_post_only = reason.starts_with("POST_ONLY");
6608
6609 OrderEventAny::Rejected(OrderRejected::new(
6610 order.trader_id(),
6611 order.strategy_id(),
6612 order.instrument_id(),
6613 order.client_order_id(),
6614 account_id,
6615 reason,
6616 UUID4::new(),
6617 ts_now,
6618 ts_now,
6619 false,
6620 due_post_only,
6621 ))
6622 }
6623
6624 fn generate_order_rejected(&self, order: &OrderAny, reason: Ustr) {
6625 let event = self.create_order_rejected(order, reason);
6626 self.dispatch_order_event(event);
6627 }
6628
6629 fn publish_order_initialized(&self, order: &OrderAny) {
6630 let event = OrderEventAny::Initialized(order.init_event().clone());
6631 msgbus::publish_order_event(
6632 format!("events.order.{}", order.strategy_id()).into(),
6633 &event,
6634 );
6635 }
6636
6637 fn create_order_accepted(
6638 &self,
6639 order: &OrderAny,
6640 venue_order_id: VenueOrderId,
6641 ) -> OrderEventAny {
6642 let ts_now = self.clock.borrow().timestamp_ns();
6643 let account_id = order
6644 .account_id()
6645 .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
6646 OrderEventAny::Accepted(OrderAccepted::new(
6647 order.trader_id(),
6648 order.strategy_id(),
6649 order.instrument_id(),
6650 order.client_order_id(),
6651 venue_order_id,
6652 account_id,
6653 UUID4::new(),
6654 ts_now,
6655 ts_now,
6656 false,
6657 ))
6658 }
6659
6660 fn generate_order_accepted(&self, order: &OrderAny, venue_order_id: VenueOrderId) {
6661 let event = self.create_order_accepted(order, venue_order_id);
6662 self.dispatch_order_event(event);
6663 }
6664
6665 #[expect(clippy::too_many_arguments)]
6666 fn generate_order_modify_rejected(
6667 &self,
6668 trader_id: TraderId,
6669 strategy_id: StrategyId,
6670 instrument_id: InstrumentId,
6671 client_order_id: ClientOrderId,
6672 reason: Ustr,
6673 venue_order_id: Option<VenueOrderId>,
6674 account_id: Option<AccountId>,
6675 ) {
6676 let ts_now = self.clock.borrow().timestamp_ns();
6677 let event = OrderEventAny::ModifyRejected(OrderModifyRejected::new(
6678 trader_id,
6679 strategy_id,
6680 instrument_id,
6681 client_order_id,
6682 reason,
6683 UUID4::new(),
6684 ts_now,
6685 ts_now,
6686 false,
6687 venue_order_id,
6688 account_id,
6689 ));
6690 self.dispatch_order_event(event);
6691 }
6692
6693 #[expect(clippy::too_many_arguments)]
6694 fn generate_order_cancel_rejected(
6695 &self,
6696 trader_id: TraderId,
6697 strategy_id: StrategyId,
6698 account_id: AccountId,
6699 instrument_id: InstrumentId,
6700 client_order_id: ClientOrderId,
6701 venue_order_id: Option<VenueOrderId>,
6702 reason: Ustr,
6703 ) {
6704 let ts_now = self.clock.borrow().timestamp_ns();
6705 let event = OrderEventAny::CancelRejected(OrderCancelRejected::new(
6706 trader_id,
6707 strategy_id,
6708 instrument_id,
6709 client_order_id,
6710 reason,
6711 UUID4::new(),
6712 ts_now,
6713 ts_now,
6714 false,
6715 venue_order_id,
6716 Some(account_id),
6717 ));
6718 self.dispatch_order_event(event);
6719 }
6720
6721 fn generate_order_updated(
6722 &self,
6723 order: &OrderAny,
6724 quantity: Quantity,
6725 price: Option<Price>,
6726 trigger_price: Option<Price>,
6727 protection_price: Option<Price>,
6728 ) {
6729 let ts_now = self.clock.borrow().timestamp_ns();
6730 let event = OrderUpdated::new(
6731 order.trader_id(),
6732 order.strategy_id(),
6733 order.instrument_id(),
6734 order.client_order_id(),
6735 quantity,
6736 UUID4::new(),
6737 ts_now,
6738 ts_now,
6739 false,
6740 order.venue_order_id(),
6741 order.account_id(),
6742 price,
6743 trigger_price,
6744 protection_price,
6745 order.is_quote_quantity(),
6746 );
6747
6748 self.pending_order_updates
6749 .borrow_mut()
6750 .entry(order.client_order_id())
6751 .or_default()
6752 .push(event);
6753 self.dispatch_order_event(OrderEventAny::Updated(event));
6754 }
6755
6756 fn generate_order_canceled(&self, order: &OrderAny, venue_order_id: VenueOrderId) {
6757 let ts_now = self.clock.borrow().timestamp_ns();
6758 let event = OrderEventAny::Canceled(OrderCanceled::new(
6759 order.trader_id(),
6760 order.strategy_id(),
6761 order.instrument_id(),
6762 order.client_order_id(),
6763 UUID4::new(),
6764 ts_now,
6765 ts_now,
6766 false,
6767 Some(venue_order_id),
6768 order.account_id(),
6769 None,
6770 ));
6771 self.dispatch_order_event(event);
6772 }
6773
6774 fn create_order_triggered(&self, order: &OrderAny) -> OrderEventAny {
6775 let ts_now = self.clock.borrow().timestamp_ns();
6776 OrderEventAny::Triggered(OrderTriggered::new(
6777 order.trader_id(),
6778 order.strategy_id(),
6779 order.instrument_id(),
6780 order.client_order_id(),
6781 UUID4::new(),
6782 ts_now,
6783 ts_now,
6784 false,
6785 order.venue_order_id(),
6786 order.account_id(),
6787 ))
6788 }
6789
6790 fn generate_order_expired(&self, order: &OrderAny) {
6791 let ts_now = self.clock.borrow().timestamp_ns();
6792 let event = OrderEventAny::Expired(OrderExpired::new(
6793 order.trader_id(),
6794 order.strategy_id(),
6795 order.instrument_id(),
6796 order.client_order_id(),
6797 UUID4::new(),
6798 ts_now,
6799 ts_now,
6800 false,
6801 order.venue_order_id(),
6802 order.account_id(),
6803 ));
6804 self.dispatch_order_event(event);
6805 }
6806
6807 #[expect(clippy::too_many_arguments)]
6808 fn generate_order_filled(
6809 &mut self,
6810 order: &OrderAny,
6811 venue_order_id: VenueOrderId,
6812 venue_position_id: Option<PositionId>,
6813 last_qty: Quantity,
6814 last_px: Price,
6815 quote_currency: Currency,
6816 commission: Money,
6817 liquidity_side: LiquiditySide,
6818 ) {
6819 debug_assert!(
6820 last_qty <= order.quantity(),
6821 "Fill quantity {last_qty} exceeds order quantity {order_qty} for {client_order_id}",
6822 order_qty = order.quantity(),
6823 client_order_id = order.client_order_id()
6824 );
6825
6826 let ts_now = self.clock.borrow().timestamp_ns();
6827 let account_id = order
6828 .account_id()
6829 .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
6830 let fill = OrderFilled::new(
6831 order.trader_id(),
6832 order.strategy_id(),
6833 order.instrument_id(),
6834 order.client_order_id(),
6835 venue_order_id,
6836 account_id,
6837 self.ids_generator.generate_trade_id(ts_now),
6838 order.order_side(),
6839 order.order_type(),
6840 last_qty,
6841 last_px,
6842 quote_currency,
6843 liquidity_side,
6844 UUID4::new(),
6845 ts_now,
6846 ts_now,
6847 false,
6848 venue_position_id,
6849 Some(commission),
6850 None,
6851 );
6852
6853 self.record_pending_fill(&fill);
6854 self.dispatch_order_event(OrderEventAny::Filled(fill));
6855 }
6856
6857 fn record_pending_fill(&mut self, fill: &OrderFilled) {
6858 if !self.config.use_reduce_only || self.instrument.is_spread() {
6859 return;
6860 }
6861 self.purge_applied_fills();
6862 let cache = self.cache.borrow();
6863 let position_id = cache
6864 .position_id(&fill.client_order_id)
6865 .copied()
6866 .or(fill.position_id)
6867 .or_else(|| {
6868 (self.oms_type == OmsType::Netting).then(|| {
6869 PositionId::new(format!("{}-{}", fill.instrument_id, fill.strategy_id))
6870 })
6871 });
6872 let opening_trade_id = position_id.and_then(|id| {
6873 cache
6874 .position(&id)
6875 .and_then(|position| position.events.first().map(|event| event.trade_id))
6876 });
6877 let mut quantity_change = if fill.order_side == OrderSide::Buy {
6878 fill.last_qty.as_decimal()
6879 } else {
6880 -fill.last_qty.as_decimal()
6881 };
6882
6883 if matches!(self.instrument, InstrumentAny::CurrencyPair(_))
6884 && let Some(commission) = fill.commission
6885 && Some(commission.currency) == self.instrument.base_currency()
6886 {
6887 quantity_change -= commission.as_decimal();
6888 }
6889 self.pending_fills.insert(
6890 fill.trade_id,
6891 PendingFill {
6892 client_order_id: fill.client_order_id,
6893 position_id,
6894 opening_trade_id,
6895 quantity_change,
6896 },
6897 );
6898 }
6899}
6900
6901#[derive(Debug)]
6902struct PendingFill {
6903 client_order_id: ClientOrderId,
6904 position_id: Option<PositionId>,
6905 opening_trade_id: Option<TradeId>,
6906 quantity_change: Decimal,
6907}
6908
6909#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6910enum ModifyOutcome {
6911 Applied,
6912 Rejected,
6913}
6914
6915#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6916enum OrderMatchMode {
6917 All,
6918 LastPriceStopTriggers,
6919}
6920
6921#[derive(Debug)]
6922enum PostMatchOrderAction {
6923 RemoveClosed,
6924 Expire(OrderAny),
6925 UpdateTrailing(OrderAny),
6926 NoMaintenance,
6927}
6928
6929fn order_precision_valid(actual: u8, expected: u8) -> bool {
6930 actual <= expected && raw_scales_match(actual, expected)
6931}
6932
6933fn post_match_order_action<F>(
6934 order: &OrderAny,
6935 support_gtd_orders: bool,
6936 timestamp_ns: UnixNanos,
6937 clone_order: F,
6938) -> PostMatchOrderAction
6939where
6940 F: FnOnce(&OrderAny) -> OrderAny,
6941{
6942 if order.is_closed() {
6943 PostMatchOrderAction::RemoveClosed
6944 } else if support_gtd_orders
6945 && order
6946 .expire_time()
6947 .is_some_and(|expire_ns| timestamp_ns >= expire_ns)
6948 {
6949 PostMatchOrderAction::Expire(clone_order(order))
6950 } else if matches!(
6951 order.order_type(),
6952 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
6953 ) {
6954 PostMatchOrderAction::UpdateTrailing(clone_order(order))
6955 } else {
6956 PostMatchOrderAction::NoMaintenance
6957 }
6958}
6959
6960fn write_filled_qty(order: &mut OrderAny, filled_qty: Quantity) {
6965 match order {
6966 OrderAny::Limit(o) => o.filled_qty = filled_qty,
6967 OrderAny::LimitIfTouched(o) => o.filled_qty = filled_qty,
6968 OrderAny::Market(o) => o.filled_qty = filled_qty,
6969 OrderAny::MarketIfTouched(o) => o.filled_qty = filled_qty,
6970 OrderAny::MarketToLimit(o) => o.filled_qty = filled_qty,
6971 OrderAny::StopLimit(o) => o.filled_qty = filled_qty,
6972 OrderAny::StopMarket(o) => o.filled_qty = filled_qty,
6973 OrderAny::TrailingStopLimit(o) => o.filled_qty = filled_qty,
6974 OrderAny::TrailingStopMarket(o) => o.filled_qty = filled_qty,
6975 }
6976}
6977
6978#[derive(Debug, Clone, Copy)]
6979struct BarTickSizes {
6980 open: Quantity,
6981 high: Quantity,
6982 low: Quantity,
6983 close: Quantity,
6984}
6985
6986impl BarTickSizes {
6987 fn from_volume(volume: Quantity, size_increment: Quantity) -> Self {
6988 let precision_diff = FIXED_PRECISION.saturating_sub(volume.precision);
6989 let scale = QuantityRaw::pow(10, u32::from(precision_diff));
6990 let units = volume.raw() / scale;
6991 let increment_units = (size_increment.raw() / scale).max(1);
6992 let rounded_units = (units / increment_units) * increment_units;
6993 let increments = rounded_units / increment_units;
6994 let zero = Quantity::zero(volume.precision);
6995 let size =
6996 |increments| Quantity::from_raw(increments * increment_units * scale, volume.precision);
6997
6998 match increments {
6999 0 => Self {
7000 open: zero,
7001 high: zero,
7002 low: zero,
7003 close: zero,
7004 },
7005
7006 1 => Self {
7008 open: zero,
7009 high: zero,
7010 low: zero,
7011 close: size(1),
7012 },
7013 2 => Self {
7014 open: zero,
7015 high: size(1),
7016 low: size(1),
7017 close: zero,
7018 },
7019 3 => {
7020 let path_size = size(1);
7021
7022 Self {
7023 open: path_size,
7024 high: path_size,
7025 low: path_size,
7026 close: zero,
7027 }
7028 }
7029 _ => {
7030 let path_increments = increments / 4;
7031 let close_increments = increments - (path_increments * 3);
7032 let path_size = size(path_increments);
7033
7034 Self {
7035 open: path_size,
7036 high: path_size,
7037 low: path_size,
7038 close: size(close_increments),
7039 }
7040 }
7041 }
7042 }
7043}
7044
7045#[cfg(test)]
7046mod tests {
7047 use std::{
7048 cell::{Cell, RefCell},
7049 collections::{HashMap, HashSet},
7050 rc::Rc,
7051 };
7052
7053 use nautilus_common::{
7054 cache::Cache,
7055 clock::VirtualClock,
7056 messages::execution::{CancelAllOrders, ModifyOrder},
7057 };
7058 use nautilus_core::{UUID4, UnixNanos, correctness::CorrectnessError};
7059 #[cfg(feature = "high-precision")]
7060 use nautilus_model::orderbook::BookLevel;
7061 use nautilus_model::{
7062 data::{
7063 Bar, BarType, DEPTH10_LEN, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick,
7064 TradeTick,
7065 option_chain::OptionGreeks,
7066 order::{BookOrder, OrderId},
7067 },
7068 enums::{
7069 AccountType, AggressorSide, BookAction, BookType, ContingencyType, LiquiditySide,
7070 OmsType, OrderSide, OrderStatus, OrderType, PositionSide, RecordFlag, TimeInForce,
7071 TrailingOffsetType, TriggerType,
7072 },
7073 events::OrderEventAny,
7074 identifiers::{AccountId, ClientOrderId, StrategyId, TradeId, TraderId, VenueOrderId},
7075 instruments::{
7076 Instrument, InstrumentAny,
7077 stubs::{crypto_option_btc_deribit, crypto_perpetual_ethusdt, futures_contract_es},
7078 },
7079 orderbook::OrderBook,
7080 orders::{Order, OrderAny, OrderTestBuilder, stubs::TestOrderEventStubs},
7081 types::{Money, Price, Quantity, fixed::FIXED_PRECISION, quantity::QuantityRaw},
7082 };
7083 use proptest::prelude::*;
7084 use rstest::rstest;
7085 use rust_decimal::Decimal;
7086
7087 use super::{
7088 BarTickSizes, OrderFilled, OrderMatchingEngine, Position, PositionId, PostMatchOrderAction,
7089 order_precision_valid, post_match_order_action,
7090 };
7091 use crate::{
7092 matching_engine::config::OrderMatchingEngineConfig,
7093 models::{
7094 fee::{FeeModel, FeeModelAny, FeeModelHandle},
7095 fill::{FillModel, FillModelHandle},
7096 },
7097 };
7098
7099 fn assert_valid_bar_tick_sizes(volume: Quantity, size_increment: Quantity) {
7100 let sizes = BarTickSizes::from_volume(volume, size_increment);
7101 let total_raw = sizes.open.raw() + sizes.high.raw() + sizes.low.raw() + sizes.close.raw();
7102 assert!(total_raw <= volume.raw());
7103
7104 for quantity in [sizes.open, sizes.high, sizes.low, sizes.close] {
7105 assert_eq!(quantity.precision, volume.precision);
7106 assert!(
7107 OrderMatchingEngine::quantity_matches_precision(quantity, volume.precision),
7108 "bar tick quantity {quantity} not aligned to precision {}",
7109 volume.precision,
7110 );
7111 assert!(
7112 size_increment.is_zero() || quantity.raw().is_multiple_of(size_increment.raw()),
7113 "bar tick quantity {quantity} not aligned to increment {size_increment}",
7114 );
7115 }
7116
7117 if size_increment.is_positive() {
7118 assert!(
7119 volume.raw() - total_raw < size_increment.raw(),
7120 "bar tick split left {} raw units from volume {volume} and increment {size_increment}",
7121 volume.raw() - total_raw,
7122 );
7123 }
7124 }
7125
7126 #[rstest]
7127 #[case::lower(0, FIXED_PRECISION, true)]
7128 #[case::equal(FIXED_PRECISION, FIXED_PRECISION, true)]
7129 #[case::excess(3, 2, false)]
7130 #[case::native_equal(18, 18, true)]
7131 #[case::native_lower(17, 18, false)]
7132 #[case::native_shared_lower(FIXED_PRECISION, 18, false)]
7133 fn test_order_precision_valid(
7134 #[case] actual: u8,
7135 #[case] expected: u8,
7136 #[case] accepted: bool,
7137 ) {
7138 assert_eq!(order_precision_valid(actual, expected), accepted);
7139 }
7140
7141 #[rstest]
7142 #[case("100.009", "100.011", "100.000", true)]
7143 #[case("100.009", "100.020", "100.008", false)]
7144 #[case("100.010", "100.020", "100.000", false)]
7145 fn test_bar_high_first_preserves_stored_distances(
7146 #[case] open: &str,
7147 #[case] high: &str,
7148 #[case] low: &str,
7149 #[case] expected: bool,
7150 ) {
7151 let (mut engine, _, _) = collision_engine();
7152 engine.config.bar_adaptive_high_low_ordering = true;
7153 let mut prices = [Price::from(open), Price::from(high), Price::from(low)];
7154 for price in &mut prices {
7155 price.precision = 2;
7156 }
7157
7158 let bar = Bar::new(
7159 BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
7160 prices[0],
7161 prices[1],
7162 prices[2],
7163 prices[0],
7164 Quantity::from("1.000"),
7165 1.into(),
7166 1.into(),
7167 );
7168 assert_eq!(engine.bar_high_first(&bar), expected);
7169 }
7170
7171 #[cfg(feature = "high-precision")]
7172 #[rstest]
7173 fn test_consume_trade_level_preserves_native_raw_units() {
7174 let precision = if Quantity::from_raw_checked(0, 18).is_ok() {
7175 18
7176 } else {
7177 FIXED_PRECISION
7178 };
7179
7180 let size = Quantity::from_raw(2_000_000_000_000_000_000, precision);
7181 let level = BookLevel::from_order(BookOrder::new(
7182 OrderSide::Sell,
7183 Price::from("1.00"),
7184 size,
7185 1,
7186 ));
7187 let mut consumption = indexmap::IndexMap::default();
7188 let mut remaining = size.raw();
7189 OrderMatchingEngine::consume_trade_level(&mut consumption, &mut remaining, &level);
7190 assert_eq!(remaining, 0);
7191 assert_eq!(
7192 consumption[&level.price.value.raw()],
7193 (size.raw(), size.raw())
7194 );
7195 }
7196
7197 #[rstest]
7198 fn test_post_match_order_action_does_not_clone_no_maintenance_order() {
7199 let order = post_match_limit_order();
7200 let clone_count = Cell::new(0);
7201
7202 let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
7203 clone_count.set(clone_count.get() + 1);
7204 order.clone()
7205 });
7206
7207 assert!(matches!(action, PostMatchOrderAction::NoMaintenance));
7208 assert_eq!(clone_count.get(), 0);
7209 }
7210
7211 #[rstest]
7212 #[case::spread(
7213 InstrumentAny::FuturesSpread(nautilus_model::instruments::stubs::futures_spread_es()),
7214 0
7215 )]
7216 #[case::outright(InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt()), 1)]
7217 fn test_pending_fills_exclude_instruments_without_positions(
7218 #[case] instrument: InstrumentAny,
7219 #[case] expected_pending: usize,
7220 ) {
7221 let cache = Rc::new(RefCell::new(Cache::default()));
7222 let mut engine = OrderMatchingEngine::new(
7223 instrument.clone(),
7224 1,
7225 FillModelHandle::default(),
7226 FeeModelAny::default().into(),
7227 BookType::L1_MBP,
7228 OmsType::Netting,
7229 AccountType::Margin,
7230 Rc::new(RefCell::new(VirtualClock::new())),
7231 cache.clone(),
7232 Default::default(),
7233 );
7234 let (order, fill) = pending_position_fill(
7235 &instrument,
7236 PositionId::from("POSITION-001"),
7237 "OPEN",
7238 OrderSide::Buy,
7239 "1",
7240 );
7241 cache
7242 .borrow_mut()
7243 .add_order(order, None, None, false)
7244 .unwrap();
7245 engine.record_pending_fill(&fill);
7246 cache
7247 .borrow_mut()
7248 .update_order(&OrderEventAny::Filled(fill))
7249 .unwrap();
7250 engine.purge_applied_fills();
7251 assert_eq!(engine.pending_fills.len(), expected_pending);
7252 }
7253
7254 #[rstest]
7255 fn test_pending_fills_wait_for_position_acknowledgement(
7256 #[values(OmsType::Netting, OmsType::Hedging)] oms_type: OmsType,
7257 #[values(OrderSide::Buy, OrderSide::Sell)] closing_side: OrderSide,
7258 ) {
7259 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7260 let cache = Rc::new(RefCell::new(Cache::default()));
7261 let mut engine = OrderMatchingEngine::new(
7262 instrument.clone(),
7263 1,
7264 FillModelHandle::default(),
7265 FeeModelAny::default().into(),
7266 BookType::L1_MBP,
7267 oms_type,
7268 AccountType::Margin,
7269 Rc::new(RefCell::new(VirtualClock::new())),
7270 cache.clone(),
7271 Default::default(),
7272 );
7273 let position_id = PositionId::from("POSITION-001");
7274 let opening_side = if closing_side == OrderSide::Buy {
7275 OrderSide::Sell
7276 } else {
7277 OrderSide::Buy
7278 };
7279 let (opening, opening_fill) =
7280 pending_position_fill(&instrument, position_id, "OPEN", opening_side, "0.500");
7281 let (closing, first_fill) = pending_position_fill(
7282 &instrument,
7283 position_id,
7284 "CLOSE-FIRST",
7285 closing_side,
7286 "0.400",
7287 );
7288 let (_, second_fill) = pending_position_fill(
7289 &instrument,
7290 position_id,
7291 "CLOSE-SECOND",
7292 closing_side,
7293 "0.100",
7294 );
7295 let (unrelated, unrelated_fill) = pending_position_fill(
7296 &instrument,
7297 PositionId::from("POSITION-002"),
7298 "UNRELATED",
7299 closing_side,
7300 "0.200",
7301 );
7302 let position = Position::new(&instrument, opening_fill);
7303 cache
7304 .borrow_mut()
7305 .add_order(opening, None, None, false)
7306 .unwrap();
7307 cache
7308 .borrow_mut()
7309 .add_position(&position, oms_type)
7310 .unwrap();
7311 cache
7312 .borrow_mut()
7313 .add_order(closing.clone(), Some(position_id), None, false)
7314 .unwrap();
7315 engine.record_pending_fill(&first_fill);
7316 cache
7317 .borrow_mut()
7318 .add_order(unrelated, None, None, false)
7319 .unwrap();
7320 engine.record_pending_fill(&unrelated_fill);
7321 assert_eq!(
7322 engine
7323 .position_quantity_remaining(&closing, &position)
7324 .unwrap(),
7325 Quantity::from("0.100")
7326 );
7327
7328 cache
7329 .borrow_mut()
7330 .update_order(&OrderEventAny::Filled(first_fill.clone()))
7331 .unwrap();
7332 assert_eq!(
7333 engine
7334 .position_quantity_remaining(&closing, &position)
7335 .unwrap(),
7336 Quantity::from("0.100")
7337 );
7338 let position = cache
7339 .borrow_mut()
7340 .update_position_from_fill(position_id, &first_fill)
7341 .unwrap();
7342 assert_eq!(
7343 engine
7344 .position_quantity_remaining(&closing, &position)
7345 .unwrap(),
7346 Quantity::from("0.100")
7347 );
7348 assert!(!engine.pending_fills.contains_key(&first_fill.trade_id));
7349
7350 engine.record_pending_fill(&second_fill);
7351 assert_eq!(
7352 engine
7353 .position_quantity_remaining(&closing, &position)
7354 .unwrap(),
7355 Quantity::from("0.000")
7356 );
7357 engine.reset();
7358 assert!(engine.pending_fills.is_empty());
7359 assert_eq!(
7360 engine
7361 .position_quantity_remaining(&closing, &position)
7362 .unwrap(),
7363 Quantity::from("0.100")
7364 );
7365 }
7366
7367 #[rstest]
7368 #[case::base_fee("0.010 ETH", "0.89000")]
7369 #[case::quote_fee("0.010 USDT", "0.90000")]
7370 fn test_pending_spot_fills_include_base_currency_commission(
7371 #[case] commission: &str,
7372 #[case] expected: &str,
7373 ) {
7374 let instrument = InstrumentAny::CurrencyPair(
7375 nautilus_model::instruments::stubs::currency_pair_ethusdt(),
7376 );
7377 let cache = Rc::new(RefCell::new(Cache::default()));
7378 let mut engine = OrderMatchingEngine::new(
7379 instrument.clone(),
7380 1,
7381 FillModelHandle::default(),
7382 FeeModelAny::default().into(),
7383 BookType::L1_MBP,
7384 OmsType::Netting,
7385 AccountType::Cash,
7386 Rc::new(RefCell::new(VirtualClock::new())),
7387 cache.clone(),
7388 Default::default(),
7389 );
7390 let position_id = PositionId::from("POSITION-001");
7391 let (opening, opening_fill) =
7392 pending_position_fill(&instrument, position_id, "OPEN", OrderSide::Buy, "0.50000");
7393 let (_, mut increase_fill) = pending_position_fill(
7394 &instrument,
7395 position_id,
7396 "INCREASE",
7397 OrderSide::Buy,
7398 "0.40000",
7399 );
7400 let (closing, _) = pending_position_fill(
7401 &instrument,
7402 position_id,
7403 "CLOSE",
7404 OrderSide::Sell,
7405 "1.00000",
7406 );
7407 increase_fill.commission = Some(Money::from(commission));
7408 let position = Position::new(&instrument, opening_fill);
7409 cache
7410 .borrow_mut()
7411 .add_order(opening, None, None, false)
7412 .unwrap();
7413 cache
7414 .borrow_mut()
7415 .add_position(&position, OmsType::Netting)
7416 .unwrap();
7417 engine.record_pending_fill(&increase_fill);
7418 assert_eq!(
7419 engine
7420 .position_quantity_remaining(&closing, &position)
7421 .unwrap(),
7422 Quantity::from(expected)
7423 );
7424 let position = cache
7425 .borrow_mut()
7426 .update_position_from_fill(position_id, &increase_fill)
7427 .unwrap();
7428 assert_eq!(
7429 engine
7430 .position_quantity_remaining(&closing, &position)
7431 .unwrap(),
7432 Quantity::from(expected)
7433 );
7434 assert!(engine.pending_fills.is_empty());
7435 }
7436
7437 #[rstest]
7438 fn test_pending_fills_survive_position_flip_and_archive_acknowledgement() {
7439 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7440 let cache = Rc::new(RefCell::new(Cache::default()));
7441 let mut engine = OrderMatchingEngine::new(
7442 instrument.clone(),
7443 1,
7444 FillModelHandle::default(),
7445 FeeModelAny::default().into(),
7446 BookType::L1_MBP,
7447 OmsType::Netting,
7448 AccountType::Margin,
7449 Rc::new(RefCell::new(VirtualClock::new())),
7450 cache.clone(),
7451 Default::default(),
7452 );
7453 let position_id = PositionId::from("POSITION-001");
7454 let (opening, opening_fill) =
7455 pending_position_fill(&instrument, position_id, "OPEN", OrderSide::Buy, "10.000");
7456 let (flipping, flip_fill) =
7457 pending_position_fill(&instrument, position_id, "FLIP", OrderSide::Sell, "15.000");
7458 let (closing, close_fill) =
7459 pending_position_fill(&instrument, position_id, "CLOSE", OrderSide::Buy, "4.000");
7460
7461 for order in [opening, flipping, closing.clone()] {
7462 cache
7463 .borrow_mut()
7464 .add_order(order, None, None, false)
7465 .unwrap();
7466 }
7467 let mut position = Position::new(&instrument, opening_fill);
7468 cache
7469 .borrow_mut()
7470 .add_position(&position, OmsType::Netting)
7471 .unwrap();
7472 engine.record_pending_fill(&flip_fill);
7473 engine.record_pending_fill(&close_fill);
7474 assert_eq!(
7475 engine
7476 .position_quantity_remaining(&closing, &position)
7477 .unwrap(),
7478 Quantity::from("1.000")
7479 );
7480
7481 let (closing_flip, opening_flip) = flip_fill
7482 .split_for_position_flip(Quantity::from("10.000"), Some(position_id), UUID4::new())
7483 .unwrap();
7484 position.apply(&closing_flip);
7485 cache.borrow_mut().snapshot_position(&position).unwrap();
7486 let position = Position::new(&instrument, opening_flip);
7487 cache
7488 .borrow_mut()
7489 .add_position(&position, OmsType::Netting)
7490 .unwrap();
7491 assert_eq!(
7492 engine
7493 .position_quantity_remaining(&closing, &position)
7494 .unwrap(),
7495 Quantity::from("1.000")
7496 );
7497 assert!(!engine.pending_fills.contains_key(&flip_fill.trade_id));
7498 assert!(engine.pending_fills.contains_key(&close_fill.trade_id));
7499
7500 let position = cache
7501 .borrow_mut()
7502 .update_position_from_fill(position_id, &close_fill)
7503 .unwrap();
7504 assert_eq!(
7505 engine
7506 .position_quantity_remaining(&closing, &position)
7507 .unwrap(),
7508 Quantity::from("1.000")
7509 );
7510 assert!(engine.pending_fills.is_empty());
7511
7512 let (_, flatten_fill) =
7513 pending_position_fill(&instrument, position_id, "FLATTEN", OrderSide::Buy, "1.000");
7514 let (_, reopen_fill) =
7515 pending_position_fill(&instrument, position_id, "REOPEN", OrderSide::Sell, "3.000");
7516 engine.record_pending_fill(&flatten_fill);
7517 engine.record_pending_fill(&reopen_fill);
7518 cache
7519 .borrow_mut()
7520 .update_position_from_fill(position_id, &flatten_fill)
7521 .unwrap();
7522 let closed = cache.borrow().position(&position_id).unwrap().clone();
7523 cache.borrow_mut().snapshot_position(&closed).unwrap();
7524 let position = Position::new(&instrument, reopen_fill);
7525 cache
7526 .borrow_mut()
7527 .add_position_without_order(&position, OmsType::Netting)
7528 .unwrap();
7529 assert_eq!(
7530 engine
7531 .position_quantity_remaining(&closing, &position)
7532 .unwrap(),
7533 Quantity::from("3.000")
7534 );
7535 assert!(engine.pending_fills.is_empty());
7536 }
7537
7538 #[rstest]
7539 fn test_position_fills_sync_reduce_only_orders(
7540 #[values(OrderSide::Buy, OrderSide::Sell)] opening_side: OrderSide,
7541 #[values(OmsType::Netting, OmsType::Hedging)] oms_type: OmsType,
7542 #[values(false, true)] deferred: bool,
7543 #[values(OrderType::Limit, OrderType::StopMarket, OrderType::StopLimit)]
7544 resting_type: OrderType,
7545 #[values(false, true)] support_contingent_orders: bool,
7546 #[values(false, true)] indexed: bool,
7547 ) {
7548 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7549 let cache = Rc::new(RefCell::new(Cache::default()));
7550 let mut engine = OrderMatchingEngine::new(
7551 instrument.clone(),
7552 1,
7553 FillModelHandle::default(),
7554 FeeModelAny::default().into(),
7555 BookType::L2_MBP,
7556 oms_type,
7557 AccountType::Margin,
7558 Rc::new(RefCell::new(VirtualClock::new())),
7559 cache.clone(),
7560 OrderMatchingEngineConfig {
7561 support_contingent_orders,
7562 ..Default::default()
7563 },
7564 );
7565 let position_id = PositionId::from("SYNC-POSITION");
7566 let closing_side = if opening_side == OrderSide::Buy {
7567 OrderSide::Sell
7568 } else {
7569 OrderSide::Buy
7570 };
7571 let (opening, mut opening_fill) = pending_position_fill(
7572 &instrument,
7573 position_id,
7574 "SYNC-OPEN",
7575 opening_side,
7576 if support_contingent_orders {
7577 "3.000"
7578 } else {
7579 "10.000"
7580 },
7581 );
7582 let position_id = if oms_type == OmsType::Netting {
7583 PositionId::new(format!("{}-{}", instrument.id(), opening.strategy_id()))
7584 } else {
7585 position_id
7586 };
7587 opening_fill.position_id = Some(position_id);
7588 let mut position = Position::new(&instrument, opening_fill.clone());
7589 cache
7590 .borrow_mut()
7591 .add_order(opening, Some(position_id), None, false)
7592 .unwrap();
7593 cache
7594 .borrow_mut()
7595 .update_order(&OrderEventAny::Filled(opening_fill))
7596 .unwrap();
7597
7598 for (id, qty) in [("SYNC-PARENT-A", "2.000"), ("SYNC-PARENT-B", "5.000")] {
7599 let (parent, mut fill) =
7600 pending_position_fill(&instrument, position_id, id, opening_side, qty);
7601 fill.venue_order_id = VenueOrderId::from(id);
7602 if support_contingent_orders {
7603 position.apply(&fill);
7604 }
7605 cache
7606 .borrow_mut()
7607 .add_order(parent, Some(position_id), None, false)
7608 .unwrap();
7609
7610 if support_contingent_orders {
7611 cache
7612 .borrow_mut()
7613 .update_order(&OrderEventAny::Filled(fill))
7614 .unwrap();
7615 }
7616 }
7617 cache
7618 .borrow_mut()
7619 .add_position(&position, oms_type)
7620 .unwrap();
7621 engine
7622 .account_ids
7623 .insert(position.trader_id, position.account_id);
7624 let events = Rc::new(RefCell::new(Vec::new()));
7625 let events_handler = events.clone();
7626 let handler_cache = cache.clone();
7627 engine.set_event_handler(Rc::new(move |event| {
7628 if !deferred || matches!(event, OrderEventAny::Accepted(_)) {
7629 handler_cache.borrow_mut().update_order(&event).unwrap();
7630 if let OrderEventAny::Filled(fill) = &event {
7631 handler_cache
7632 .borrow_mut()
7633 .update_position_from_fill(position_id, fill)
7634 .unwrap();
7635 }
7636 }
7637 events_handler.borrow_mut().push(event);
7638 }));
7639
7640 for (id, parent, reduce_only, assigned_position) in [
7641 ("SYNC-A", Some("SYNC-PARENT-A"), true, position_id),
7642 ("SYNC-B", Some("SYNC-PARENT-B"), true, position_id),
7643 ("SYNC-STANDALONE", None, true, position_id),
7644 ("SYNC-NON-REDUCE", None, false, position_id),
7645 (
7646 "SYNC-UNRELATED",
7647 None,
7648 true,
7649 PositionId::from("OTHER-POSITION"),
7650 ),
7651 ] {
7652 let mut builder = OrderTestBuilder::new(resting_type);
7653 builder
7654 .instrument_id(instrument.id())
7655 .client_order_id(ClientOrderId::from(id))
7656 .side(closing_side)
7657 .quantity(Quantity::from("10.000"))
7658 .reduce_only(reduce_only)
7659 .submit(true);
7660
7661 if resting_type != OrderType::StopMarket {
7662 builder.price(Price::from("2000.00"));
7663 }
7664
7665 if resting_type != OrderType::Limit {
7666 builder.trigger_price(Price::from("3000.00"));
7667 }
7668
7669 if let Some(parent) = parent {
7670 builder.parent_order_id(ClientOrderId::from(parent));
7671 }
7672 let mut order = builder.build();
7673 cache
7674 .borrow_mut()
7675 .add_order(
7676 order.clone(),
7677 if !indexed && assigned_position == position_id {
7678 None
7679 } else {
7680 Some(assigned_position)
7681 },
7682 None,
7683 false,
7684 )
7685 .unwrap();
7686 engine.accept_order(&mut order);
7687 }
7688 let (closing, _) = pending_position_fill(
7689 &instrument,
7690 position_id,
7691 "SYNC-CLOSE",
7692 closing_side,
7693 "10.000",
7694 );
7695 cache
7696 .borrow_mut()
7697 .add_order(closing.clone(), Some(position_id), None, false)
7698 .unwrap();
7699 events.borrow_mut().clear();
7700
7701 for (quantity, expected_updates, expected_cancels) in [
7702 (
7703 "4.000",
7704 vec![
7705 (
7706 "SYNC-A",
7707 if support_contingent_orders {
7708 "2.000"
7709 } else {
7710 "6.000"
7711 },
7712 ),
7713 (
7714 "SYNC-B",
7715 if support_contingent_orders {
7716 "5.000"
7717 } else {
7718 "6.000"
7719 },
7720 ),
7721 ("SYNC-STANDALONE", "6.000"),
7722 ],
7723 Vec::new(),
7724 ),
7725 (
7726 "2.000",
7727 if support_contingent_orders {
7728 vec![("SYNC-B", "4.000"), ("SYNC-STANDALONE", "4.000")]
7729 } else {
7730 vec![
7731 ("SYNC-A", "4.000"),
7732 ("SYNC-B", "4.000"),
7733 ("SYNC-STANDALONE", "4.000"),
7734 ]
7735 },
7736 Vec::new(),
7737 ),
7738 (
7739 "4.000",
7740 Vec::new(),
7741 vec!["SYNC-A", "SYNC-B", "SYNC-STANDALONE"],
7742 ),
7743 ] {
7744 let start = events.borrow().len();
7745 engine
7746 .apply_fills(
7747 &closing,
7748 &[(Price::from("1000.00"), Quantity::from(quantity))],
7749 LiquiditySide::Taker,
7750 Some(position_id),
7751 Some(&position),
7752 None,
7753 )
7754 .unwrap();
7755 let events = events.borrow();
7756 let emitted = &events[start..];
7757 assert_eq!(
7758 emitted.len(),
7759 1 + expected_updates.len() + expected_cancels.len()
7760 );
7761 let OrderEventAny::Filled(fill) = &emitted[0] else {
7762 panic!("Expected closing fill first")
7763 };
7764 assert_eq!(fill.client_order_id, closing.client_order_id());
7765 assert_eq!(fill.last_qty, Quantity::from(quantity));
7766 assert_eq!(fill.last_px, Price::from("1000.00"));
7767 let mut updates = Vec::new();
7768 let mut cancels = Vec::new();
7769
7770 for event in &emitted[1..] {
7771 match event {
7772 OrderEventAny::Updated(update) => {
7773 assert_eq!(
7774 update.price,
7775 (resting_type != OrderType::StopMarket).then(|| Price::from("2000.00"))
7776 );
7777 assert_eq!(
7778 update.trigger_price,
7779 (resting_type != OrderType::Limit).then(|| Price::from("3000.00"))
7780 );
7781 updates.push((update.client_order_id.to_string(), update.quantity));
7782 }
7783 OrderEventAny::Canceled(cancel) => {
7784 cancels.push(cancel.client_order_id.to_string());
7785 }
7786 other => panic!("Unexpected event {other:?}"),
7787 }
7788 }
7789 updates.sort_by(|a, b| a.0.cmp(&b.0));
7790 cancels.sort();
7791 assert_eq!(
7792 updates,
7793 expected_updates
7794 .into_iter()
7795 .map(|(id, qty)| (id.to_string(), Quantity::from(qty)))
7796 .collect::<Vec<_>>()
7797 );
7798 assert_eq!(cancels, expected_cancels);
7799 }
7800
7801 if deferred {
7802 for event in events.borrow().iter() {
7803 cache.borrow_mut().update_order(event).unwrap();
7804 if let OrderEventAny::Filled(fill) = event {
7805 cache
7806 .borrow_mut()
7807 .update_position_from_fill(position_id, fill)
7808 .unwrap();
7809 }
7810 }
7811 }
7812 let cache = cache.borrow();
7813 assert_eq!(
7814 cache.position(&position_id).unwrap().quantity,
7815 Quantity::from("0.000")
7816 );
7817
7818 for (id, quantity) in [
7819 (
7820 "SYNC-A",
7821 if support_contingent_orders {
7822 "2.000"
7823 } else {
7824 "4.000"
7825 },
7826 ),
7827 ("SYNC-B", "4.000"),
7828 ("SYNC-STANDALONE", "4.000"),
7829 ] {
7830 let id = ClientOrderId::from(id);
7831 let order = cache.order(&id).unwrap();
7832 assert_eq!(order.status(), OrderStatus::Canceled);
7833 assert_eq!(order.quantity(), Quantity::from(quantity));
7834 assert!(!engine.order_exists(id));
7835 }
7836
7837 for id in ["SYNC-NON-REDUCE", "SYNC-UNRELATED"] {
7838 let id = ClientOrderId::from(id);
7839 let order = cache.order(&id).unwrap();
7840 assert_eq!(order.status(), OrderStatus::Accepted);
7841 assert_eq!(order.quantity(), Quantity::from("10.000"));
7842 assert!(engine.order_exists(id));
7843 }
7844 }
7845
7846 #[rstest]
7847 #[case(None, "7.000", "11.000", OrderStatus::PartiallyFilled)]
7848 #[case(None, "9.000", "9.000", OrderStatus::PartiallyFilled)]
7849 #[case(None, "10.000", "10.000", OrderStatus::Canceled)]
7850 #[case(Some("10.000"), "7.000", "10.000", OrderStatus::PartiallyFilled)]
7851 #[case(Some("9.000"), "7.000", "9.000", OrderStatus::PartiallyFilled)]
7852 #[case(Some("8.000"), "7.000", "8.000", OrderStatus::Canceled)]
7853 fn test_position_sync_accounts_for_prior_fills(
7854 #[case] parent_filled: Option<&str>,
7855 #[case] closing_quantity: &str,
7856 #[case] expected_quantity: &str,
7857 #[case] expected_status: OrderStatus,
7858 #[values(false, true)] deferred: bool,
7859 #[values(false, true)] use_reduce_only: bool,
7860 ) {
7861 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7862 let position_id = PositionId::from("FLOOR-POSITION");
7863 let cache = Rc::new(RefCell::new(Cache::default()));
7864 let mut engine = OrderMatchingEngine::new(
7865 instrument.clone(),
7866 1,
7867 FillModelHandle::default(),
7868 FeeModelAny::default().into(),
7869 BookType::L2_MBP,
7870 OmsType::Hedging,
7871 AccountType::Margin,
7872 Rc::new(RefCell::new(VirtualClock::new())),
7873 cache.clone(),
7874 OrderMatchingEngineConfig {
7875 use_reduce_only,
7876 ..Default::default()
7877 },
7878 );
7879 let opening_quantity = parent_filled.map_or(Quantity::from("18.000"), |quantity| {
7880 Quantity::from("18.000") - Quantity::from(quantity)
7881 });
7882 let (opening, opening_fill) = pending_position_fill(
7883 &instrument,
7884 position_id,
7885 "FLOOR-OPEN",
7886 OrderSide::Buy,
7887 &opening_quantity.to_string(),
7888 );
7889 let mut position = Position::new(&instrument, opening_fill.clone());
7890 engine
7891 .account_ids
7892 .insert(position.trader_id, position.account_id);
7893 cache
7894 .borrow_mut()
7895 .add_order(opening, Some(position_id), None, false)
7896 .unwrap();
7897 cache
7898 .borrow_mut()
7899 .update_order(&OrderEventAny::Filled(opening_fill))
7900 .unwrap();
7901 let parent_id = parent_filled.map(|quantity| {
7902 let (parent, mut fill) = pending_position_fill(
7903 &instrument,
7904 position_id,
7905 "FLOOR-PARENT",
7906 OrderSide::Buy,
7907 quantity,
7908 );
7909 fill.venue_order_id = VenueOrderId::from("FLOOR-PARENT");
7910 position.apply(&fill);
7911 let parent_id = parent.client_order_id();
7912 cache
7913 .borrow_mut()
7914 .add_order(parent, Some(position_id), None, false)
7915 .unwrap();
7916 cache
7917 .borrow_mut()
7918 .update_order(&OrderEventAny::Filled(fill))
7919 .unwrap();
7920 parent_id
7921 });
7922 let mut builder = OrderTestBuilder::new(OrderType::Limit);
7923 if let Some(parent_id) = parent_id {
7924 builder.parent_order_id(parent_id);
7925 }
7926 let mut resting = builder
7927 .instrument_id(instrument.id())
7928 .client_order_id(ClientOrderId::from("FLOOR-RESTING"))
7929 .side(OrderSide::Sell)
7930 .quantity(Quantity::from("10.000"))
7931 .price(Price::from("2000.00"))
7932 .reduce_only(true)
7933 .submit(true)
7934 .build();
7935 cache
7936 .borrow_mut()
7937 .add_order(resting.clone(), Some(position_id), None, false)
7938 .unwrap();
7939 let handler_cache = cache.clone();
7940 engine.set_event_handler(Rc::new(move |event| {
7941 handler_cache.borrow_mut().update_order(&event).unwrap();
7942 }));
7943 engine.accept_order(&mut resting);
7944 let (_, mut prior_fill) = pending_position_fill(
7945 &instrument,
7946 position_id,
7947 "FLOOR-RESTING",
7948 OrderSide::Sell,
7949 "8.000",
7950 );
7951 prior_fill.venue_order_id = resting.venue_order_id().unwrap();
7952 prior_fill.order_type = OrderType::Limit;
7953 position.apply(&prior_fill);
7954 cache
7955 .borrow_mut()
7956 .update_order(&OrderEventAny::Filled(prior_fill))
7957 .unwrap();
7958 cache
7959 .borrow_mut()
7960 .add_position(&position, OmsType::Hedging)
7961 .unwrap();
7962 let (closing, _) = pending_position_fill(
7963 &instrument,
7964 position_id,
7965 "FLOOR-CLOSE",
7966 OrderSide::Sell,
7967 closing_quantity,
7968 );
7969 cache
7970 .borrow_mut()
7971 .add_order(closing.clone(), Some(position_id), None, false)
7972 .unwrap();
7973 let events = Rc::new(RefCell::new(Vec::new()));
7974 let events_handler = events.clone();
7975 let handler_cache = cache.clone();
7976 engine.set_event_handler(Rc::new(move |event| {
7977 if !deferred {
7978 handler_cache.borrow_mut().update_order(&event).unwrap();
7979 if let OrderEventAny::Filled(fill) = &event {
7980 handler_cache
7981 .borrow_mut()
7982 .update_position_from_fill(position_id, fill)
7983 .unwrap();
7984 }
7985 }
7986 events_handler.borrow_mut().push(event);
7987 }));
7988
7989 engine
7990 .apply_fills(
7991 &closing,
7992 &[(Price::from("1000.00"), Quantity::from(closing_quantity))],
7993 LiquiditySide::Taker,
7994 Some(position_id),
7995 Some(&position),
7996 None,
7997 )
7998 .unwrap();
7999
8000 let expected_quantity = Quantity::from(if use_reduce_only {
8001 expected_quantity
8002 } else {
8003 "10.000"
8004 });
8005 let expected_status = if use_reduce_only {
8006 expected_status
8007 } else {
8008 OrderStatus::PartiallyFilled
8009 };
8010 let updated = expected_quantity != Quantity::from("10.000");
8011 let canceled = expected_status == OrderStatus::Canceled;
8012 let events = events.borrow();
8013 assert_eq!(
8014 events.len(),
8015 1 + usize::from(updated) + usize::from(canceled)
8016 );
8017 assert!(
8018 matches!(&events[0], OrderEventAny::Filled(fill) if fill.last_qty == Quantity::from(closing_quantity))
8019 );
8020
8021 if updated {
8022 let OrderEventAny::Updated(update) = &events[1] else {
8023 panic!("Expected remaining quantity update")
8024 };
8025 assert_eq!(update.client_order_id, resting.client_order_id());
8026 assert_eq!(update.quantity, expected_quantity);
8027 assert_eq!(update.price, Some(Price::from("2000.00")));
8028 assert_eq!(update.trigger_price, None);
8029 }
8030
8031 if canceled {
8032 let OrderEventAny::Canceled(cancel) = events.last().unwrap() else {
8033 panic!("Expected cancellation with no remaining capacity")
8034 };
8035 assert_eq!(cancel.client_order_id, resting.client_order_id());
8036 }
8037
8038 if deferred {
8039 for event in events.iter() {
8040 cache.borrow_mut().update_order(event).unwrap();
8041 if let OrderEventAny::Filled(fill) = event {
8042 cache
8043 .borrow_mut()
8044 .update_position_from_fill(position_id, fill)
8045 .unwrap();
8046 }
8047 }
8048 }
8049 let cache = cache.borrow();
8050 let resting = cache.order(&resting.client_order_id()).unwrap();
8051 assert_eq!(resting.filled_qty(), Quantity::from("8.000"));
8052 assert_eq!(resting.quantity(), expected_quantity);
8053 assert_eq!(
8054 resting.leaves_qty(),
8055 expected_quantity - Quantity::from("8.000")
8056 );
8057 assert_eq!(resting.status(), expected_status);
8058 assert_eq!(engine.order_exists(resting.client_order_id()), !canceled);
8059 assert_eq!(
8060 cache.position(&position_id).unwrap().quantity,
8061 Quantity::from("10.000") - Quantity::from(closing_quantity)
8062 );
8063 }
8064
8065 #[rstest]
8066 #[case(("0.000", "0.000"), (None, None), "open", (["6.000", "4.000"], [Some("6.000"), Some("4.000")]), false)]
8067 #[case(("2.000", "3.000"), (None, None), "open", (["8.000", "6.000"], [Some("9.000"), Some("7.000")]), false)]
8068 #[case(("2.000", "3.000"), (Some("7.000"), Some("6.000")), "open", (["7.000", "6.000"], [Some("6.000"), None]), false)]
8069 #[case(("2.000", "3.000"), (Some("2.000"), None), "open", (["2.000", "2.000"], [None, None]), true)]
8070 #[case(("2.000", "3.000"), (None, Some("3.000")), "open", (["8.000", "6.000"], [Some("3.000"), None]), true)]
8071 #[case(("0.000", "0.000"), (None, None), "closed", (["6.000", "4.000"], [None, None]), false)]
8072 #[case(("0.000", "0.000"), (None, None), "local", (["6.000", "4.000"], [None, None]), false)]
8073 #[case(("0.000", "0.000"), (None, None), "cancellation_unacknowledged", (["6.000", "4.000"], [None, None]), false)]
8074 fn test_position_sync_resizes_mixed_ouo_sibling(
8075 #[case] filled: (&str, &str),
8076 #[case] parents: (Option<&str>, Option<&str>),
8077 #[case] sibling_state: &str,
8078 #[case] expected: ([&str; 2], [Option<&str>; 2]),
8079 #[case] first_cancel: bool,
8080 #[values(0, 1, 2)] delivery: usize,
8081 #[values(false, true)] support_contingent_orders: bool,
8082 ) {
8083 let (source_filled, sibling_filled) = filled;
8084 let (source_parent, sibling_parent) = parents;
8085 let (source_quantities, sibling_updates) = expected;
8086 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8087 let position_id = PositionId::from("MIXED-POSITION");
8088 let cache = Rc::new(RefCell::new(Cache::default()));
8089 let mut engine = OrderMatchingEngine::new(
8090 instrument.clone(),
8091 1,
8092 FillModelHandle::default(),
8093 FeeModelAny::default().into(),
8094 BookType::L2_MBP,
8095 OmsType::Hedging,
8096 AccountType::Margin,
8097 Rc::new(RefCell::new(VirtualClock::new())),
8098 cache.clone(),
8099 OrderMatchingEngineConfig {
8100 support_contingent_orders,
8101 ..Default::default()
8102 },
8103 );
8104 let opening_quantity = Quantity::from("10.000")
8105 + Quantity::from(source_filled)
8106 + Quantity::from(sibling_filled);
8107 let (opening, opening_fill) = pending_position_fill(
8108 &instrument,
8109 position_id,
8110 "MIXED-OPEN",
8111 OrderSide::Buy,
8112 &opening_quantity.to_string(),
8113 );
8114 let mut position = Position::new(&instrument, opening_fill.clone());
8115 engine
8116 .account_ids
8117 .insert(position.trader_id, position.account_id);
8118 cache
8119 .borrow_mut()
8120 .add_order(opening, Some(position_id), None, false)
8121 .unwrap();
8122 cache
8123 .borrow_mut()
8124 .update_order(&OrderEventAny::Filled(opening_fill))
8125 .unwrap();
8126 let handler_cache = cache.clone();
8127 engine.set_event_handler(Rc::new(move |event| {
8128 handler_cache.borrow_mut().update_order(&event).unwrap();
8129 }));
8130
8131 for (id, sibling, reduce_only, filled, parent_quantity) in [
8132 ("MIXED-A", "MIXED-B", true, source_filled, source_parent),
8133 ("MIXED-B", "MIXED-A", false, sibling_filled, sibling_parent),
8134 ] {
8135 let mut builder = OrderTestBuilder::new(OrderType::Limit);
8136
8137 if let Some(quantity) = parent_quantity {
8138 let parent_id = format!("{id}-PARENT");
8139 let (parent, mut fill) = pending_position_fill(
8140 &instrument,
8141 position_id,
8142 &parent_id,
8143 OrderSide::Buy,
8144 quantity,
8145 );
8146 fill.venue_order_id = VenueOrderId::from(parent_id.as_str());
8147 cache
8148 .borrow_mut()
8149 .add_order(parent, Some(position_id), None, false)
8150 .unwrap();
8151 cache
8152 .borrow_mut()
8153 .update_order(&OrderEventAny::Filled(fill))
8154 .unwrap();
8155 builder.parent_order_id(ClientOrderId::from(parent_id));
8156 }
8157 let mut order = builder
8158 .instrument_id(instrument.id())
8159 .client_order_id(ClientOrderId::from(id))
8160 .side(OrderSide::Sell)
8161 .quantity(Quantity::from("10.000"))
8162 .price(Price::from("2000.00"))
8163 .reduce_only(reduce_only)
8164 .contingency_type(ContingencyType::Ouo)
8165 .linked_order_ids(vec![ClientOrderId::from(sibling)])
8166 .submit(sibling_state != "local" || reduce_only)
8167 .build();
8168 cache
8169 .borrow_mut()
8170 .add_order(order.clone(), Some(position_id), None, false)
8171 .unwrap();
8172
8173 if sibling_state != "local" || reduce_only {
8174 engine.accept_order(&mut order);
8175 }
8176
8177 if Quantity::from(filled).non_zero() {
8178 let (_, mut fill) =
8179 pending_position_fill(&instrument, position_id, id, OrderSide::Sell, filled);
8180 fill.venue_order_id = order.venue_order_id().unwrap();
8181 fill.order_type = OrderType::Limit;
8182 position.apply(&fill);
8183 cache
8184 .borrow_mut()
8185 .update_order(&OrderEventAny::Filled(fill))
8186 .unwrap();
8187 }
8188
8189 if !reduce_only && sibling_state == "closed" {
8190 engine.cancel_order(&order, Some(false));
8191 }
8192 }
8193 cache
8194 .borrow_mut()
8195 .add_position(&position, OmsType::Hedging)
8196 .unwrap();
8197 let events = Rc::new(RefCell::new(Vec::new()));
8198 let events_handler = events.clone();
8199 let handler_cache = cache.clone();
8200 engine.set_event_handler(Rc::new(move |event| {
8201 if delivery == 0 {
8202 handler_cache.borrow_mut().update_order(&event).unwrap();
8203 if let OrderEventAny::Filled(fill) = &event {
8204 handler_cache
8205 .borrow_mut()
8206 .update_position_from_fill(position_id, fill)
8207 .unwrap();
8208 }
8209 }
8210 events_handler.borrow_mut().push(event);
8211 }));
8212
8213 if sibling_state == "cancellation_unacknowledged" {
8214 let sibling = engine
8215 .order_snapshot(ClientOrderId::from("MIXED-B"))
8216 .unwrap();
8217 engine.cancel_order(&sibling, Some(false));
8218 }
8219 let (closing, _) = pending_position_fill(
8220 &instrument,
8221 position_id,
8222 "MIXED-CLOSE",
8223 OrderSide::Sell,
8224 "10.000",
8225 );
8226 cache
8227 .borrow_mut()
8228 .add_order(closing.clone(), Some(position_id), None, false)
8229 .unwrap();
8230 let mut acknowledged = 0;
8231 let mut source_quantity = Quantity::from("10.000");
8232 let mut sibling_quantity = Quantity::from("10.000");
8233 let mut source_canceled = false;
8234 let mut sibling_canceled =
8235 matches!(sibling_state, "closed" | "cancellation_unacknowledged");
8236
8237 for (step, (quantity, remaining)) in
8238 [("4.000", "6.000"), ("2.000", "4.000"), ("4.000", "0.000")]
8239 .into_iter()
8240 .enumerate()
8241 {
8242 let start = events.borrow().len();
8243 engine
8244 .apply_fills(
8245 &closing,
8246 &[(Price::from("1000.00"), Quantity::from(quantity))],
8247 LiquiditySide::Taker,
8248 Some(position_id),
8249 Some(&position),
8250 None,
8251 )
8252 .unwrap();
8253 let mut expected = vec![("fill", "MIXED-CLOSE", Quantity::from(quantity))];
8254
8255 if !source_canceled {
8256 if step < 2 {
8257 let target = if support_contingent_orders {
8258 Quantity::from(source_quantities[step])
8259 } else {
8260 Quantity::from(source_filled) + Quantity::from(remaining)
8261 };
8262
8263 if target != source_quantity {
8264 expected.push(("update", "MIXED-A", target));
8265 source_quantity = target;
8266
8267 if support_contingent_orders && source_parent == Some(source_filled) {
8268 expected.push(("cancel", "MIXED-A", Quantity::zero(3)));
8269 source_canceled = true;
8270
8271 if !sibling_canceled && sibling_state != "local" {
8272 expected.push(("cancel", "MIXED-B", Quantity::zero(3)));
8273 sibling_canceled = true;
8274 }
8275 } else if support_contingent_orders {
8276 if let Some(target) = sibling_updates[step] {
8277 sibling_quantity = Quantity::from(target);
8278 expected.push(("update", "MIXED-B", sibling_quantity));
8279 }
8280
8281 if step == 0 && first_cancel {
8282 expected.push(("cancel", "MIXED-B", Quantity::zero(3)));
8283 sibling_canceled = true;
8284 }
8285 }
8286 }
8287 } else {
8288 expected.push(("cancel", "MIXED-A", Quantity::zero(3)));
8289 source_canceled = true;
8290
8291 if support_contingent_orders && !sibling_canceled && sibling_state != "local" {
8292 expected.push(("cancel", "MIXED-B", Quantity::zero(3)));
8293 sibling_canceled = true;
8294 }
8295 }
8296 }
8297 let recorded = events.borrow();
8298 let actual: Vec<_> = recorded[start..]
8299 .iter()
8300 .map(|event| match event {
8301 OrderEventAny::Filled(fill) => {
8302 assert_eq!(fill.last_px, Price::from("1000.00"));
8303 ("fill", fill.client_order_id.as_str(), fill.last_qty)
8304 }
8305 OrderEventAny::Updated(update) => {
8306 assert_eq!(update.price, Some(Price::from("2000.00")));
8307 assert_eq!(update.trigger_price, None);
8308 ("update", update.client_order_id.as_str(), update.quantity)
8309 }
8310 OrderEventAny::Canceled(cancel) => {
8311 ("cancel", cancel.client_order_id.as_str(), Quantity::zero(3))
8312 }
8313 other => panic!("Unexpected event {other:?}"),
8314 })
8315 .collect();
8316 assert_eq!(actual, expected);
8317 drop(recorded);
8318
8319 if delivery == 2 {
8320 let end = events.borrow().len() - 1;
8321 for event in &events.borrow()[acknowledged..end] {
8322 cache.borrow_mut().update_order(event).unwrap();
8323 if let OrderEventAny::Filled(fill) = event {
8324 cache
8325 .borrow_mut()
8326 .update_position_from_fill(position_id, fill)
8327 .unwrap();
8328 }
8329 }
8330 acknowledged = end;
8331 }
8332 let before = events.borrow().len();
8333 let ids = engine.reduce_only_order_ids(position_id);
8334 engine
8335 .sync_reduce_only_orders(&closing, &position, &ids)
8336 .unwrap();
8337 assert_eq!(events.borrow().len(), before);
8338 }
8339
8340 if delivery != 0 {
8341 for event in &events.borrow()[acknowledged..] {
8342 cache.borrow_mut().update_order(event).unwrap();
8343 if let OrderEventAny::Filled(fill) = event {
8344 cache
8345 .borrow_mut()
8346 .update_position_from_fill(position_id, fill)
8347 .unwrap();
8348 }
8349 }
8350 }
8351 let cache = cache.borrow();
8352
8353 for (id, filled, quantity, canceled) in [
8354 ("MIXED-A", source_filled, source_quantity, source_canceled),
8355 (
8356 "MIXED-B",
8357 sibling_filled,
8358 sibling_quantity,
8359 sibling_canceled,
8360 ),
8361 ] {
8362 let order = cache.order(&ClientOrderId::from(id)).unwrap();
8363 assert_eq!(order.quantity(), quantity);
8364 assert_eq!(order.filled_qty(), Quantity::from(filled));
8365 assert_eq!(order.leaves_qty(), quantity - Quantity::from(filled));
8366 assert_eq!(
8367 order.status(),
8368 if canceled {
8369 OrderStatus::Canceled
8370 } else if sibling_state == "local" {
8371 OrderStatus::Initialized
8372 } else if Quantity::from(filled).is_zero() {
8373 OrderStatus::Accepted
8374 } else {
8375 OrderStatus::PartiallyFilled
8376 }
8377 );
8378 assert_eq!(
8379 engine.order_exists(order.client_order_id()),
8380 !canceled && sibling_state != "local"
8381 );
8382 }
8383 assert_eq!(
8384 cache.position(&position_id).unwrap().quantity,
8385 Quantity::from("0.000")
8386 );
8387 assert_eq!(
8388 cache.position(&position_id).unwrap().side,
8389 PositionSide::Flat
8390 );
8391 }
8392
8393 #[rstest]
8394 fn test_position_sync_does_not_resize_order_being_filled(
8395 #[values(false, true)] deferred: bool,
8396 ) {
8397 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8398 let position_id = PositionId::from("REENTRANT-POSITION");
8399 let cache = Rc::new(RefCell::new(Cache::default()));
8400 let mut engine = OrderMatchingEngine::new(
8401 instrument.clone(),
8402 1,
8403 FillModelHandle::default(),
8404 FeeModelAny::default().into(),
8405 BookType::L2_MBP,
8406 OmsType::Hedging,
8407 AccountType::Margin,
8408 Rc::new(RefCell::new(VirtualClock::new())),
8409 cache.clone(),
8410 Default::default(),
8411 );
8412 let (opening, opening_fill) = pending_position_fill(
8413 &instrument,
8414 position_id,
8415 "REENTRANT-OPEN",
8416 OrderSide::Buy,
8417 "6.000",
8418 );
8419 let position = Position::new(&instrument, opening_fill.clone());
8420 engine
8421 .account_ids
8422 .insert(position.trader_id, position.account_id);
8423 cache
8424 .borrow_mut()
8425 .add_order(opening, Some(position_id), None, false)
8426 .unwrap();
8427 cache
8428 .borrow_mut()
8429 .update_order(&OrderEventAny::Filled(opening_fill))
8430 .unwrap();
8431 cache
8432 .borrow_mut()
8433 .add_position(&position, OmsType::Hedging)
8434 .unwrap();
8435
8436 for (id, price, size) in [(1, "1000.00", "4.000"), (2, "999.00", "5.000")] {
8437 engine
8438 .process_order_book_delta(&OrderBookDelta::new(
8439 instrument.id(),
8440 BookAction::Add,
8441 BookOrder::new(OrderSide::Buy, Price::from(price), Quantity::from(size), id),
8442 0,
8443 id,
8444 UnixNanos::from(id),
8445 UnixNanos::from(id),
8446 ))
8447 .unwrap();
8448 }
8449 let events = Rc::new(RefCell::new(Vec::new()));
8450 let events_handler = events.clone();
8451 let handler_cache = cache.clone();
8452 engine.set_event_handler(Rc::new(move |event| {
8453 if !deferred || matches!(event, OrderEventAny::Accepted(_)) {
8454 handler_cache.borrow_mut().update_order(&event).unwrap();
8455 if let OrderEventAny::Filled(fill) = &event {
8456 handler_cache
8457 .borrow_mut()
8458 .update_position_from_fill(position_id, fill)
8459 .unwrap();
8460 }
8461 }
8462 events_handler.borrow_mut().push(event);
8463 }));
8464
8465 for (id, sibling, reduce_only) in [
8466 ("REENTRANT-A", "REENTRANT-B", true),
8467 ("REENTRANT-B", "REENTRANT-A", false),
8468 ] {
8469 let mut builder = OrderTestBuilder::new(OrderType::Limit);
8470 builder
8471 .instrument_id(instrument.id())
8472 .client_order_id(ClientOrderId::from(id))
8473 .side(OrderSide::Sell)
8474 .quantity(Quantity::from("10.000"))
8475 .price(Price::from(if reduce_only { "2000.00" } else { "999.00" }))
8476 .reduce_only(reduce_only)
8477 .contingency_type(ContingencyType::Ouo)
8478 .linked_order_ids(vec![ClientOrderId::from(sibling)])
8479 .submit(true);
8480 let mut order = builder.build();
8481 order.set_liquidity_side(LiquiditySide::Taker);
8482 cache
8483 .borrow_mut()
8484 .add_order(order.clone(), Some(position_id), None, false)
8485 .unwrap();
8486 engine.accept_order(&mut order);
8487 }
8488 events.borrow_mut().clear();
8489
8490 engine.iterate(UnixNanos::from(3), AggressorSide::NoAggressor);
8491
8492 if deferred {
8493 for event in events.borrow().iter() {
8494 cache.borrow_mut().update_order(event).unwrap();
8495 if let OrderEventAny::Filled(fill) = event {
8496 cache
8497 .borrow_mut()
8498 .update_position_from_fill(position_id, fill)
8499 .unwrap();
8500 }
8501 }
8502 }
8503 let cache = cache.borrow();
8504 let filled = cache.order(&ClientOrderId::from("REENTRANT-B")).unwrap();
8505 assert_eq!(filled.quantity(), Quantity::from("10.000"));
8506 assert_eq!(filled.filled_qty(), Quantity::from("9.000"));
8507 assert_eq!(filled.leaves_qty(), Quantity::from("1.000"));
8508 assert_eq!(filled.overfill_qty(), Quantity::from("0.000"));
8509 assert_eq!(filled.status(), OrderStatus::PartiallyFilled);
8510 assert_eq!(
8511 cache.position(&position_id).unwrap().quantity,
8512 Quantity::from("3.000")
8513 );
8514 assert_eq!(
8515 cache.position(&position_id).unwrap().side,
8516 PositionSide::Short
8517 );
8518 let recorded = events.borrow();
8519 let actual: Vec<_> = recorded
8520 .iter()
8521 .map(|event| match event {
8522 OrderEventAny::Filled(fill) => {
8523 ("fill", fill.client_order_id.as_str(), fill.last_qty)
8524 }
8525 OrderEventAny::Updated(update) => {
8526 ("update", update.client_order_id.as_str(), update.quantity)
8527 }
8528 OrderEventAny::Canceled(cancel) => {
8529 ("cancel", cancel.client_order_id.as_str(), Quantity::zero(3))
8530 }
8531 other => panic!("Unexpected event {other:?}"),
8532 })
8533 .collect();
8534 assert_eq!(
8535 actual,
8536 vec![
8537 ("fill", "REENTRANT-B", Quantity::from("4.000")),
8538 ("update", "REENTRANT-A", Quantity::from("6.000")),
8539 ("update", "REENTRANT-A", Quantity::from("2.000")),
8540 ("fill", "REENTRANT-B", Quantity::from("5.000")),
8541 ("update", "REENTRANT-A", Quantity::from("1.000")),
8542 ("cancel", "REENTRANT-A", Quantity::zero(3)),
8543 ]
8544 );
8545 }
8546
8547 #[rstest]
8548 #[case("5.000", "5.000", false)]
8549 #[case("10.000", "0.000", true)]
8550 fn test_position_sync_handles_unacknowledged_sibling_acceptance(
8551 #[case] closing_quantity: &str,
8552 #[case] remaining_quantity: &str,
8553 #[case] canceled: bool,
8554 #[values(false, true)] deferred: bool,
8555 ) {
8556 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8557 let position_id = PositionId::from("REENTRANT-POSITION");
8558 let cache = Rc::new(RefCell::new(Cache::default()));
8559 let mut engine = OrderMatchingEngine::new(
8560 instrument.clone(),
8561 1,
8562 FillModelHandle::default(),
8563 FeeModelAny::default().into(),
8564 BookType::L2_MBP,
8565 OmsType::Hedging,
8566 AccountType::Margin,
8567 Rc::new(RefCell::new(VirtualClock::new())),
8568 cache.clone(),
8569 Default::default(),
8570 );
8571 let (opening, opening_fill) = pending_position_fill(
8572 &instrument,
8573 position_id,
8574 "REENTRANT-OPEN",
8575 OrderSide::Buy,
8576 "10.000",
8577 );
8578 let position = Position::new(&instrument, opening_fill.clone());
8579 engine
8580 .account_ids
8581 .insert(position.trader_id, position.account_id);
8582 cache
8583 .borrow_mut()
8584 .add_order(opening, Some(position_id), None, false)
8585 .unwrap();
8586 cache
8587 .borrow_mut()
8588 .update_order(&OrderEventAny::Filled(opening_fill))
8589 .unwrap();
8590 cache
8591 .borrow_mut()
8592 .add_position(&position, OmsType::Hedging)
8593 .unwrap();
8594 let events = Rc::new(RefCell::new(Vec::new()));
8595 let events_handler = events.clone();
8596 let handler_cache = cache.clone();
8597 let sibling_id = ClientOrderId::from("ACCEPT-B");
8598 engine.set_event_handler(Rc::new(move |event| {
8599 let id = match &event {
8600 OrderEventAny::Accepted(event) => event.client_order_id,
8601 OrderEventAny::Filled(event) => event.client_order_id,
8602 OrderEventAny::Canceled(event) => event.client_order_id,
8603 OrderEventAny::Updated(event) => event.client_order_id,
8604 other => panic!("Unexpected event {other:?}"),
8605 };
8606 let applied =
8607 id != sibling_id && (!deferred || matches!(event, OrderEventAny::Accepted(_)));
8608 if applied {
8609 handler_cache.borrow_mut().update_order(&event).unwrap();
8610 if let OrderEventAny::Filled(fill) = &event {
8611 handler_cache
8612 .borrow_mut()
8613 .update_position_from_fill(position_id, fill)
8614 .unwrap();
8615 }
8616 }
8617 events_handler.borrow_mut().push((event, applied));
8618 }));
8619
8620 for (id, sibling, reduce_only) in [
8621 ("ACCEPT-A", "ACCEPT-B", true),
8622 ("ACCEPT-B", "ACCEPT-A", false),
8623 ] {
8624 let mut order = OrderTestBuilder::new(OrderType::Limit)
8625 .instrument_id(instrument.id())
8626 .client_order_id(ClientOrderId::from(id))
8627 .side(OrderSide::Sell)
8628 .quantity(Quantity::from("10.000"))
8629 .price(Price::from("2000.00"))
8630 .reduce_only(reduce_only)
8631 .contingency_type(ContingencyType::Ouo)
8632 .linked_order_ids(vec![ClientOrderId::from(sibling)])
8633 .submit(true)
8634 .build();
8635 cache
8636 .borrow_mut()
8637 .add_order(order.clone(), Some(position_id), None, false)
8638 .unwrap();
8639 engine.accept_order(&mut order);
8640 }
8641 assert_eq!(
8642 cache.borrow().order(&sibling_id).unwrap().status(),
8643 OrderStatus::Submitted
8644 );
8645 assert!(engine.order_exists(sibling_id));
8646 let (closing, _) = pending_position_fill(
8647 &instrument,
8648 position_id,
8649 "ACCEPT-CLOSE",
8650 OrderSide::Sell,
8651 closing_quantity,
8652 );
8653 cache
8654 .borrow_mut()
8655 .add_order(closing.clone(), Some(position_id), None, false)
8656 .unwrap();
8657 engine
8658 .apply_fills(
8659 &closing,
8660 &[(Price::from("1000.00"), Quantity::from(closing_quantity))],
8661 LiquiditySide::Taker,
8662 Some(position_id),
8663 Some(&position),
8664 None,
8665 )
8666 .unwrap();
8667 let ids = engine.reduce_only_order_ids(position_id);
8668 engine
8669 .sync_reduce_only_orders(&closing, &position, &ids)
8670 .unwrap();
8671 let events = events.borrow();
8672 let actual: Vec<_> = events
8673 .iter()
8674 .map(|(event, _)| match event {
8675 OrderEventAny::Accepted(event) => ("accepted", event.client_order_id.as_str()),
8676 OrderEventAny::Filled(fill) => {
8677 assert_eq!(fill.last_qty, Quantity::from(closing_quantity));
8678 assert_eq!(fill.last_px, Price::from("1000.00"));
8679 ("filled", fill.client_order_id.as_str())
8680 }
8681 OrderEventAny::Updated(event) => {
8682 assert_eq!(event.quantity, Quantity::from("5.000"));
8683 assert_eq!(event.price, Some(Price::from("2000.00")));
8684 assert_eq!(event.trigger_price, None);
8685 ("updated", event.client_order_id.as_str())
8686 }
8687 OrderEventAny::Canceled(event) => ("canceled", event.client_order_id.as_str()),
8688 other => panic!("Unexpected event {other:?}"),
8689 })
8690 .collect();
8691 let mut expected = vec![
8692 ("accepted", "ACCEPT-A"),
8693 ("accepted", "ACCEPT-B"),
8694 ("filled", "ACCEPT-CLOSE"),
8695 ];
8696
8697 if canceled {
8698 expected.extend([("canceled", "ACCEPT-A"), ("canceled", "ACCEPT-B")]);
8699 } else {
8700 expected.push(("updated", "ACCEPT-A"));
8701 }
8702 assert_eq!(actual, expected);
8703 assert_eq!(engine.order_exists(sibling_id), !canceled);
8704
8705 for (event, applied) in events.iter() {
8706 if !applied {
8707 cache.borrow_mut().update_order(event).unwrap();
8708 if let OrderEventAny::Filled(fill) = event {
8709 cache
8710 .borrow_mut()
8711 .update_position_from_fill(position_id, fill)
8712 .unwrap();
8713 }
8714 }
8715 }
8716 let cache = cache.borrow();
8717 for id in ["ACCEPT-A", "ACCEPT-B"] {
8718 let order = cache.order(&ClientOrderId::from(id)).unwrap();
8719 let quantity = Quantity::from(if !canceled && id == "ACCEPT-A" {
8720 "5.000"
8721 } else {
8722 "10.000"
8723 });
8724 assert_eq!(
8725 order.status(),
8726 if canceled {
8727 OrderStatus::Canceled
8728 } else {
8729 OrderStatus::Accepted
8730 }
8731 );
8732 assert_eq!(order.quantity(), quantity);
8733 assert_eq!(order.filled_qty(), Quantity::from("0.000"));
8734 assert_eq!(order.leaves_qty(), quantity);
8735 }
8736 assert_eq!(
8737 cache.position(&position_id).unwrap().quantity,
8738 Quantity::from(remaining_quantity)
8739 );
8740 assert_eq!(
8741 cache.position(&position_id).unwrap().side,
8742 if canceled {
8743 PositionSide::Flat
8744 } else {
8745 PositionSide::Long
8746 }
8747 );
8748 }
8749
8750 #[rstest]
8751 fn test_position_sync_mixed_ouo_does_not_match_recursively(#[values(0, 1, 2)] delivery: usize) {
8752 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8753 let position_id = PositionId::from("REENTRANT-POSITION");
8754 let cache = Rc::new(RefCell::new(Cache::default()));
8755 let mut engine = OrderMatchingEngine::new(
8756 instrument.clone(),
8757 1,
8758 FillModelHandle::default(),
8759 FeeModelAny::default().into(),
8760 BookType::L2_MBP,
8761 OmsType::Hedging,
8762 AccountType::Margin,
8763 Rc::new(RefCell::new(VirtualClock::new())),
8764 cache.clone(),
8765 Default::default(),
8766 );
8767 let (opening, opening_fill) = pending_position_fill(
8768 &instrument,
8769 position_id,
8770 "REENTRANT-OPEN",
8771 OrderSide::Buy,
8772 "10.000",
8773 );
8774 let position = Position::new(&instrument, opening_fill.clone());
8775 engine
8776 .account_ids
8777 .insert(position.trader_id, position.account_id);
8778 cache
8779 .borrow_mut()
8780 .add_order(opening, Some(position_id), None, false)
8781 .unwrap();
8782 cache
8783 .borrow_mut()
8784 .update_order(&OrderEventAny::Filled(opening_fill))
8785 .unwrap();
8786 cache
8787 .borrow_mut()
8788 .add_position(&position, OmsType::Hedging)
8789 .unwrap();
8790
8791 for (id, price, size) in [(1, "1000.00", "1.000"), (2, "999.00", "9.000")] {
8792 engine
8793 .process_order_book_delta(&OrderBookDelta::new(
8794 instrument.id(),
8795 BookAction::Add,
8796 BookOrder::new(OrderSide::Buy, Price::from(price), Quantity::from(size), id),
8797 0,
8798 id,
8799 UnixNanos::from(id),
8800 UnixNanos::from(id),
8801 ))
8802 .unwrap();
8803 }
8804 let events = Rc::new(RefCell::new(Vec::new()));
8805 let events_handler = events.clone();
8806 let handler_cache = cache.clone();
8807 engine.set_event_handler(Rc::new(move |event| {
8808 if delivery == 0 || matches!(event, OrderEventAny::Accepted(_)) {
8809 handler_cache.borrow_mut().update_order(&event).unwrap();
8810 if let OrderEventAny::Filled(fill) = &event {
8811 handler_cache
8812 .borrow_mut()
8813 .update_position_from_fill(position_id, fill)
8814 .unwrap();
8815 }
8816 }
8817 events_handler.borrow_mut().push(event);
8818 }));
8819
8820 for (id, sibling, reduce_only) in [
8821 ("REENTRANT-A", "REENTRANT-B", true),
8822 ("REENTRANT-B", "REENTRANT-A", false),
8823 ] {
8824 let mut builder = OrderTestBuilder::new(OrderType::Limit);
8825 builder
8826 .instrument_id(instrument.id())
8827 .client_order_id(ClientOrderId::from(id))
8828 .side(OrderSide::Sell)
8829 .quantity(Quantity::from("10.000"))
8830 .price(Price::from("999.00"))
8831 .reduce_only(reduce_only)
8832 .contingency_type(ContingencyType::Ouo)
8833 .linked_order_ids(vec![ClientOrderId::from(sibling)])
8834 .submit(true);
8835 let mut order = builder.build();
8836 order.set_liquidity_side(LiquiditySide::Taker);
8837 cache
8838 .borrow_mut()
8839 .add_order(order.clone(), Some(position_id), None, false)
8840 .unwrap();
8841 engine.accept_order(&mut order);
8842 }
8843 events.borrow_mut().clear();
8844
8845 let (mut closing, _) = pending_position_fill(
8846 &instrument,
8847 position_id,
8848 "REENTRANT-CLOSE",
8849 OrderSide::Sell,
8850 "4.000",
8851 );
8852 cache
8853 .borrow_mut()
8854 .add_order(closing.clone(), Some(position_id), None, false)
8855 .unwrap();
8856 engine.process_order(&mut closing, position.account_id);
8857 let mut acknowledged = 0;
8858
8859 if delivery == 2 {
8860 for event in &events.borrow()[..5] {
8861 cache.borrow_mut().update_order(event).unwrap();
8862 if let OrderEventAny::Filled(fill) = event {
8863 cache
8864 .borrow_mut()
8865 .update_position_from_fill(position_id, fill)
8866 .unwrap();
8867 }
8868 }
8869 acknowledged = 5;
8870 }
8871 assert_eq!(
8872 engine
8873 .position_quantity_remaining(
8874 &closing,
8875 &cache.borrow().position(&position_id).unwrap()
8876 )
8877 .unwrap(),
8878 Quantity::from("6.000")
8879 );
8880
8881 for id in ["REENTRANT-A", "REENTRANT-B"] {
8882 let order = engine.order_snapshot(ClientOrderId::from(id)).unwrap();
8883 assert_eq!(order.quantity(), Quantity::from("6.000"));
8884 assert_eq!(order.filled_qty(), Quantity::from("0.000"));
8885 assert_eq!(order.leaves_qty(), Quantity::from("6.000"));
8886 }
8887 let (mut flattening, _) = pending_position_fill(
8888 &instrument,
8889 position_id,
8890 "REENTRANT-FLAT",
8891 OrderSide::Sell,
8892 "6.000",
8893 );
8894 cache
8895 .borrow_mut()
8896 .add_order(flattening.clone(), Some(position_id), None, false)
8897 .unwrap();
8898 engine.process_order(&mut flattening, position.account_id);
8899 let recorded = events.borrow();
8900 let actual: Vec<_> = recorded
8901 .iter()
8902 .map(|event| match event {
8903 OrderEventAny::Filled(fill) => (
8904 "fill",
8905 fill.client_order_id.as_str(),
8906 fill.last_qty,
8907 Some(fill.last_px),
8908 ),
8909 OrderEventAny::Updated(update) => {
8910 assert_eq!(update.trigger_price, None);
8911 (
8912 "update",
8913 update.client_order_id.as_str(),
8914 update.quantity,
8915 update.price,
8916 )
8917 }
8918 OrderEventAny::Canceled(cancel) => (
8919 "cancel",
8920 cancel.client_order_id.as_str(),
8921 Quantity::zero(3),
8922 None,
8923 ),
8924 other => panic!("Unexpected event {other:?}"),
8925 })
8926 .collect();
8927 assert_eq!(
8928 actual,
8929 vec![
8930 (
8931 "fill",
8932 "REENTRANT-CLOSE",
8933 Quantity::from("1.000"),
8934 Some(Price::from("1000.00"))
8935 ),
8936 (
8937 "update",
8938 "REENTRANT-A",
8939 Quantity::from("9.000"),
8940 Some(Price::from("999.00"))
8941 ),
8942 (
8943 "update",
8944 "REENTRANT-B",
8945 Quantity::from("9.000"),
8946 Some(Price::from("999.00"))
8947 ),
8948 (
8949 "fill",
8950 "REENTRANT-CLOSE",
8951 Quantity::from("3.000"),
8952 Some(Price::from("999.00"))
8953 ),
8954 (
8955 "update",
8956 "REENTRANT-A",
8957 Quantity::from("6.000"),
8958 Some(Price::from("999.00"))
8959 ),
8960 (
8961 "update",
8962 "REENTRANT-B",
8963 Quantity::from("6.000"),
8964 Some(Price::from("999.00"))
8965 ),
8966 (
8967 "fill",
8968 "REENTRANT-FLAT",
8969 Quantity::from("1.000"),
8970 Some(Price::from("1000.00"))
8971 ),
8972 (
8973 "update",
8974 "REENTRANT-A",
8975 Quantity::from("5.000"),
8976 Some(Price::from("999.00"))
8977 ),
8978 (
8979 "update",
8980 "REENTRANT-B",
8981 Quantity::from("5.000"),
8982 Some(Price::from("999.00"))
8983 ),
8984 (
8985 "fill",
8986 "REENTRANT-FLAT",
8987 Quantity::from("5.000"),
8988 Some(Price::from("999.00"))
8989 ),
8990 ("cancel", "REENTRANT-A", Quantity::zero(3), None),
8991 ("cancel", "REENTRANT-B", Quantity::zero(3), None),
8992 ]
8993 );
8994
8995 if delivery != 0 {
8996 for event in &recorded[acknowledged..] {
8997 cache.borrow_mut().update_order(event).unwrap();
8998 if let OrderEventAny::Filled(fill) = event {
8999 cache
9000 .borrow_mut()
9001 .update_position_from_fill(position_id, fill)
9002 .unwrap();
9003 }
9004 }
9005 }
9006 let cache = cache.borrow();
9007 assert_eq!(
9008 cache.position(&position_id).unwrap().quantity,
9009 Quantity::from("0.000")
9010 );
9011 assert_eq!(
9012 cache.position(&position_id).unwrap().side,
9013 PositionSide::Flat
9014 );
9015
9016 for id in ["REENTRANT-A", "REENTRANT-B"] {
9017 let order = cache.order(&ClientOrderId::from(id)).unwrap();
9018 assert_eq!(order.status(), OrderStatus::Canceled);
9019 assert_eq!(order.quantity(), Quantity::from("5.000"));
9020 assert_eq!(order.filled_qty(), Quantity::from("0.000"));
9021 assert_eq!(order.leaves_qty(), Quantity::from("5.000"));
9022 assert!(!engine.order_exists(order.client_order_id()));
9023 }
9024 }
9025
9026 #[rstest]
9027 fn test_position_sync_does_not_match_recursively(#[values(false, true)] deferred: bool) {
9028 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9029 let position_id = PositionId::from("REENTRANT-POSITION");
9030 let cache = Rc::new(RefCell::new(Cache::default()));
9031 let mut engine = OrderMatchingEngine::new(
9032 instrument.clone(),
9033 1,
9034 FillModelHandle::default(),
9035 FeeModelAny::default().into(),
9036 BookType::L2_MBP,
9037 OmsType::Hedging,
9038 AccountType::Margin,
9039 Rc::new(RefCell::new(VirtualClock::new())),
9040 cache.clone(),
9041 Default::default(),
9042 );
9043 let (opening, opening_fill) = pending_position_fill(
9044 &instrument,
9045 position_id,
9046 "REENTRANT-OPEN",
9047 OrderSide::Buy,
9048 "10.000",
9049 );
9050 let position = Position::new(&instrument, opening_fill.clone());
9051 engine
9052 .account_ids
9053 .insert(position.trader_id, position.account_id);
9054 cache
9055 .borrow_mut()
9056 .add_order(opening, Some(position_id), None, false)
9057 .unwrap();
9058 cache
9059 .borrow_mut()
9060 .update_order(&OrderEventAny::Filled(opening_fill))
9061 .unwrap();
9062 cache
9063 .borrow_mut()
9064 .add_position(&position, OmsType::Hedging)
9065 .unwrap();
9066 let (parent, mut parent_fill) = pending_position_fill(
9067 &instrument,
9068 position_id,
9069 "REENTRANT-PARENT",
9070 OrderSide::Buy,
9071 "2.000",
9072 );
9073 parent_fill.venue_order_id = VenueOrderId::from("REENTRANT-PARENT");
9074 cache
9075 .borrow_mut()
9076 .add_order(parent, Some(position_id), None, false)
9077 .unwrap();
9078 cache
9079 .borrow_mut()
9080 .update_order(&OrderEventAny::Filled(parent_fill))
9081 .unwrap();
9082
9083 for (id, price, size) in [(1, "1000.00", "1.000"), (2, "999.00", "9.000")] {
9084 engine
9085 .process_order_book_delta(&OrderBookDelta::new(
9086 instrument.id(),
9087 BookAction::Add,
9088 BookOrder::new(OrderSide::Buy, Price::from(price), Quantity::from(size), id),
9089 0,
9090 id,
9091 UnixNanos::from(id),
9092 UnixNanos::from(id),
9093 ))
9094 .unwrap();
9095 }
9096 let events = Rc::new(RefCell::new(Vec::new()));
9097 let events_handler = events.clone();
9098 let handler_cache = cache.clone();
9099 engine.set_event_handler(Rc::new(move |event| {
9100 if !deferred || matches!(event, OrderEventAny::Accepted(_)) {
9101 handler_cache.borrow_mut().update_order(&event).unwrap();
9102 if let OrderEventAny::Filled(fill) = &event {
9103 handler_cache
9104 .borrow_mut()
9105 .update_position_from_fill(position_id, fill)
9106 .unwrap();
9107 }
9108 }
9109 events_handler.borrow_mut().push(event);
9110 }));
9111
9112 for (id, quantity, parent) in [
9113 ("REENTRANT-A", "2.000", Some("REENTRANT-PARENT")),
9114 ("REENTRANT-B", "10.000", None),
9115 ] {
9116 let mut builder = OrderTestBuilder::new(OrderType::Limit);
9117 builder
9118 .instrument_id(instrument.id())
9119 .client_order_id(ClientOrderId::from(id))
9120 .side(OrderSide::Sell)
9121 .quantity(Quantity::from(quantity))
9122 .price(Price::from("999.00"))
9123 .reduce_only(true)
9124 .submit(true);
9125
9126 if let Some(parent) = parent {
9127 builder.parent_order_id(ClientOrderId::from(parent));
9128 }
9129 let mut order = builder.build();
9130 order.set_liquidity_side(LiquiditySide::Taker);
9131 cache
9132 .borrow_mut()
9133 .add_order(order.clone(), Some(position_id), None, false)
9134 .unwrap();
9135 engine.accept_order(&mut order);
9136 }
9137 events.borrow_mut().clear();
9138
9139 assert_eq!(engine.core.iterate_asks().len(), 2);
9140 assert_eq!(
9141 cache.borrow().position(&position_id).unwrap().quantity,
9142 Quantity::from("10.000")
9143 );
9144 engine.iterate(UnixNanos::from(3), AggressorSide::NoAggressor);
9145
9146 let events = events.borrow();
9147 let fills: Vec<_> = events
9148 .iter()
9149 .filter_map(|event| match event {
9150 OrderEventAny::Filled(fill) => Some((
9151 fill.client_order_id.to_string(),
9152 fill.last_qty,
9153 fill.last_px,
9154 )),
9155 _ => None,
9156 })
9157 .collect();
9158 assert_eq!(
9159 fills,
9160 vec![
9161 (
9162 "REENTRANT-A".to_string(),
9163 Quantity::from("1.000"),
9164 Price::from("1000.00")
9165 ),
9166 (
9167 "REENTRANT-A".to_string(),
9168 Quantity::from("1.000"),
9169 Price::from("999.00")
9170 ),
9171 (
9172 "REENTRANT-B".to_string(),
9173 Quantity::from("1.000"),
9174 Price::from("1000.00")
9175 ),
9176 (
9177 "REENTRANT-B".to_string(),
9178 Quantity::from("7.000"),
9179 Price::from("999.00")
9180 ),
9181 ]
9182 );
9183 assert!(
9184 !events
9185 .iter()
9186 .any(|event| matches!(event, OrderEventAny::Canceled(_)))
9187 );
9188
9189 if deferred {
9190 for event in events.iter() {
9191 cache.borrow_mut().update_order(event).unwrap();
9192 if let OrderEventAny::Filled(fill) = event {
9193 cache
9194 .borrow_mut()
9195 .update_position_from_fill(position_id, fill)
9196 .unwrap();
9197 }
9198 }
9199 }
9200 let cache = cache.borrow();
9201 assert_eq!(
9202 cache.position(&position_id).unwrap().quantity,
9203 Quantity::from("0.000")
9204 );
9205
9206 for (id, quantity) in [("REENTRANT-A", "2.000"), ("REENTRANT-B", "8.000")] {
9207 let order = cache.order(&ClientOrderId::from(id)).unwrap();
9208 assert_eq!(order.status(), OrderStatus::Filled);
9209 assert_eq!(order.quantity(), Quantity::from(quantity));
9210 assert_eq!(order.filled_qty(), Quantity::from(quantity));
9211 }
9212 }
9213
9214 #[rstest]
9215 fn test_position_sync_includes_newly_activated_oto_child(
9216 #[values(false, true)] deferred: bool,
9217 ) {
9218 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9219 let position_id = PositionId::from("ACTIVATION-POSITION");
9220 let cache = Rc::new(RefCell::new(Cache::default()));
9221 let mut engine = OrderMatchingEngine::new(
9222 instrument.clone(),
9223 1,
9224 FillModelHandle::default(),
9225 FeeModelAny::default().into(),
9226 BookType::L2_MBP,
9227 OmsType::Hedging,
9228 AccountType::Margin,
9229 Rc::new(RefCell::new(VirtualClock::new())),
9230 cache.clone(),
9231 Default::default(),
9232 );
9233 let (opening, opening_fill) = pending_position_fill(
9234 &instrument,
9235 position_id,
9236 "ACTIVATION-OPEN",
9237 OrderSide::Buy,
9238 "10.000",
9239 );
9240 let position = Position::new(&instrument, opening_fill.clone());
9241 engine
9242 .account_ids
9243 .insert(position.trader_id, position.account_id);
9244 cache
9245 .borrow_mut()
9246 .add_order(opening, Some(position_id), None, false)
9247 .unwrap();
9248 cache
9249 .borrow_mut()
9250 .update_order(&OrderEventAny::Filled(opening_fill))
9251 .unwrap();
9252 cache
9253 .borrow_mut()
9254 .add_position(&position, OmsType::Hedging)
9255 .unwrap();
9256 let parent_id = ClientOrderId::from("ACTIVATION-PARENT");
9257 let child_id = ClientOrderId::from("ACTIVATION-CHILD");
9258 let parent = OrderTestBuilder::new(OrderType::Market)
9259 .instrument_id(instrument.id())
9260 .client_order_id(parent_id)
9261 .side(OrderSide::Buy)
9262 .quantity(Quantity::from("10.000"))
9263 .contingency_type(ContingencyType::Oto)
9264 .linked_order_ids(vec![child_id])
9265 .submit(true)
9266 .build();
9267 let child = OrderTestBuilder::new(OrderType::Limit)
9268 .instrument_id(instrument.id())
9269 .client_order_id(child_id)
9270 .side(OrderSide::Sell)
9271 .quantity(Quantity::from("10.000"))
9272 .price(Price::from("2000.00"))
9273 .reduce_only(true)
9274 .parent_order_id(parent_id)
9275 .submit(true)
9276 .build();
9277
9278 for order in [parent.clone(), child] {
9279 cache
9280 .borrow_mut()
9281 .add_order(order, Some(position_id), None, false)
9282 .unwrap();
9283 }
9284 let events = Rc::new(RefCell::new(Vec::new()));
9285 let events_handler = events.clone();
9286 let handler_cache = cache.clone();
9287 engine.set_event_handler(Rc::new(move |event| {
9288 if !deferred || matches!(event, OrderEventAny::Accepted(_)) {
9289 handler_cache.borrow_mut().update_order(&event).unwrap();
9290 if let OrderEventAny::Filled(fill) = &event {
9291 handler_cache
9292 .borrow_mut()
9293 .update_position_from_fill(position_id, fill)
9294 .unwrap();
9295 }
9296 }
9297 events_handler.borrow_mut().push(event);
9298 }));
9299 assert!(!engine.order_exists(child_id));
9300
9301 engine
9302 .apply_fills(
9303 &parent,
9304 &[(Price::from("1000.00"), Quantity::from("2.000"))],
9305 LiquiditySide::Taker,
9306 Some(position_id),
9307 Some(&position),
9308 None,
9309 )
9310 .unwrap();
9311
9312 let events = events.borrow();
9313 assert_eq!(events.len(), 3);
9314 assert!(
9315 matches!(&events[0], OrderEventAny::Filled(fill) if fill.client_order_id == parent_id && fill.last_qty == Quantity::from("2.000"))
9316 );
9317 assert!(
9318 matches!(&events[1], OrderEventAny::Accepted(accepted) if accepted.client_order_id == child_id)
9319 );
9320 let OrderEventAny::Updated(update) = &events[2] else {
9321 panic!("Expected child quantity update")
9322 };
9323 assert_eq!(update.client_order_id, child_id);
9324 assert_eq!(update.quantity, Quantity::from("2.000"));
9325 assert_eq!(update.price, Some(Price::from("2000.00")));
9326 assert_eq!(update.trigger_price, None);
9327 assert!(engine.order_exists(child_id));
9328 assert_eq!(
9329 engine.order_snapshot(child_id).unwrap().quantity(),
9330 Quantity::from("2.000")
9331 );
9332
9333 if deferred {
9334 for event in events.iter() {
9335 if matches!(event, OrderEventAny::Accepted(_)) {
9336 continue;
9337 }
9338 cache.borrow_mut().update_order(event).unwrap();
9339 if let OrderEventAny::Filled(fill) = event {
9340 cache
9341 .borrow_mut()
9342 .update_position_from_fill(position_id, fill)
9343 .unwrap();
9344 }
9345 }
9346 }
9347 let cache = cache.borrow();
9348 assert_eq!(
9349 cache.position(&position_id).unwrap().quantity,
9350 Quantity::from("12.000")
9351 );
9352 let child = cache.order(&child_id).unwrap();
9353 assert_eq!(child.status(), OrderStatus::Accepted);
9354 assert_eq!(child.quantity(), Quantity::from("2.000"));
9355 assert_eq!(child.filled_qty(), Quantity::from("0.000"));
9356 assert_eq!(child.leaves_qty(), Quantity::from("2.000"));
9357 }
9358
9359 fn pending_position_fill(
9360 instrument: &InstrumentAny,
9361 position_id: PositionId,
9362 id: &str,
9363 side: OrderSide,
9364 quantity: &str,
9365 ) -> (OrderAny, OrderFilled) {
9366 let order = OrderTestBuilder::new(OrderType::Market)
9367 .instrument_id(instrument.id())
9368 .client_order_id(ClientOrderId::from(id))
9369 .side(side)
9370 .quantity(Quantity::from(quantity))
9371 .submit(true)
9372 .build();
9373 let OrderEventAny::Filled(fill) = TestOrderEventStubs::filled(
9374 &order,
9375 instrument,
9376 Some(TradeId::from(id)),
9377 Some(position_id),
9378 Some(Price::from("1000.00")),
9379 None,
9380 None,
9381 Some(Money::zero(instrument.quote_currency())),
9382 None,
9383 None,
9384 ) else {
9385 unreachable!()
9386 };
9387 (order, fill)
9388 }
9389
9390 #[rstest]
9391 fn test_pending_modify_updates_acknowledge_individually_and_reset() {
9392 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9393 let cache = Rc::new(RefCell::new(Cache::default()));
9394 let mut engine = OrderMatchingEngine::new(
9395 instrument.clone(),
9396 1,
9397 FillModelHandle::default(),
9398 FeeModelAny::default().into(),
9399 BookType::L2_MBP,
9400 OmsType::Netting,
9401 AccountType::Margin,
9402 Rc::new(RefCell::new(VirtualClock::new())),
9403 cache.clone(),
9404 OrderMatchingEngineConfig::default(),
9405 );
9406 let mut order = OrderTestBuilder::new(OrderType::Limit)
9407 .instrument_id(instrument.id())
9408 .side(OrderSide::Buy)
9409 .quantity(Quantity::from("1.000"))
9410 .price(Price::from("99.00"))
9411 .submit(true)
9412 .build();
9413 let id = order.client_order_id();
9414 engine.set_event_handler(Rc::new(|_| {}));
9415 engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
9416 let pending = Rc::new(RefCell::new(Vec::new()));
9417 let events = pending.clone();
9418 engine.set_event_handler(Rc::new(move |event| events.borrow_mut().push(event)));
9419
9420 for (quantity, price) in [
9421 (Some(Quantity::from("2.000")), None),
9422 (None, Some(Price::from("100.00"))),
9423 ] {
9424 engine.process_modify(
9425 &ModifyOrder::new(
9426 order.trader_id(),
9427 None,
9428 order.strategy_id(),
9429 order.instrument_id(),
9430 id,
9431 None,
9432 quantity,
9433 price,
9434 None,
9435 UUID4::new(),
9436 UnixNanos::from(1),
9437 None,
9438 None,
9439 ),
9440 AccountId::from("ACCOUNT-001"),
9441 );
9442 }
9443 assert_eq!(pending.borrow().len(), 2);
9444 cache
9445 .borrow_mut()
9446 .update_order(&pending.borrow()[0])
9447 .unwrap();
9448 let snapshot = engine.order_snapshot(id).unwrap();
9449 assert_eq!(snapshot.quantity(), Quantity::from("2.000"));
9450 assert_eq!(snapshot.price(), Some(Price::from("100.00")));
9451 assert_eq!(engine.pending_order_updates.borrow()[&id].len(), 1);
9452 cache
9453 .borrow_mut()
9454 .update_order(&pending.borrow()[1])
9455 .unwrap();
9456 engine.iterate(UnixNanos::from(2), AggressorSide::NoAggressor);
9457 assert!(engine.pending_order_updates.borrow().is_empty());
9458 engine.process_modify(
9459 &ModifyOrder::new(
9460 order.trader_id(),
9461 None,
9462 order.strategy_id(),
9463 order.instrument_id(),
9464 id,
9465 None,
9466 Some(Quantity::from("3.000")),
9467 None,
9468 None,
9469 UUID4::new(),
9470 UnixNanos::from(3),
9471 None,
9472 None,
9473 ),
9474 AccountId::from("ACCOUNT-001"),
9475 );
9476 assert_eq!(
9477 engine.order_snapshot(id).unwrap().quantity(),
9478 Quantity::from("3.000")
9479 );
9480 engine.reset();
9481 assert!(engine.pending_order_updates.borrow().is_empty());
9482 assert_eq!(
9483 engine.order_snapshot(id).unwrap().quantity(),
9484 Quantity::from("2.000")
9485 );
9486 }
9487
9488 #[rstest]
9489 fn test_process_order_rejects_reduce_only_when_support_is_disabled() {
9490 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9491 let mut engine = OrderMatchingEngine::new(
9492 instrument.clone(),
9493 1,
9494 FillModelHandle::default(),
9495 FeeModelAny::default().into(),
9496 BookType::L1_MBP,
9497 OmsType::Netting,
9498 AccountType::Margin,
9499 Rc::new(RefCell::new(VirtualClock::new())),
9500 Rc::new(RefCell::new(Cache::default())),
9501 OrderMatchingEngineConfig::builder()
9502 .use_reduce_only(false)
9503 .build(),
9504 );
9505 let events = Rc::new(RefCell::new(Vec::new()));
9506 let events_handler = Rc::clone(&events);
9507 engine.set_event_handler(Rc::new(move |event| {
9508 events_handler.borrow_mut().push(event);
9509 }));
9510 let mut order = OrderTestBuilder::new(OrderType::Market)
9511 .instrument_id(instrument.id())
9512 .side(OrderSide::Sell)
9513 .quantity(Quantity::from("1.000"))
9514 .reduce_only(true)
9515 .submit(true)
9516 .build();
9517
9518 engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
9519
9520 let events = events.borrow();
9521 assert_eq!(events.len(), 1);
9522 let OrderEventAny::Rejected(rejected) = &events[0] else {
9523 panic!("Expected OrderRejected, was {:?}", events[0]);
9524 };
9525 assert_eq!(
9526 rejected.reason,
9527 "Reduce-only orders are not supported by this matching engine"
9528 );
9529 }
9530
9531 #[rstest]
9532 fn test_post_match_order_action_does_not_clone_closed_order() {
9533 let order = post_match_closed_limit_order();
9534 let clone_count = Cell::new(0);
9535
9536 let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
9537 clone_count.set(clone_count.get() + 1);
9538 order.clone()
9539 });
9540
9541 assert!(matches!(action, PostMatchOrderAction::RemoveClosed));
9542 assert_eq!(clone_count.get(), 0);
9543 }
9544
9545 #[rstest]
9546 fn test_post_match_order_action_clones_expired_gtd_order_once() {
9547 let order = post_match_gtd_limit_order();
9548 let clone_count = Cell::new(0);
9549
9550 let action = post_match_order_action(&order, true, UnixNanos::from(10_u64), |order| {
9551 clone_count.set(clone_count.get() + 1);
9552 order.clone()
9553 });
9554
9555 let PostMatchOrderAction::Expire(cloned) = action else {
9556 panic!("Expected expired action, was {action:?}");
9557 };
9558 assert_eq!(cloned.client_order_id(), order.client_order_id());
9559 assert_eq!(clone_count.get(), 1);
9560 }
9561
9562 #[rstest]
9563 fn test_post_match_order_action_clones_trailing_order_once() {
9564 let order = post_match_trailing_stop_order();
9565 let clone_count = Cell::new(0);
9566
9567 let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
9568 clone_count.set(clone_count.get() + 1);
9569 order.clone()
9570 });
9571
9572 let PostMatchOrderAction::UpdateTrailing(cloned) = action else {
9573 panic!("Expected trailing update action, was {action:?}");
9574 };
9575 assert_eq!(cloned.client_order_id(), order.client_order_id());
9576 assert_eq!(clone_count.get(), 1);
9577 }
9578
9579 fn post_match_limit_order() -> OrderAny {
9580 OrderTestBuilder::new(OrderType::Limit)
9581 .instrument_id(crypto_perpetual_ethusdt().id())
9582 .side(OrderSide::Buy)
9583 .price(Price::from("1500.00"))
9584 .quantity(Quantity::from("1.000"))
9585 .client_order_id(ClientOrderId::from("POST-MATCH-LIMIT"))
9586 .submit(true)
9587 .build()
9588 }
9589
9590 fn post_match_closed_limit_order() -> OrderAny {
9591 let account_id = AccountId::from("SIM-001");
9592 let venue_order_id = VenueOrderId::from("V-001");
9593 let mut order = post_match_limit_order();
9594 order
9595 .apply(TestOrderEventStubs::accepted(
9596 &order,
9597 account_id,
9598 venue_order_id,
9599 ))
9600 .unwrap();
9601 order
9602 .apply(TestOrderEventStubs::canceled(
9603 &order,
9604 account_id,
9605 Some(venue_order_id),
9606 ))
9607 .unwrap();
9608 order
9609 }
9610
9611 fn post_match_gtd_limit_order() -> OrderAny {
9612 OrderTestBuilder::new(OrderType::Limit)
9613 .instrument_id(crypto_perpetual_ethusdt().id())
9614 .side(OrderSide::Buy)
9615 .price(Price::from("1500.00"))
9616 .quantity(Quantity::from("1.000"))
9617 .time_in_force(TimeInForce::Gtd)
9618 .expire_time(UnixNanos::from(10_u64))
9619 .client_order_id(ClientOrderId::from("POST-MATCH-GTD"))
9620 .submit(true)
9621 .build()
9622 }
9623
9624 fn post_match_trailing_stop_order() -> OrderAny {
9625 OrderTestBuilder::new(OrderType::TrailingStopMarket)
9626 .instrument_id(crypto_perpetual_ethusdt().id())
9627 .side(OrderSide::Buy)
9628 .quantity(Quantity::from("1.000"))
9629 .trigger_price(Price::from("1510.00"))
9630 .trigger_type(TriggerType::BidAsk)
9631 .trailing_offset(Decimal::new(5, 0))
9632 .trailing_offset_type(TrailingOffsetType::Price)
9633 .client_order_id(ClientOrderId::from("POST-MATCH-TRAIL"))
9634 .submit(true)
9635 .build()
9636 }
9637
9638 #[rstest]
9639 fn test_fill_order_calculates_commission_from_fill_liquidity_side() {
9640 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9641 let cache = Rc::new(RefCell::new(Cache::default()));
9642 let clock = Rc::new(RefCell::new(VirtualClock::new()));
9643 let mut engine = OrderMatchingEngine::new(
9644 instrument.clone(),
9645 1,
9646 FillModelHandle::default(),
9647 FeeModelAny::default().into(),
9648 BookType::L1_MBP,
9649 OmsType::Netting,
9650 AccountType::Margin,
9651 clock,
9652 cache,
9653 Default::default(),
9654 );
9655 let events = Rc::new(RefCell::new(Vec::new()));
9656 let events_handler = Rc::clone(&events);
9657 engine.set_event_handler(Rc::new(move |event| {
9658 events_handler.borrow_mut().push(event);
9659 }));
9660
9661 let mut order = OrderTestBuilder::new(OrderType::Market)
9662 .instrument_id(instrument.id())
9663 .side(OrderSide::Buy)
9664 .quantity(Quantity::from("1.000"))
9665 .submit(true)
9666 .build();
9667 order.set_liquidity_side(LiquiditySide::Maker);
9668 engine
9669 .account_ids
9670 .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
9671
9672 engine
9673 .fill_order(
9674 &order,
9675 Price::from("1500.00"),
9676 Quantity::from("1.000"),
9677 LiquiditySide::Taker,
9678 None,
9679 None,
9680 )
9681 .unwrap();
9682
9683 let events = events.borrow();
9684 assert_eq!(events.len(), 1);
9685 let fill = match &events[0] {
9686 OrderEventAny::Filled(fill) => fill,
9687 event => panic!("Expected OrderFilled, was {event:?}"),
9688 };
9689 let commission = fill.commission.expect("expected commission");
9690 let expected_commission =
9691 fill.last_qty.as_decimal() * fill.last_px.as_decimal() * instrument.taker_fee();
9692
9693 assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
9694 assert_eq!(commission.currency, instrument.quote_currency());
9695 assert_eq!(commission.as_decimal(), expected_commission);
9696 }
9697
9698 #[rstest]
9699 fn test_custom_fee_model_handle_is_called_by_fill_order() {
9700 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9701 let cache = Rc::new(RefCell::new(Cache::default()));
9702 let clock = Rc::new(RefCell::new(VirtualClock::new()));
9703 let calls = Rc::new(Cell::new(0));
9704 let expected_commission = Money::from("1.23 USDT");
9705 let fee_model = FeeModelHandle::new(RecordingFeeModel {
9706 calls: Rc::clone(&calls),
9707 commission: expected_commission,
9708 });
9709 let cloned_fee_model = fee_model.clone();
9710 drop(fee_model);
9711 let mut engine = OrderMatchingEngine::new(
9712 instrument.clone(),
9713 1,
9714 FillModelHandle::default(),
9715 cloned_fee_model,
9716 BookType::L1_MBP,
9717 OmsType::Netting,
9718 AccountType::Margin,
9719 clock,
9720 cache,
9721 Default::default(),
9722 );
9723 let events = Rc::new(RefCell::new(Vec::new()));
9724 let events_handler = Rc::clone(&events);
9725 engine.set_event_handler(Rc::new(move |event| {
9726 events_handler.borrow_mut().push(event);
9727 }));
9728
9729 let order = OrderTestBuilder::new(OrderType::Market)
9730 .instrument_id(instrument.id())
9731 .side(OrderSide::Buy)
9732 .quantity(Quantity::from("1.000"))
9733 .submit(true)
9734 .build();
9735 engine
9736 .account_ids
9737 .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
9738
9739 engine
9740 .fill_order(
9741 &order,
9742 Price::from("1500.00"),
9743 Quantity::from("1.000"),
9744 LiquiditySide::Taker,
9745 None,
9746 None,
9747 )
9748 .unwrap();
9749
9750 let events = events.borrow();
9751 assert_eq!(events.len(), 1);
9752 let fill = match &events[0] {
9753 OrderEventAny::Filled(fill) => fill,
9754 event => panic!("Expected OrderFilled, was {event:?}"),
9755 };
9756
9757 assert_eq!(calls.get(), 1);
9758 assert_eq!(fill.commission, Some(expected_commission));
9759 }
9760
9761 #[rstest]
9762 fn test_fill_order_does_not_cache_filled_qty_when_fee_model_fails() {
9763 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9764 let cache = Rc::new(RefCell::new(Cache::default()));
9765 let clock = Rc::new(RefCell::new(VirtualClock::new()));
9766 let mut engine = OrderMatchingEngine::new(
9767 instrument.clone(),
9768 1,
9769 FillModelHandle::default(),
9770 FeeModelHandle::new(FailingFeeModel),
9771 BookType::L1_MBP,
9772 OmsType::Netting,
9773 AccountType::Margin,
9774 clock,
9775 cache,
9776 Default::default(),
9777 );
9778 let events = Rc::new(RefCell::new(Vec::new()));
9779 let events_handler = Rc::clone(&events);
9780 engine.set_event_handler(Rc::new(move |event| {
9781 events_handler.borrow_mut().push(event);
9782 }));
9783
9784 let order = OrderTestBuilder::new(OrderType::Market)
9785 .instrument_id(instrument.id())
9786 .side(OrderSide::Buy)
9787 .quantity(Quantity::from("1.000"))
9788 .submit(true)
9789 .build();
9790 engine
9791 .account_ids
9792 .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
9793
9794 let result = engine.fill_order(
9795 &order,
9796 Price::from("1500.00"),
9797 Quantity::from("1.000"),
9798 LiquiditySide::Taker,
9799 None,
9800 None,
9801 );
9802
9803 assert!(result.is_err());
9804 assert_eq!(engine.cached_filled_qty_len(), 0);
9805 assert!(events.borrow().is_empty());
9806 }
9807
9808 #[rstest]
9809 fn test_process_cancel_all_includes_submitted_orders_for_selected_account() {
9810 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9811 let instrument_id = instrument.id();
9812 let cache = Rc::new(RefCell::new(Cache::default()));
9813 let clock = Rc::new(RefCell::new(VirtualClock::new()));
9814 let mut engine = OrderMatchingEngine::new(
9815 instrument,
9816 1,
9817 FillModelHandle::default(),
9818 FeeModelAny::default().into(),
9819 BookType::L1_MBP,
9820 OmsType::Netting,
9821 AccountType::Margin,
9822 clock,
9823 Rc::clone(&cache),
9824 Default::default(),
9825 );
9826 let selected_account = AccountId::from("ACCOUNT-001");
9827 let other_account = AccountId::from("ACCOUNT-002");
9828 let selected_strategy = StrategyId::from("STRATEGY-001");
9829 let other_strategy = StrategyId::from("STRATEGY-002");
9830 let selected_order = OrderTestBuilder::new(OrderType::Limit)
9831 .strategy_id(selected_strategy)
9832 .instrument_id(instrument_id)
9833 .client_order_id(ClientOrderId::from("O-SUBMITTED-SELECTED"))
9834 .side(OrderSide::Buy)
9835 .price(Price::from("1400.00"))
9836 .quantity(Quantity::from("1.000"))
9837 .build();
9838 let other_order = OrderTestBuilder::new(OrderType::Limit)
9839 .strategy_id(other_strategy)
9840 .instrument_id(instrument_id)
9841 .client_order_id(ClientOrderId::from("O-SUBMITTED-OTHER"))
9842 .side(OrderSide::Buy)
9843 .price(Price::from("1300.00"))
9844 .quantity(Quantity::from("1.000"))
9845 .build();
9846 {
9847 let mut cache = cache.borrow_mut();
9848 cache
9849 .add_order(selected_order.clone(), None, None, false)
9850 .unwrap();
9851 cache
9852 .add_order(other_order.clone(), None, None, false)
9853 .unwrap();
9854 cache
9855 .update_order(&TestOrderEventStubs::submitted(
9856 &selected_order,
9857 selected_account,
9858 ))
9859 .unwrap();
9860 cache
9861 .update_order(&TestOrderEventStubs::submitted(&other_order, other_account))
9862 .unwrap();
9863 }
9864
9865 let events = Rc::new(RefCell::new(Vec::new()));
9866 let events_handler = Rc::clone(&events);
9867 let event_cache = Rc::clone(&cache);
9868 engine.set_event_handler(Rc::new(move |event| {
9869 event_cache.borrow_mut().update_order(&event).unwrap();
9870 events_handler.borrow_mut().push(event);
9871 }));
9872 let command = CancelAllOrders::new(
9873 TraderId::from("TRADER-001"),
9874 None,
9875 StrategyId::from("CALLER-001"),
9876 instrument_id,
9877 None,
9878 UUID4::new(),
9879 UnixNanos::default(),
9880 None,
9881 None,
9882 );
9883
9884 engine.process_cancel_all(&command, selected_account);
9885
9886 let events = events.borrow();
9887 assert_eq!(events.len(), 1);
9888 let OrderEventAny::Canceled(canceled) = &events[0] else {
9889 panic!("Expected OrderCanceled, was {:?}", events[0]);
9890 };
9891 assert_eq!(canceled.client_order_id, selected_order.client_order_id());
9892 assert_eq!(canceled.strategy_id, selected_strategy);
9893 assert_eq!(canceled.account_id, Some(selected_account));
9894 let cache = cache.borrow();
9895 assert_eq!(
9896 cache
9897 .order(&selected_order.client_order_id())
9898 .unwrap()
9899 .status(),
9900 OrderStatus::Canceled
9901 );
9902 assert_eq!(
9903 cache
9904 .order(&other_order.client_order_id())
9905 .unwrap()
9906 .status(),
9907 OrderStatus::Submitted
9908 );
9909 }
9910
9911 #[rstest]
9912 fn test_process_cancel_all_excluding_leaves_excluded_orders_untouched() {
9913 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9914 let instrument_id = instrument.id();
9915 let cache = Rc::new(RefCell::new(Cache::default()));
9916 let clock = Rc::new(RefCell::new(VirtualClock::new()));
9917 let mut engine = OrderMatchingEngine::new(
9918 instrument,
9919 1,
9920 FillModelHandle::default(),
9921 FeeModelAny::default().into(),
9922 BookType::L1_MBP,
9923 OmsType::Netting,
9924 AccountType::Margin,
9925 clock,
9926 Rc::clone(&cache),
9927 Default::default(),
9928 );
9929 let account_id = AccountId::from("ACCOUNT-001");
9930 let strategy_id = StrategyId::from("STRATEGY-001");
9931 let received = OrderTestBuilder::new(OrderType::Limit)
9932 .strategy_id(strategy_id)
9933 .instrument_id(instrument_id)
9934 .client_order_id(ClientOrderId::from("O-RECEIVED"))
9935 .side(OrderSide::Buy)
9936 .price(Price::from("1400.00"))
9937 .quantity(Quantity::from("1.000"))
9938 .build();
9939 let in_transit = OrderTestBuilder::new(OrderType::Limit)
9940 .strategy_id(strategy_id)
9941 .instrument_id(instrument_id)
9942 .client_order_id(ClientOrderId::from("O-IN-TRANSIT"))
9943 .side(OrderSide::Buy)
9944 .price(Price::from("1300.00"))
9945 .quantity(Quantity::from("1.000"))
9946 .build();
9947 {
9948 let mut cache = cache.borrow_mut();
9949 cache
9950 .add_order(received.clone(), None, None, false)
9951 .unwrap();
9952 cache
9953 .add_order(in_transit.clone(), None, None, false)
9954 .unwrap();
9955 cache
9956 .update_order(&TestOrderEventStubs::submitted(&received, account_id))
9957 .unwrap();
9958 cache
9959 .update_order(&TestOrderEventStubs::submitted(&in_transit, account_id))
9960 .unwrap();
9961 }
9962
9963 let events = Rc::new(RefCell::new(Vec::new()));
9964 let events_handler = Rc::clone(&events);
9965 let event_cache = Rc::clone(&cache);
9966 engine.set_event_handler(Rc::new(move |event| {
9967 event_cache.borrow_mut().update_order(&event).unwrap();
9968 events_handler.borrow_mut().push(event);
9969 }));
9970 let command = CancelAllOrders::new(
9971 TraderId::from("TRADER-001"),
9972 None,
9973 StrategyId::from("CALLER-001"),
9974 instrument_id,
9975 None,
9976 UUID4::new(),
9977 UnixNanos::default(),
9978 None,
9979 None,
9980 );
9981
9982 engine.process_cancel_all_excluding(&command, account_id, &[in_transit.client_order_id()]);
9983
9984 let events = events.borrow();
9985 assert_eq!(
9986 events.len(),
9987 1,
9988 "expected one OrderCanceled, was {events:?}"
9989 );
9990 let OrderEventAny::Canceled(canceled) = &events[0] else {
9991 panic!("Expected OrderCanceled, was {:?}", events[0]);
9992 };
9993 assert_eq!(canceled.client_order_id, received.client_order_id());
9994 assert_eq!(
9995 cache
9996 .borrow()
9997 .order(&in_transit.client_order_id())
9998 .unwrap()
9999 .status(),
10000 OrderStatus::Submitted,
10001 "an excluded order must be left untouched",
10002 );
10003 }
10004
10005 #[rstest]
10006 fn test_process_cancel_all_excluding_spares_an_excluded_contingent_order() {
10007 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10008 let instrument_id = instrument.id();
10009 let cache = Rc::new(RefCell::new(Cache::default()));
10010 let clock = Rc::new(RefCell::new(VirtualClock::new()));
10011 let mut engine = OrderMatchingEngine::new(
10012 instrument,
10013 1,
10014 FillModelHandle::default(),
10015 FeeModelAny::default().into(),
10016 BookType::L1_MBP,
10017 OmsType::Netting,
10018 AccountType::Margin,
10019 clock,
10020 Rc::clone(&cache),
10021 Default::default(),
10022 );
10023 assert!(engine.config.support_contingent_orders);
10024 let account_id = AccountId::from("ACCOUNT-001");
10025 let strategy_id = StrategyId::from("STRATEGY-001");
10026 let received_id = ClientOrderId::from("O-RECEIVED");
10027 let in_transit_id = ClientOrderId::from("O-IN-TRANSIT");
10028 let received = OrderTestBuilder::new(OrderType::Limit)
10029 .strategy_id(strategy_id)
10030 .instrument_id(instrument_id)
10031 .client_order_id(received_id)
10032 .side(OrderSide::Buy)
10033 .price(Price::from("1400.00"))
10034 .quantity(Quantity::from("1.000"))
10035 .contingency_type(ContingencyType::Oco)
10036 .linked_order_ids(vec![in_transit_id])
10037 .build();
10038 let in_transit = OrderTestBuilder::new(OrderType::Limit)
10039 .strategy_id(strategy_id)
10040 .instrument_id(instrument_id)
10041 .client_order_id(in_transit_id)
10042 .side(OrderSide::Buy)
10043 .price(Price::from("1300.00"))
10044 .quantity(Quantity::from("1.000"))
10045 .contingency_type(ContingencyType::Oco)
10046 .linked_order_ids(vec![received_id])
10047 .build();
10048 {
10049 let mut cache = cache.borrow_mut();
10050 cache
10051 .add_order(received.clone(), None, None, false)
10052 .unwrap();
10053 cache
10054 .add_order(in_transit.clone(), None, None, false)
10055 .unwrap();
10056 cache
10057 .update_order(&TestOrderEventStubs::submitted(&received, account_id))
10058 .unwrap();
10059 cache
10060 .update_order(&TestOrderEventStubs::submitted(&in_transit, account_id))
10061 .unwrap();
10062 }
10063
10064 let events = Rc::new(RefCell::new(Vec::new()));
10065 let events_handler = Rc::clone(&events);
10066 let event_cache = Rc::clone(&cache);
10067 engine.set_event_handler(Rc::new(move |event| {
10068 event_cache.borrow_mut().update_order(&event).unwrap();
10069 events_handler.borrow_mut().push(event);
10070 }));
10071 let command = CancelAllOrders::new(
10072 TraderId::from("TRADER-001"),
10073 None,
10074 StrategyId::from("CALLER-001"),
10075 instrument_id,
10076 None,
10077 UUID4::new(),
10078 UnixNanos::default(),
10079 None,
10080 None,
10081 );
10082
10083 engine.process_cancel_all_excluding(&command, account_id, &[in_transit_id]);
10084
10085 let events = events.borrow();
10086 assert_eq!(
10087 events.len(),
10088 1,
10089 "expected one OrderCanceled, was {events:?}"
10090 );
10091 let OrderEventAny::Canceled(canceled) = &events[0] else {
10092 panic!("Expected OrderCanceled, was {:?}", events[0]);
10093 };
10094 assert_eq!(canceled.client_order_id, received_id);
10095 assert_eq!(
10096 cache.borrow().order(&in_transit_id).unwrap().status(),
10097 OrderStatus::Submitted,
10098 "canceling its OCO sibling must not cancel an excluded order",
10099 );
10100 }
10101
10102 fn collision_engine() -> (OrderMatchingEngine, Rc<RefCell<Cache>>, VenueOrderId) {
10103 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10104 let cache = Rc::new(RefCell::new(Cache::default()));
10105 let venue_order_id = VenueOrderId::from(format!("{}-1-1", instrument.id().venue));
10106 cache
10107 .borrow_mut()
10108 .add_venue_order_id(&ClientOrderId::from("O-OWNER"), &venue_order_id, false)
10109 .unwrap();
10110 let engine = OrderMatchingEngine::new(
10111 instrument,
10112 1,
10113 FillModelHandle::default(),
10114 FeeModelAny::default().into(),
10115 BookType::L1_MBP,
10116 OmsType::Netting,
10117 AccountType::Margin,
10118 Rc::new(RefCell::new(VirtualClock::new())),
10119 Rc::clone(&cache),
10120 Default::default(),
10121 );
10122
10123 (engine, cache, venue_order_id)
10124 }
10125
10126 #[rstest]
10127 #[case(OrderType::Market)]
10128 #[case(OrderType::MarketToLimit)]
10129 fn test_market_collision_probes_and_fills_with_default_ack_config(
10130 #[case] order_type: OrderType,
10131 ) {
10132 let (mut engine, cache, venue_order_id) = collision_engine();
10133 assert!(!engine.config.use_market_order_acks);
10134 let quote = QuoteTick::new(
10135 engine.instrument.id(),
10136 Price::from("1499.00"),
10137 Price::from("1500.00"),
10138 Quantity::from("10.000"),
10139 Quantity::from("10.000"),
10140 UnixNanos::default(),
10141 UnixNanos::default(),
10142 );
10143 engine.process_quote_tick("e);
10144 let events = Rc::new(RefCell::new(Vec::new()));
10145 let events_handler = Rc::clone(&events);
10146 engine.set_event_handler(Rc::new(move |event| {
10147 events_handler.borrow_mut().push(event);
10148 }));
10149 let mut order = OrderTestBuilder::new(order_type)
10150 .instrument_id(engine.instrument.id())
10151 .client_order_id(ClientOrderId::from("O-CLAIMANT"))
10152 .side(OrderSide::Buy)
10153 .quantity(Quantity::from("1.000"))
10154 .submit(true)
10155 .build();
10156
10157 engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
10158
10159 assert!(
10160 !events
10161 .borrow()
10162 .iter()
10163 .any(|event| matches!(event, OrderEventAny::Rejected(_)))
10164 );
10165 assert!(
10166 events
10167 .borrow()
10168 .iter()
10169 .any(|event| matches!(event, OrderEventAny::Filled(_)))
10170 );
10171 assert!(cache.borrow().order_exists(&order.client_order_id()));
10172 assert_eq!(
10173 cache.borrow().client_order_id(&venue_order_id),
10174 Some(&ClientOrderId::from("O-OWNER"))
10175 );
10176 assert_eq!(
10177 cache.borrow().venue_order_id(&order.client_order_id()),
10178 Some(&VenueOrderId::from(format!("{}-1-2", engine.venue)))
10179 );
10180 }
10181
10182 struct RecordingFeeModel {
10183 calls: Rc<Cell<u32>>,
10184 commission: Money,
10185 }
10186
10187 impl FeeModel for RecordingFeeModel {
10188 fn get_commission(
10189 &self,
10190 _order: &OrderAny,
10191 _fill_quantity: Quantity,
10192 _fill_px: Price,
10193 _instrument: &InstrumentAny,
10194 ) -> anyhow::Result<Money> {
10195 self.calls.set(self.calls.get() + 1);
10196 Ok(self.commission)
10197 }
10198 }
10199
10200 struct FailingFeeModel;
10201
10202 impl FeeModel for FailingFeeModel {
10203 fn get_commission(
10204 &self,
10205 _order: &OrderAny,
10206 _fill_quantity: Quantity,
10207 _fill_px: Price,
10208 _instrument: &InstrumentAny,
10209 ) -> anyhow::Result<Money> {
10210 Err(anyhow::anyhow!("fee model failed"))
10211 }
10212 }
10213
10214 #[rstest]
10215 fn test_custom_fill_model_handle_is_called_by_market_fill() {
10216 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10217 let cache = Rc::new(RefCell::new(Cache::default()));
10218 let clock = Rc::new(RefCell::new(VirtualClock::new()));
10219 let calls = Rc::new(Cell::new(0));
10220 let fill_model = FillModelHandle::new(RecordingFillModel {
10221 calls: Rc::clone(&calls),
10222 });
10223 let mut engine = OrderMatchingEngine::new(
10224 instrument.clone(),
10225 1,
10226 fill_model,
10227 FeeModelAny::default().into(),
10228 BookType::L1_MBP,
10229 OmsType::Netting,
10230 AccountType::Margin,
10231 clock,
10232 cache,
10233 Default::default(),
10234 );
10235 let quote = QuoteTick::new(
10236 instrument.id(),
10237 Price::from("1500.00"),
10238 Price::from("1501.00"),
10239 Quantity::from("10.000"),
10240 Quantity::from("10.000"),
10241 UnixNanos::default(),
10242 UnixNanos::default(),
10243 );
10244 engine.process_quote_tick("e);
10245
10246 let mut order = OrderTestBuilder::new(OrderType::Market)
10247 .instrument_id(instrument.id())
10248 .side(OrderSide::Buy)
10249 .quantity(Quantity::from("1.000"))
10250 .submit(true)
10251 .build();
10252 engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
10253
10254 assert_eq!(calls.get(), 1);
10255 }
10256
10257 #[rstest]
10258 fn test_l1_depth_skips_padding_for_last_quote_tracking() {
10259 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10260 let cache = Rc::new(RefCell::new(Cache::default()));
10261 let clock = Rc::new(RefCell::new(VirtualClock::new()));
10262 let mut engine = OrderMatchingEngine::new(
10263 instrument.clone(),
10264 1,
10265 FillModelHandle::default(),
10266 FeeModelAny::default().into(),
10267 BookType::L1_MBP,
10268 OmsType::Netting,
10269 AccountType::Margin,
10270 clock,
10271 cache,
10272 Default::default(),
10273 );
10274 let mut bids = [BookOrder::default(); DEPTH10_LEN];
10275 let mut asks = [BookOrder::default(); DEPTH10_LEN];
10276 bids[1] = BookOrder::new(
10277 OrderSide::Buy,
10278 Price::from("1499.00"),
10279 Quantity::from("1.000"),
10280 1,
10281 );
10282 asks[0] = BookOrder::new(
10283 OrderSide::Sell,
10284 Price::from("1500.00"),
10285 Quantity::from("1.000"),
10286 2,
10287 );
10288
10289 let depth = OrderBookDepth::new(
10290 instrument.id(),
10291 bids,
10292 asks,
10293 [0; DEPTH10_LEN],
10294 [0; DEPTH10_LEN],
10295 0,
10296 0,
10297 UnixNanos::from(1_u64),
10298 UnixNanos::from(1_u64),
10299 );
10300 engine.process_order_book_depth(&depth).unwrap();
10301
10302 assert_eq!(engine.last_quote_bid, Some(Price::from("1499.00")));
10303 assert_eq!(engine.last_quote_ask, Some(Price::from("1500.00")));
10304
10305 let depth_without_bid = OrderBookDepth::new(
10306 instrument.id(),
10307 [BookOrder::default(); DEPTH10_LEN],
10308 asks,
10309 [0; DEPTH10_LEN],
10310 [0; DEPTH10_LEN],
10311 0,
10312 1,
10313 UnixNanos::from(2_u64),
10314 UnixNanos::from(2_u64),
10315 );
10316 engine.process_order_book_depth(&depth_without_bid).unwrap();
10317
10318 assert_eq!(engine.last_quote_bid, None);
10319 assert_eq!(engine.last_quote_ask, Some(Price::from("1500.00")));
10320 }
10321
10322 struct RecordingFillModel {
10323 calls: Rc<Cell<u32>>,
10324 }
10325
10326 impl FillModel for RecordingFillModel {
10327 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
10328 Ok(true)
10329 }
10330
10331 fn is_slipped(&mut self) -> anyhow::Result<bool> {
10332 Ok(false)
10333 }
10334
10335 fn get_orderbook_for_fill_simulation(
10336 &mut self,
10337 _instrument: &InstrumentAny,
10338 _order: &OrderAny,
10339 _best_bid: Price,
10340 _best_ask: Price,
10341 ) -> anyhow::Result<Option<OrderBook>> {
10342 self.calls.set(self.calls.get() + 1);
10343 Ok(None)
10344 }
10345 }
10346
10347 #[rstest]
10348 fn test_fee_underlying_price_uses_valid_cached_greeks_price() {
10349 let instrument = InstrumentAny::CryptoOption(crypto_option_btc_deribit(
10350 3,
10351 1,
10352 Price::from("0.001"),
10353 Quantity::from("0.1"),
10354 ));
10355 let cache = Rc::new(RefCell::new(Cache::default()));
10356 cache.borrow_mut().add_option_greeks(OptionGreeks {
10357 instrument_id: instrument.id(),
10358 underlying_price: Some(50_000.0),
10359 ..Default::default()
10360 });
10361 let clock = Rc::new(RefCell::new(VirtualClock::new()));
10362 let engine = OrderMatchingEngine::new(
10363 instrument,
10364 1,
10365 FillModelHandle::default(),
10366 FeeModelAny::default().into(),
10367 BookType::L1_MBP,
10368 OmsType::Netting,
10369 AccountType::Margin,
10370 clock,
10371 cache,
10372 Default::default(),
10373 );
10374
10375 let price = engine
10376 .fee_underlying_price()
10377 .unwrap()
10378 .expect("expected underlying price");
10379
10380 assert_eq!(price.precision, FIXED_PRECISION);
10381 assert_eq!(price.as_decimal(), Decimal::from(50_000));
10382 }
10383
10384 #[rstest]
10385 fn test_fee_underlying_price_rejects_invalid_cached_greeks_price() {
10386 let instrument = InstrumentAny::CryptoOption(crypto_option_btc_deribit(
10387 3,
10388 1,
10389 Price::from("0.001"),
10390 Quantity::from("0.1"),
10391 ));
10392 let cache = Rc::new(RefCell::new(Cache::default()));
10393 cache.borrow_mut().add_option_greeks(OptionGreeks {
10394 instrument_id: instrument.id(),
10395 underlying_price: Some(f64::NAN),
10396 ..Default::default()
10397 });
10398 let clock = Rc::new(RefCell::new(VirtualClock::new()));
10399 let engine = OrderMatchingEngine::new(
10400 instrument,
10401 1,
10402 FillModelHandle::default(),
10403 FeeModelAny::default().into(),
10404 BookType::L1_MBP,
10405 OmsType::Netting,
10406 AccountType::Margin,
10407 clock,
10408 cache,
10409 Default::default(),
10410 );
10411
10412 let error = engine.fee_underlying_price().unwrap_err();
10413
10414 assert_eq!(
10415 error,
10416 CorrectnessError::InvalidValue {
10417 param: "value".to_string(),
10418 value: "NaN".to_string(),
10419 type_name: "f64",
10420 }
10421 );
10422 }
10423
10424 #[rstest]
10425 fn test_bar_tick_sizes_divisible() {
10426 let volume = Quantity::from("100.000");
10428 let increment = Quantity::from("0.001");
10429 let sizes = BarTickSizes::from_volume(volume, increment);
10430 assert_eq!(sizes.open, Quantity::from("25.000"));
10431 assert_eq!(sizes.high, Quantity::from("25.000"));
10432 assert_eq!(sizes.low, Quantity::from("25.000"));
10433 assert_eq!(sizes.close, Quantity::from("25.000"));
10434 assert_valid_bar_tick_sizes(volume, increment);
10435 }
10436
10437 #[rstest]
10438 fn test_bar_tick_sizes_indivisible_with_remainder() {
10439 let volume = Quantity::from("0.05");
10441 let increment = Quantity::from("0.01");
10442 let sizes = BarTickSizes::from_volume(volume, increment);
10443 assert_eq!(sizes.open, Quantity::from("0.01"));
10444 assert_eq!(sizes.high, Quantity::from("0.01"));
10445 assert_eq!(sizes.low, Quantity::from("0.01"));
10446 assert_eq!(sizes.close, Quantity::from("0.02"));
10447 assert_valid_bar_tick_sizes(volume, increment);
10448 assert_eq!(
10449 sizes.open.raw() + sizes.high.raw() + sizes.low.raw() + sizes.close.raw(),
10450 volume.raw()
10451 );
10452 }
10453
10454 #[rstest]
10455 #[case("1", "0", "0", "0", "1")]
10456 #[case("2", "0", "1", "1", "0")]
10457 #[case("3", "1", "1", "1", "0")]
10458 fn test_bar_tick_sizes_units_less_than_four_preserves_volume(
10459 #[case] volume: &str,
10460 #[case] open_size: &str,
10461 #[case] high_size: &str,
10462 #[case] low_size: &str,
10463 #[case] close_size: &str,
10464 ) {
10465 let volume = Quantity::from(volume);
10466 let increment = Quantity::from("1");
10467 let sizes = BarTickSizes::from_volume(volume, increment);
10468
10469 assert_eq!(sizes.open, Quantity::from(open_size));
10470 assert_eq!(sizes.high, Quantity::from(high_size));
10471 assert_eq!(sizes.low, Quantity::from(low_size));
10472 assert_eq!(sizes.close, Quantity::from(close_size));
10473 assert_valid_bar_tick_sizes(volume, increment);
10474 assert_eq!(
10475 sizes.open.raw() + sizes.high.raw() + sizes.low.raw() + sizes.close.raw(),
10476 volume.raw()
10477 );
10478 }
10479
10480 #[rstest]
10481 fn test_bar_tick_sizes_zero_volume_remains_zero() {
10482 let volume = Quantity::zero(3);
10483 let increment = Quantity::from("0.001");
10484 let sizes = BarTickSizes::from_volume(volume, increment);
10485 assert_eq!(sizes.open, Quantity::zero(3));
10486 assert_eq!(sizes.high, Quantity::zero(3));
10487 assert_eq!(sizes.low, Quantity::zero(3));
10488 assert_eq!(sizes.close, Quantity::zero(3));
10489 assert_valid_bar_tick_sizes(volume, increment);
10490 }
10491
10492 #[rstest]
10493 fn test_bar_tick_sizes_rounds_down_to_size_increment() {
10494 let volume = Quantity::from("1.07");
10495 let increment = Quantity::from("0.10");
10496 let sizes = BarTickSizes::from_volume(volume, increment);
10497 assert_eq!(sizes.open, Quantity::from("0.20"));
10498 assert_eq!(sizes.high, Quantity::from("0.20"));
10499 assert_eq!(sizes.low, Quantity::from("0.20"));
10500 assert_eq!(sizes.close, Quantity::from("0.40"));
10501 assert_valid_bar_tick_sizes(volume, increment);
10502 }
10503
10504 #[rstest]
10505 fn test_bar_tick_sizes_at_fixed_precision() {
10506 let units: QuantityRaw = 17;
10509 let volume = Quantity::from_raw(units, FIXED_PRECISION);
10510 let increment = Quantity::from_raw(1, FIXED_PRECISION);
10511 let sizes = BarTickSizes::from_volume(volume, increment);
10512 assert_eq!(sizes.open.raw(), 4);
10513 assert_eq!(sizes.high.raw(), 4);
10514 assert_eq!(sizes.low.raw(), 4);
10515 assert_eq!(sizes.close.raw(), 5);
10516 assert_valid_bar_tick_sizes(volume, increment);
10517 }
10518
10519 fn get_queue_engine(
10520 instrument: InstrumentAny,
10521 book_type: BookType,
10522 ) -> (OrderMatchingEngine, Rc<RefCell<Cache>>) {
10523 let clock = Rc::new(RefCell::new(VirtualClock::new()));
10524 let cache = Rc::new(RefCell::new(Cache::default()));
10525 let config = OrderMatchingEngineConfig {
10526 trade_execution: true,
10527 queue_position: true,
10528 ..Default::default()
10529 };
10530
10531 let mut engine = OrderMatchingEngine::new(
10532 instrument,
10533 1,
10534 FillModelHandle::default(),
10535 FeeModelAny::default().into(),
10536 book_type,
10537 OmsType::Netting,
10538 AccountType::Margin,
10539 clock,
10540 Rc::clone(&cache),
10541 config,
10542 );
10543
10544 let handler_cache = Rc::clone(&cache);
10545 engine.set_event_handler(Rc::new(move |event: OrderEventAny| {
10546 if let Ok(mut cache) = handler_cache.try_borrow_mut() {
10547 let _ = cache.update_order(&event);
10548 }
10549 }));
10550
10551 (engine, cache)
10552 }
10553
10554 fn get_l3_queue_engine(instrument: InstrumentAny) -> (OrderMatchingEngine, Rc<RefCell<Cache>>) {
10555 get_queue_engine(instrument, BookType::L3_MBO)
10556 }
10557
10558 fn assert_l3_queue_synced(engine: &OrderMatchingEngine) {
10559 for (client_order_id, orders_ahead) in &engine.queue_ahead_orders {
10560 let set_sum: QuantityRaw = orders_ahead.values().sum();
10561 let counter = engine
10562 .queue_ahead_total
10563 .get(client_order_id)
10564 .map_or(0, |&(_, ahead_raw)| ahead_raw);
10565 assert_eq!(
10566 set_sum, counter,
10567 "tracked orders out of sync with quantity-ahead counter for {client_order_id}",
10568 );
10569 }
10570
10571 for (client_order_id, price_raw) in &engine.queue_pending {
10572 assert!(
10573 engine
10574 .queue_ids_by_price
10575 .get(price_raw)
10576 .is_some_and(|ids| ids.contains(client_order_id)),
10577 "pending order {client_order_id} missing from price index",
10578 );
10579 }
10580
10581 for (client_order_id, (price_raw, _)) in &engine.queue_ahead_total {
10582 assert!(
10583 engine
10584 .queue_ids_by_price
10585 .get(price_raw)
10586 .is_some_and(|ids| ids.contains(client_order_id)),
10587 "tracked order {client_order_id} missing from price index",
10588 );
10589 }
10590
10591 for (price_raw, client_order_ids) in &engine.queue_ids_by_price {
10592 for client_order_id in client_order_ids {
10593 let pending_at_price = engine.queue_pending.get(client_order_id) == Some(price_raw);
10594 let tracked_at_price = engine
10595 .queue_ahead_total
10596 .get(client_order_id)
10597 .is_some_and(|(tracked_price_raw, _)| tracked_price_raw == price_raw);
10598 assert!(
10599 pending_at_price || tracked_at_price,
10600 "price index contains stale order {client_order_id}",
10601 );
10602 }
10603 }
10604 }
10605
10606 #[rstest]
10607 fn test_reset_clears_queue_positions() {
10608 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10609 let (mut engine, _cache) = get_l3_queue_engine(instrument);
10610 let price = Price::from("100.00");
10611 let client_order_id = ClientOrderId::from("O-RESET-QUEUE");
10612
10613 rest_l3_queue_order(&mut engine, price, 1, client_order_id);
10614
10615 assert!(engine.queue_ahead_total.contains_key(&client_order_id));
10616 assert!(engine.queue_ahead_orders.contains_key(&client_order_id));
10617 assert!(
10618 engine
10619 .queue_ids_by_price
10620 .get(&price.raw())
10621 .is_some_and(|ids| ids.contains(&client_order_id)),
10622 );
10623
10624 engine.reset();
10625
10626 assert!(engine.queue_pending.is_empty());
10627 assert!(engine.queue_ahead_total.is_empty());
10628 assert!(engine.queue_ahead_orders.is_empty());
10629 assert!(engine.queue_excess.is_empty());
10630 assert!(engine.queue_ids_by_price.is_empty());
10631 }
10632
10633 #[rstest]
10634 fn test_cancel_removes_queue_position() {
10635 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10636 let (mut engine, _cache) = get_l3_queue_engine(instrument);
10637 let price = Price::from("100.00");
10638 let order =
10639 rest_l3_queue_order(&mut engine, price, 1, ClientOrderId::from("O-CANCEL-QUEUE"));
10640 let client_order_id = order.client_order_id();
10641
10642 assert!(engine.queue_ahead_total.contains_key(&client_order_id));
10643 assert!(engine.queue_ahead_orders.contains_key(&client_order_id));
10644 assert!(
10645 engine
10646 .queue_ids_by_price
10647 .get(&price.raw())
10648 .is_some_and(|ids| ids.contains(&client_order_id)),
10649 );
10650
10651 engine.cancel_order(&order, None);
10652
10653 assert!(!engine.queue_pending.contains_key(&client_order_id));
10654 assert!(!engine.queue_ahead_total.contains_key(&client_order_id));
10655 assert!(!engine.queue_ahead_orders.contains_key(&client_order_id));
10656 assert!(!engine.queue_excess.contains_key(&client_order_id));
10657 assert!(!engine.queue_ids_by_price.contains_key(&price.raw()));
10658 }
10659
10660 #[rstest]
10661 fn test_modify_reindexes_queue_position() {
10662 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10663 let (mut engine, _cache) = get_l3_queue_engine(instrument);
10664 let old_price = Price::from("100.00");
10665 let new_price = Price::from("101.00");
10666 let client_order_id = ClientOrderId::from("O-MODIFY-QUEUE");
10667 let order = rest_l3_queue_order(&mut engine, old_price, 1, client_order_id);
10668 let new_level = OrderBookDelta::new(
10669 engine.instrument.id(),
10670 BookAction::Add,
10671 BookOrder::new(OrderSide::Sell, new_price, Quantity::from("10.000"), 2),
10672 0,
10673 2,
10674 UnixNanos::from(2),
10675 UnixNanos::from(2),
10676 );
10677 engine.process_order_book_delta(&new_level).unwrap();
10678
10679 let command = ModifyOrder::new(
10680 order.trader_id(),
10681 None,
10682 order.strategy_id(),
10683 order.instrument_id(),
10684 client_order_id,
10685 order.venue_order_id(),
10686 None,
10687 Some(new_price),
10688 None,
10689 UUID4::new(),
10690 UnixNanos::from(3),
10691 None,
10692 None,
10693 );
10694 engine.process_modify(&command, AccountId::from("SIM-001"));
10695
10696 assert!(!engine.queue_ids_by_price.contains_key(&old_price.raw()));
10697 assert_eq!(
10698 engine
10699 .queue_ids_by_price
10700 .get(&new_price.raw())
10701 .map(|ids| ids.iter().copied().collect::<Vec<_>>()),
10702 Some(vec![client_order_id]),
10703 );
10704 assert_eq!(
10705 engine.queue_ahead_total.get(&client_order_id),
10706 Some(&(new_price.raw(), Quantity::from("10.000").raw())),
10707 );
10708 assert_eq!(
10709 engine
10710 .queue_ahead_orders
10711 .get(&client_order_id)
10712 .map(|orders| orders.keys().copied().collect::<Vec<_>>()),
10713 Some(vec![2]),
10714 );
10715 }
10716
10717 #[rstest]
10718 fn test_snapshot_rebases_l2_queue_position_after_size_decrease() {
10719 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10720 let instrument_id = instrument.id();
10721 let (mut engine, cache) = get_queue_engine(instrument, BookType::L2_MBP);
10722
10723 let initial = OrderBookDelta::new(
10724 instrument_id,
10725 BookAction::Add,
10726 BookOrder::new(
10727 OrderSide::Sell,
10728 Price::from("100.00"),
10729 Quantity::from("10.000"),
10730 0,
10731 ),
10732 0,
10733 1,
10734 UnixNanos::from(1_u64),
10735 UnixNanos::from(1_u64),
10736 );
10737 engine.process_order_book_delta(&initial).unwrap();
10738
10739 let client_order_id = ClientOrderId::from("O-SNAPSHOT-DECREASE");
10740 let mut order = OrderTestBuilder::new(OrderType::Limit)
10741 .instrument_id(instrument_id)
10742 .side(OrderSide::Sell)
10743 .price(Price::from("100.00"))
10744 .quantity(Quantity::from("1.000"))
10745 .client_order_id(client_order_id)
10746 .submit(true)
10747 .build();
10748 engine.process_order(&mut order, AccountId::from("SIM-001"));
10749 assert_eq!(
10750 engine.queue_ahead_total.get(&client_order_id),
10751 Some(&(Price::from("100.00").raw(), Quantity::from("10.000").raw())),
10752 );
10753
10754 let clear = OrderBookDelta::clear(
10755 instrument_id,
10756 2,
10757 UnixNanos::from(2_u64),
10758 UnixNanos::from(2_u64),
10759 );
10760 engine.process_order_book_delta(&clear).unwrap();
10761 assert_eq!(
10762 engine.queue_ahead_total.get(&client_order_id),
10763 Some(&(Price::from("100.00").raw(), Quantity::from("10.000").raw())),
10764 "partial snapshot must not discard the old queue estimate",
10765 );
10766
10767 let snapshot = OrderBookDelta::new(
10768 instrument_id,
10769 BookAction::Add,
10770 BookOrder::new(
10771 OrderSide::Sell,
10772 Price::from("100.00"),
10773 Quantity::from("8.000"),
10774 0,
10775 ),
10776 RecordFlag::F_LAST as u8,
10777 2,
10778 UnixNanos::from(2_u64),
10779 UnixNanos::from(2_u64),
10780 );
10781 engine.process_order_book_delta(&snapshot).unwrap();
10782
10783 assert_eq!(
10784 engine.queue_ahead_total.get(&client_order_id),
10785 Some(&(Price::from("100.00").raw(), Quantity::from("8.000").raw())),
10786 );
10787 assert!(cache.borrow().order(&client_order_id).is_some());
10788 }
10789
10790 #[rstest]
10791 fn test_snapshot_rebase_does_not_increase_l2_queue_position() {
10792 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10793 let instrument_id = instrument.id();
10794 let (mut engine, _cache) = get_queue_engine(instrument, BookType::L2_MBP);
10795
10796 let initial = OrderBookDelta::new(
10797 instrument_id,
10798 BookAction::Add,
10799 BookOrder::new(
10800 OrderSide::Sell,
10801 Price::from("100.00"),
10802 Quantity::from("10.000"),
10803 0,
10804 ),
10805 0,
10806 1,
10807 UnixNanos::from(1_u64),
10808 UnixNanos::from(1_u64),
10809 );
10810 engine.process_order_book_delta(&initial).unwrap();
10811
10812 let client_order_id = ClientOrderId::from("O-SNAPSHOT-INCREASE");
10813 let mut order = OrderTestBuilder::new(OrderType::Limit)
10814 .instrument_id(instrument_id)
10815 .side(OrderSide::Sell)
10816 .price(Price::from("100.00"))
10817 .quantity(Quantity::from("1.000"))
10818 .client_order_id(client_order_id)
10819 .submit(true)
10820 .build();
10821 engine.process_order(&mut order, AccountId::from("SIM-001"));
10822
10823 let snapshot = OrderBookDelta::new(
10824 instrument_id,
10825 BookAction::Add,
10826 BookOrder::new(
10827 OrderSide::Sell,
10828 Price::from("100.00"),
10829 Quantity::from("15.000"),
10830 0,
10831 ),
10832 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
10833 2,
10834 UnixNanos::from(2_u64),
10835 UnixNanos::from(2_u64),
10836 );
10837 engine.process_order_book_delta(&snapshot).unwrap();
10838
10839 assert_eq!(
10840 engine.queue_ahead_total.get(&client_order_id),
10841 Some(&(Price::from("100.00").raw(), Quantity::from("10.000").raw())),
10842 );
10843 }
10844
10845 #[rstest]
10846 fn test_depth_rebases_l2_queue_position() {
10847 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10848 let instrument_id = instrument.id();
10849 let (mut engine, _cache) = get_queue_engine(instrument, BookType::L2_MBP);
10850
10851 let mut asks = [BookOrder::default(); DEPTH10_LEN];
10852 asks[0] = BookOrder::new(
10853 OrderSide::Sell,
10854 Price::from("100.00"),
10855 Quantity::from("10.000"),
10856 0,
10857 );
10858 let initial = OrderBookDepth::new(
10859 instrument_id,
10860 [BookOrder::default(); DEPTH10_LEN],
10861 asks,
10862 [0; DEPTH10_LEN],
10863 [0; DEPTH10_LEN],
10864 0,
10865 1,
10866 UnixNanos::from(1_u64),
10867 UnixNanos::from(1_u64),
10868 );
10869 engine.process_order_book_depth(&initial).unwrap();
10870
10871 let client_order_id = ClientOrderId::from("O-DEPTH-REBASE");
10872 let mut order = OrderTestBuilder::new(OrderType::Limit)
10873 .instrument_id(instrument_id)
10874 .side(OrderSide::Sell)
10875 .price(Price::from("100.00"))
10876 .quantity(Quantity::from("1.000"))
10877 .client_order_id(client_order_id)
10878 .submit(true)
10879 .build();
10880 engine.process_order(&mut order, AccountId::from("SIM-001"));
10881
10882 asks[0] = BookOrder::new(
10883 OrderSide::Sell,
10884 Price::from("100.00"),
10885 Quantity::from("8.000"),
10886 0,
10887 );
10888 let replacement = OrderBookDepth::new(
10889 instrument_id,
10890 [BookOrder::default(); DEPTH10_LEN],
10891 asks,
10892 [0; DEPTH10_LEN],
10893 [0; DEPTH10_LEN],
10894 0,
10895 2,
10896 UnixNanos::from(2_u64),
10897 UnixNanos::from(2_u64),
10898 );
10899 engine.process_order_book_depth(&replacement).unwrap();
10900
10901 assert_eq!(
10902 engine.queue_ahead_total.get(&client_order_id),
10903 Some(&(Price::from("100.00").raw(), Quantity::from("8.000").raw())),
10904 );
10905 }
10906
10907 #[rstest]
10908 fn test_snapshot_rebases_each_l3_order_independently() {
10909 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10910 let instrument_id = instrument.id();
10911 let (mut engine, _cache) = get_l3_queue_engine(instrument);
10912
10913 for (order_id, sequence) in [(1, 1), (2, 2)] {
10914 let delta = OrderBookDelta::new(
10915 instrument_id,
10916 BookAction::Add,
10917 BookOrder::new(
10918 OrderSide::Sell,
10919 Price::from("100.00"),
10920 Quantity::from("5.000"),
10921 order_id,
10922 ),
10923 0,
10924 sequence,
10925 UnixNanos::from(sequence),
10926 UnixNanos::from(sequence),
10927 );
10928 engine.process_order_book_delta(&delta).unwrap();
10929 }
10930
10931 let client_order_id = ClientOrderId::from("O-SNAPSHOT-L3");
10932 let order = rest_l3_queue_order(&mut engine, Price::from("100.00"), 3, client_order_id);
10933 assert_eq!(
10934 engine.queue_ahead_orders[&client_order_id]
10935 .keys()
10936 .copied()
10937 .collect::<Vec<_>>(),
10938 vec![1, 2, 3],
10939 );
10940 assert_eq!(
10941 engine.queue_ahead_total.get(&client_order_id),
10942 Some(&(Price::from("100.00").raw(), Quantity::from("20.000").raw())),
10943 );
10944
10945 let snapshot = OrderBookDeltas::new(
10946 instrument_id,
10947 vec![
10948 OrderBookDelta::clear(
10949 instrument_id,
10950 4,
10951 UnixNanos::from(4_u64),
10952 UnixNanos::from(4_u64),
10953 ),
10954 OrderBookDelta::new(
10955 instrument_id,
10956 BookAction::Add,
10957 BookOrder::new(
10958 OrderSide::Sell,
10959 Price::from("100.00"),
10960 Quantity::from("10.000"),
10961 1,
10962 ),
10963 RecordFlag::F_SNAPSHOT as u8,
10964 4,
10965 UnixNanos::from(4_u64),
10966 UnixNanos::from(4_u64),
10967 ),
10968 OrderBookDelta::new(
10969 instrument_id,
10970 BookAction::Add,
10971 BookOrder::new(
10972 OrderSide::Sell,
10973 Price::from("100.00"),
10974 Quantity::from("5.000"),
10975 2,
10976 ),
10977 RecordFlag::F_LAST as u8,
10978 4,
10979 UnixNanos::from(4_u64),
10980 UnixNanos::from(4_u64),
10981 ),
10982 ],
10983 );
10984 engine.process_order_book_deltas(&snapshot).unwrap();
10985
10986 assert_eq!(
10987 engine.queue_ahead_orders[&client_order_id]
10988 .iter()
10989 .map(|(&order_id, &size)| (order_id, size))
10990 .collect::<Vec<_>>(),
10991 vec![
10992 (1, Quantity::from("5.000").raw()),
10993 (2, Quantity::from("5.000").raw())
10994 ],
10995 );
10996 assert_eq!(
10997 engine.queue_ahead_total.get(&client_order_id),
10998 Some(&(Price::from("100.00").raw(), Quantity::from("10.000").raw())),
10999 );
11000
11001 let delete_a = OrderBookDelta::new(
11002 instrument_id,
11003 BookAction::Delete,
11004 BookOrder::new(
11005 OrderSide::Sell,
11006 Price::from("100.00"),
11007 Quantity::from("10.000"),
11008 1,
11009 ),
11010 0,
11011 5,
11012 UnixNanos::from(5_u64),
11013 UnixNanos::from(5_u64),
11014 );
11015 engine.process_order_book_delta(&delete_a).unwrap();
11016
11017 assert_eq!(
11018 engine.queue_ahead_orders[&client_order_id]
11019 .keys()
11020 .copied()
11021 .collect::<Vec<_>>(),
11022 vec![2],
11023 );
11024 assert_eq!(
11025 engine.queue_ahead_total.get(&client_order_id),
11026 Some(&(Price::from("100.00").raw(), Quantity::from("5.000").raw())),
11027 );
11028 assert_eq!(order.client_order_id(), client_order_id);
11029 }
11030
11031 #[rstest]
11032 fn test_queue_price_index_filters_other_prices() {
11033 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
11034 let (mut engine, _cache) = get_l3_queue_engine(instrument);
11035 let target_price = Price::from("100.00");
11036 let other_price = Price::from("101.00");
11037 let target_id = ClientOrderId::from("O-QUEUE-TARGET");
11038 let other_id = ClientOrderId::from("O-QUEUE-OTHER");
11039
11040 rest_l3_queue_order(&mut engine, target_price, 1, target_id);
11041 rest_l3_queue_order(&mut engine, other_price, 2, other_id);
11042
11043 let indexed_ids = engine.take_queue_ids_at_price(target_price.raw());
11044
11045 assert_eq!(indexed_ids, vec![target_id]);
11046 assert!(
11047 engine
11048 .queue_ids_by_price
11049 .get(&other_price.raw())
11050 .is_some_and(|ids| ids.contains(&other_id)),
11051 );
11052 }
11053
11054 fn rest_l3_queue_order(
11055 engine: &mut OrderMatchingEngine,
11056 price: Price,
11057 sequence: u64,
11058 client_order_id: ClientOrderId,
11059 ) -> OrderAny {
11060 let instrument_id = engine.instrument.id();
11061 let delta = OrderBookDelta::new(
11062 instrument_id,
11063 BookAction::Add,
11064 BookOrder::new(OrderSide::Sell, price, Quantity::from("10.000"), sequence),
11065 0,
11066 sequence,
11067 UnixNanos::from(sequence),
11068 UnixNanos::from(sequence),
11069 );
11070 engine.process_order_book_delta(&delta).unwrap();
11071
11072 let mut order = OrderTestBuilder::new(OrderType::Limit)
11073 .instrument_id(instrument_id)
11074 .side(OrderSide::Sell)
11075 .price(price)
11076 .quantity(Quantity::from("5.000"))
11077 .client_order_id(client_order_id)
11078 .submit(true)
11079 .build();
11080 engine.process_order(&mut order, AccountId::from("SIM-001"));
11081
11082 order
11083 }
11084
11085 #[derive(Debug, Clone, Copy)]
11086 enum QueueEvent {
11087 Add { id: OrderId, size: u64 },
11088 Update { id: OrderId, size: u64 },
11089 MoveAway { id: OrderId },
11090 Delete { id: OrderId },
11091 Trade { size: u64, aggressor: u8 },
11092 AggregateCap { size: u64 },
11093 AggregateDelete,
11094 RestOrder,
11095 }
11096
11097 fn granular_queue_event() -> impl Strategy<Value = QueueEvent> {
11098 prop_oneof![
11099 3 => (1u64..=6, 1u64..=9).prop_map(|(id, size)| QueueEvent::Add { id, size }),
11100 3 => (1u64..=6, 1u64..=9).prop_map(|(id, size)| QueueEvent::Update { id, size }),
11101 1 => (1u64..=6).prop_map(|id| QueueEvent::MoveAway { id }),
11102 2 => (1u64..=6).prop_map(|id| QueueEvent::Delete { id }),
11103 2 => Just(QueueEvent::RestOrder),
11104 ]
11105 }
11106
11107 fn any_queue_event() -> impl Strategy<Value = QueueEvent> {
11108 prop_oneof![
11109 5 => granular_queue_event(),
11110 3 => (1u64..=9, 0u8..3).prop_map(|(size, aggressor)| QueueEvent::Trade {
11111 size,
11112 aggressor,
11113 }),
11114 1 => (1u64..=9).prop_map(|size| QueueEvent::AggregateCap { size }),
11115 1 => Just(QueueEvent::AggregateDelete),
11116 ]
11117 }
11118
11119 struct L3QueueSim {
11122 engine: OrderMatchingEngine,
11123 account_id: AccountId,
11124 live_main: HashMap<OrderId, u64>,
11125 live_away: HashSet<OrderId>,
11126 rest_snapshots: HashMap<ClientOrderId, HashSet<OrderId>>,
11127 rested: usize,
11128 sequence: u64,
11129 }
11130
11131 impl L3QueueSim {
11132 const MAIN_PRICE: &'static str = "100.00";
11133 const AWAY_PRICE: &'static str = "101.00";
11134
11135 fn new() -> Self {
11136 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
11137 let (engine, _cache) = get_l3_queue_engine(instrument);
11138
11139 Self {
11140 engine,
11141 account_id: AccountId::from("SIM-001"),
11142 live_main: HashMap::new(),
11143 live_away: HashSet::new(),
11144 rest_snapshots: HashMap::new(),
11145 rested: 0,
11146 sequence: 0,
11147 }
11148 }
11149
11150 fn quantity(size: u64) -> Quantity {
11151 Quantity::from(format!("{size}.000").as_str())
11152 }
11153
11154 fn process_delta(
11155 &mut self,
11156 action: BookAction,
11157 price: &str,
11158 size: u64,
11159 order_id: OrderId,
11160 flags: u8,
11161 ) {
11162 self.sequence += 1;
11163 let delta = OrderBookDelta::new(
11164 self.engine.instrument.id(),
11165 action,
11166 BookOrder::new(
11167 OrderSide::Sell,
11168 Price::from(price),
11169 Self::quantity(size),
11170 order_id,
11171 ),
11172 flags,
11173 self.sequence,
11174 UnixNanos::from(self.sequence),
11175 UnixNanos::from(self.sequence),
11176 );
11177 self.engine.process_order_book_delta(&delta).unwrap();
11178 }
11179
11180 fn apply(&mut self, event: QueueEvent) {
11181 match event {
11182 QueueEvent::Add { id, size } => {
11183 if self.live_main.contains_key(&id) || self.live_away.contains(&id) {
11184 return;
11185 }
11186 self.process_delta(BookAction::Add, Self::MAIN_PRICE, size, id, 0);
11187 self.live_main.insert(id, size);
11188 }
11189 QueueEvent::Update { id, size } => {
11190 if !self.live_main.contains_key(&id) {
11191 return;
11192 }
11193 self.process_delta(BookAction::Update, Self::MAIN_PRICE, size, id, 0);
11194 self.live_main.insert(id, size);
11195 }
11196 QueueEvent::MoveAway { id } => {
11197 let Some(size) = self.live_main.remove(&id) else {
11198 return;
11199 };
11200 self.process_delta(BookAction::Update, Self::AWAY_PRICE, size, id, 0);
11201 self.live_away.insert(id);
11202 }
11203 QueueEvent::Delete { id } => {
11204 if let Some(size) = self.live_main.remove(&id) {
11205 self.process_delta(BookAction::Delete, Self::MAIN_PRICE, size, id, 0);
11206 } else if self.live_away.remove(&id) {
11207 self.process_delta(BookAction::Delete, Self::AWAY_PRICE, 1, id, 0);
11208 } else {
11209 self.process_delta(BookAction::Delete, Self::MAIN_PRICE, 1, id, 0);
11211 }
11212
11213 for snapshot_ids in self.rest_snapshots.values_mut() {
11216 snapshot_ids.remove(&id);
11217 }
11218 }
11219 QueueEvent::Trade { size, aggressor } => {
11220 self.sequence += 1;
11221 let aggressor_side = match aggressor {
11222 0 => AggressorSide::Buy,
11223 1 => AggressorSide::Sell,
11224 _ => AggressorSide::NoAggressor,
11225 };
11226 let trade = TradeTick::new(
11227 self.engine.instrument.id(),
11228 Price::from(Self::MAIN_PRICE),
11229 Self::quantity(size),
11230 aggressor_side,
11231 TradeId::new(format!("T-{}", self.sequence).as_str()),
11232 UnixNanos::from(self.sequence),
11233 UnixNanos::from(self.sequence),
11234 );
11235 self.engine.process_trade_tick(&trade);
11236 }
11237 QueueEvent::AggregateCap { size } => {
11238 self.process_delta(
11239 BookAction::Update,
11240 Self::MAIN_PRICE,
11241 size,
11242 0,
11243 RecordFlag::F_MBP as u8,
11244 );
11245 }
11246 QueueEvent::AggregateDelete => {
11247 self.process_delta(
11248 BookAction::Delete,
11249 Self::MAIN_PRICE,
11250 1,
11251 0,
11252 RecordFlag::F_MBP as u8,
11253 );
11254 }
11255 QueueEvent::RestOrder => {
11256 if self.rested >= 3 {
11257 return;
11258 }
11259 self.rested += 1;
11260 let mut order = OrderTestBuilder::new(OrderType::Limit)
11261 .instrument_id(self.engine.instrument.id())
11262 .side(OrderSide::Sell)
11263 .price(Price::from(Self::MAIN_PRICE))
11264 .quantity(Self::quantity(5))
11265 .client_order_id(ClientOrderId::from(
11266 format!("O-PROP-{}", self.rested).as_str(),
11267 ))
11268 .submit(true)
11269 .build();
11270 self.engine.process_order(&mut order, self.account_id);
11271
11272 assert!(
11273 self.engine
11274 .queue_ahead_orders
11275 .contains_key(&order.client_order_id()),
11276 "L3 snapshot must track the resting order",
11277 );
11278
11279 self.rest_snapshots.insert(
11280 order.client_order_id(),
11281 self.live_main.keys().copied().collect(),
11282 );
11283 }
11284 }
11285 }
11286
11287 fn assert_tracked_orders_match_book(&self) {
11290 let level: HashMap<OrderId, QuantityRaw> = self
11291 .engine
11292 .book
11293 .get_orders_at_level(Price::from(Self::MAIN_PRICE), OrderSide::Buy)
11294 .iter()
11295 .map(|order| (order.order_id, order.size.raw()))
11296 .collect();
11297
11298 for (client_order_id, orders_ahead) in &self.engine.queue_ahead_orders {
11299 for (order_id, size_raw) in orders_ahead {
11300 let book_size = level.get(order_id).copied().unwrap_or_else(|| {
11301 panic!("tracked order {order_id} for {client_order_id} not in book level")
11302 });
11303 assert_eq!(
11304 book_size, *size_raw,
11305 "tracked size diverged from book for order {order_id}",
11306 );
11307 }
11308
11309 let tracked: HashSet<OrderId> = orders_ahead.keys().copied().collect();
11310 let expected: HashSet<OrderId> = self.rest_snapshots[client_order_id]
11311 .iter()
11312 .filter(|id| self.live_main.contains_key(id))
11313 .copied()
11314 .collect();
11315 assert_eq!(
11316 tracked, expected,
11317 "tracked set incomplete or stale for {client_order_id}",
11318 );
11319 }
11320 }
11321 }
11322
11323 #[rstest]
11324 fn prop_test_l3_queue_tracking_stays_synced_with_counter() {
11325 proptest!(|(events in prop::collection::vec(any_queue_event(), 1..=80))| {
11326 let mut sim = L3QueueSim::new();
11327 for event in events {
11328 sim.apply(event);
11329 assert_l3_queue_synced(&sim.engine);
11330 }
11331 });
11332 }
11333
11334 #[rstest]
11335 fn prop_test_l3_queue_tracking_mirrors_book_without_trades() {
11336 proptest!(|(events in prop::collection::vec(granular_queue_event(), 1..=80))| {
11337 let mut sim = L3QueueSim::new();
11338 for event in events {
11339 sim.apply(event);
11340 assert_l3_queue_synced(&sim.engine);
11341 sim.assert_tracked_orders_match_book();
11342 }
11343 });
11344 }
11345
11346 #[rstest]
11351 fn test_l3_queue_position_replay_databento_mbo_stays_synced() {
11352 let json = include_str!("../../../../test_data/databento/esh4-glbx-mdp3-20231225.mbo.json");
11353 let records: Vec<serde_json::Value> = serde_json::from_str(json).unwrap();
11354 assert!(records.len() > 1000);
11355
11356 let instrument = InstrumentAny::FuturesContract(futures_contract_es(None, None));
11357 let instrument_id = instrument.id();
11358 let (mut engine, cache) = get_l3_queue_engine(instrument);
11359 let account_id = AccountId::from("SIM-001");
11360
11361 let mut rested = 0usize;
11362 let mut trades = 0usize;
11363
11364 for (index, record) in records.iter().enumerate() {
11365 match record.get("type").and_then(serde_json::Value::as_str) {
11366 Some("OrderBookDelta") => {
11367 let mut delta: OrderBookDelta = serde_json::from_value(record.clone()).unwrap();
11368 delta.instrument_id = instrument_id;
11369 engine.process_order_book_delta(&delta).unwrap();
11370 }
11371 Some("TradeTick") => {
11372 let mut trade: TradeTick = serde_json::from_value(record.clone()).unwrap();
11373 trade.instrument_id = instrument_id;
11374 engine.process_trade_tick(&trade);
11375 trades += 1;
11376 }
11377 other => panic!("unexpected record type {other:?}"),
11378 }
11379
11380 if index % 150 == 100 {
11381 let (side, price) = if rested.is_multiple_of(2) {
11382 (OrderSide::Sell, engine.book.best_ask_price())
11383 } else {
11384 (OrderSide::Buy, engine.book.best_bid_price())
11385 };
11386
11387 if let Some(price) = price {
11388 rested += 1;
11389 let mut order = OrderTestBuilder::new(OrderType::Limit)
11390 .instrument_id(instrument_id)
11391 .side(side)
11392 .price(price)
11393 .quantity(Quantity::from("1"))
11394 .client_order_id(ClientOrderId::from(format!("O-MBO-{rested}").as_str()))
11395 .submit(true)
11396 .build();
11397 engine.process_order(&mut order, account_id);
11398
11399 let is_open = cache
11402 .borrow()
11403 .order(&order.client_order_id())
11404 .is_some_and(|order| order.is_open());
11405 if is_open {
11406 assert!(
11407 engine
11408 .queue_ahead_orders
11409 .contains_key(&order.client_order_id()),
11410 "L3 snapshot must track the resting order",
11411 );
11412 }
11413 }
11414 }
11415
11416 assert_l3_queue_synced(&engine);
11417 }
11418
11419 assert!(rested >= 5, "replay must exercise resting orders");
11420 assert!(trades >= 50, "replay must exercise trade interleavings");
11421 }
11422}