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