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