Skip to main content

nautilus_execution/matching_engine/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Order matching engine components for simulating trading venue behavior.
17
18pub mod config;
19pub mod ids_generator;
20pub mod inflight;
21
22mod settlement;
23
24use std::{
25    cell::RefCell,
26    cmp::min,
27    fmt::Debug,
28    mem,
29    ops::{Add, Sub},
30    rc::Rc,
31};
32
33use indexmap::{IndexMap, IndexSet};
34use jiff::SignedDuration;
35use nautilus_common::{
36    cache::Cache,
37    clock::Clock,
38    messages::execution::{
39        BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, ModifyOrder,
40    },
41    msgbus::{self, MessagingSwitchboard},
42};
43use nautilus_core::{UUID4, UnixNanos, correctness::CorrectnessResult};
44use nautilus_model::{
45    data::{
46        Bar, BarType, InstrumentClose, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick,
47        TradeTick,
48        order::{BookOrder, OrderId},
49    },
50    enums::{
51        AccountType, AggregationSource, AggressorSide, BookAction, BookType, ContingencyType,
52        InstrumentCloseType, LiquiditySide, MarketStatus, MarketStatusAction, OmsType, OrderSide,
53        OrderStatus, OrderType, PositionSide, PriceType, RecordFlag, TimeInForce, TriggerType,
54    },
55    events::{
56        OrderAccepted, OrderCancelRejected, OrderCanceled, OrderEventAny, OrderExpired,
57        OrderFilled, OrderModifyRejected, OrderRejected, OrderSubmitted, OrderTriggered,
58        OrderUpdated,
59    },
60    identifiers::{
61        AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId, Venue,
62        VenueOrderId,
63    },
64    instruments::{Instrument, InstrumentAny},
65    orderbook::{BookLevel, OrderBook},
66    orders::{MarketOrder, Order, OrderAny, OrderCore},
67    position::{Position, PositionReplayEvent},
68    types::{
69        Currency, Money, Price, Quantity,
70        fixed::{FIXED_PRECISION, raw_scales_match},
71        price::PriceRaw,
72        quantity::QuantityRaw,
73    },
74};
75use rust_decimal::Decimal;
76use ustr::Ustr;
77
78use self::{
79    config::OrderMatchingEngineConfig, ids_generator::IdsGenerator, inflight::InflightOrders,
80};
81use crate::{
82    matching_core::{MatchAction, OrderMatchingCore, RestingOrder},
83    models::{
84        fee::{FeeModel, FeeModelHandle},
85        fill::{FillModel, FillModelHandle, is_price_tradable},
86    },
87    protection::protection_price_calculate,
88    trailing::{store_trailing_stop_activation, trailing_stop_calculate},
89};
90
91/// An order matching engine for a single market.
92pub struct OrderMatchingEngine {
93    /// The venue for the matching engine.
94    pub venue: Venue,
95    /// The instrument for the matching engine.
96    pub instrument: InstrumentAny,
97    /// The instruments raw integer ID for the venue.
98    pub raw_id: u32,
99    /// The order book type for the matching engine.
100    pub book_type: BookType,
101    /// The order management system (OMS) type for the matching engine.
102    pub oms_type: OmsType,
103    /// The account type for the matching engine.
104    pub account_type: AccountType,
105    /// The market status for the matching engine.
106    pub market_status: MarketStatus,
107    /// The config for the matching engine.
108    pub config: OrderMatchingEngineConfig,
109    core: OrderMatchingCore,
110    clock: Rc<RefCell<dyn Clock>>,
111    cache: Rc<RefCell<Cache>>,
112    book: OrderBook,
113    fill_model: FillModelHandle,
114    fee_model: FeeModelHandle,
115    event_handler: Option<Rc<dyn Fn(OrderEventAny)>>,
116    inflight_orders: InflightOrders,
117    target_bid: Option<Price>,
118    target_ask: Option<Price>,
119    target_last: Option<Price>,
120    last_bar_bid: Option<Bar>,
121    last_bar_ask: Option<Bar>,
122    fill_at_market: bool,
123    execution_bar_types: IndexMap<InstrumentId, BarType>,
124    execution_bar_deltas: IndexMap<BarType, SignedDuration>,
125    account_ids: IndexMap<TraderId, AccountId>,
126    cached_filled_qty: IndexMap<ClientOrderId, Quantity>,
127    pending_updates: RefCell<IndexMap<ClientOrderId, Vec<OrderUpdated>>>,
128    pending_fills: IndexMap<TradeId, PendingFill>,
129    post_match_order_ids: IndexSet<ClientOrderId>,
130    rejected_oto_parent_ids: RefCell<IndexSet<ClientOrderId>>,
131    pending_oto_order_ids: IndexSet<ClientOrderId>,
132    other_instrument_oto_order_ids: IndexSet<ClientOrderId>,
133    canceled_oto_order_ids: IndexSet<ClientOrderId>,
134    ids_generator: IdsGenerator,
135    last_trade_size: Option<Quantity>,
136    l1_trade_size: Option<QuantityRaw>,
137    trade_consumption: QuantityRaw,
138    bid_consumption: IndexMap<PriceRaw, (QuantityRaw, QuantityRaw)>,
139    ask_consumption: IndexMap<PriceRaw, (QuantityRaw, QuantityRaw)>,
140    queue_pending: IndexMap<ClientOrderId, PriceRaw>,
141    queue_ahead_orders: IndexMap<ClientOrderId, IndexMap<OrderId, QuantityRaw>>,
142    queue_ahead_total: IndexMap<ClientOrderId, (PriceRaw, QuantityRaw)>,
143    queue_snapshot_in_progress: bool,
144    queue_ids_by_price: IndexMap<PriceRaw, IndexSet<ClientOrderId>>,
145    queue_excess: IndexMap<ClientOrderId, QuantityRaw>,
146    queue_id_scratch: Vec<ClientOrderId>,
147    queue_pending_scratch: Vec<(ClientOrderId, PriceRaw)>,
148    queue_stale_scratch: Vec<ClientOrderId>,
149    queue_entry_scratch: Vec<(ClientOrderId, QuantityRaw, QuantityRaw)>,
150    prev_bid_price_raw: PriceRaw,
151    prev_ask_price_raw: PriceRaw,
152    tob_initialized: bool,
153    last_quote_bid: Option<Price>,
154    last_quote_ask: Option<Price>,
155    precision_mismatch_streak: u32,
156    instrument_close: Option<InstrumentClose>,
157    pending_resolution: bool,
158    expiration_processed: bool,
159    option_settlement_failed: bool,
160    option_settlement_warning: Option<&'static str>,
161    option_expiration_orders_canceled: bool,
162}
163
164impl Debug for OrderMatchingEngine {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        f.debug_struct(stringify!(OrderMatchingEngine))
167            .field("venue", &self.venue)
168            .field("instrument", &self.instrument.id())
169            .finish()
170    }
171}
172
173impl OrderMatchingEngine {
174    /// Creates a new [`OrderMatchingEngine`] instance.
175    #[expect(clippy::too_many_arguments)]
176    pub fn new(
177        instrument: InstrumentAny,
178        raw_id: u32,
179        fill_model: FillModelHandle,
180        fee_model: FeeModelHandle,
181        book_type: BookType,
182        oms_type: OmsType,
183        account_type: AccountType,
184        clock: Rc<RefCell<dyn Clock>>,
185        cache: Rc<RefCell<Cache>>,
186        config: OrderMatchingEngineConfig,
187    ) -> Self {
188        let book = OrderBook::new(instrument.id(), book_type);
189        let mut core = OrderMatchingCore::new(instrument.id(), instrument.price_increment());
190        core.set_fill_limit_inside_spread(Self::fill_limit_inside_spread_or_false(&fill_model));
191        let ids_generator = IdsGenerator::new(
192            instrument.id().venue,
193            oms_type,
194            raw_id,
195            config.use_random_ids,
196            config.use_position_ids,
197            cache.clone(),
198        );
199
200        Self {
201            venue: instrument.id().venue,
202            instrument,
203            raw_id,
204            fill_model,
205            fee_model,
206            event_handler: None,
207            inflight_orders: InflightOrders::default(),
208            book_type,
209            oms_type,
210            account_type,
211            clock,
212            cache,
213            book,
214            market_status: MarketStatus::Open,
215            config,
216            core,
217            target_bid: None,
218            target_ask: None,
219            target_last: None,
220            last_bar_bid: None,
221            last_bar_ask: None,
222            fill_at_market: true,
223            execution_bar_types: IndexMap::new(),
224            execution_bar_deltas: IndexMap::new(),
225            account_ids: IndexMap::new(),
226            cached_filled_qty: IndexMap::new(),
227            pending_updates: RefCell::new(IndexMap::new()),
228            pending_fills: IndexMap::new(),
229            post_match_order_ids: IndexSet::new(),
230            rejected_oto_parent_ids: RefCell::new(IndexSet::new()),
231            pending_oto_order_ids: IndexSet::new(),
232            other_instrument_oto_order_ids: IndexSet::new(),
233            canceled_oto_order_ids: IndexSet::new(),
234            ids_generator,
235            last_trade_size: None,
236            l1_trade_size: None,
237            trade_consumption: 0,
238            bid_consumption: IndexMap::new(),
239            ask_consumption: IndexMap::new(),
240            queue_pending: IndexMap::new(),
241            queue_ahead_orders: IndexMap::new(),
242            queue_ahead_total: IndexMap::new(),
243            queue_snapshot_in_progress: false,
244            queue_ids_by_price: IndexMap::new(),
245            queue_excess: IndexMap::new(),
246            queue_id_scratch: Vec::new(),
247            queue_pending_scratch: Vec::new(),
248            queue_stale_scratch: Vec::new(),
249            queue_entry_scratch: Vec::new(),
250            prev_bid_price_raw: 0,
251            prev_ask_price_raw: 0,
252            tob_initialized: false,
253            last_quote_bid: None,
254            last_quote_ask: None,
255            precision_mismatch_streak: 0,
256            instrument_close: None,
257            pending_resolution: false,
258            expiration_processed: false,
259            option_settlement_failed: false,
260            option_settlement_warning: None,
261            option_expiration_orders_canceled: false,
262        }
263    }
264
265    /// Sets the event handler for dispatching order events.
266    ///
267    /// When set, events are routed through the handler instead of directly
268    /// through the message bus. This allows sandbox execution clients to
269    /// dispatch events through the async runner channel, avoiding `RefCell`
270    /// re-entrancy panics.
271    pub fn set_event_handler(&mut self, handler: Rc<dyn Fn(OrderEventAny)>) {
272        self.event_handler = Some(handler);
273    }
274
275    /// Attaches the venue's shared state for submits awaiting receipt.
276    pub fn set_inflight_orders(&mut self, orders: InflightOrders) {
277        self.inflight_orders = orders;
278    }
279
280    fn dispatch_order_event(&self, event: OrderEventAny) {
281        if let Some(handler) = &self.event_handler {
282            handler(event);
283        } else {
284            let endpoint = MessagingSwitchboard::exec_engine_process();
285            msgbus::send_order_event(endpoint, event);
286        }
287    }
288
289    /// Resets the matching engine to its initial state.
290    ///
291    /// Clears the order book, execution state, cached data, and resets all
292    /// internal components. This is typically used for backtesting scenarios
293    /// where the engine needs to be reset between test runs.
294    pub fn reset(&mut self) {
295        self.book.reset();
296        self.execution_bar_types.clear();
297        self.execution_bar_deltas.clear();
298        self.account_ids.clear();
299        self.cached_filled_qty.clear();
300        self.pending_updates.get_mut().clear();
301        self.pending_fills.clear();
302        self.post_match_order_ids.clear();
303        self.rejected_oto_parent_ids.get_mut().clear();
304        self.pending_oto_order_ids.clear();
305        self.other_instrument_oto_order_ids.clear();
306        self.canceled_oto_order_ids.clear();
307        self.core.reset();
308        self.target_bid = None;
309        self.target_ask = None;
310        self.target_last = None;
311        self.last_trade_size = None;
312        self.l1_trade_size = None;
313        self.trade_consumption = 0;
314        self.bid_consumption.clear();
315        self.ask_consumption.clear();
316        self.queue_pending.clear();
317        self.queue_ahead_orders.clear();
318        self.queue_ahead_total.clear();
319        self.queue_snapshot_in_progress = false;
320        self.queue_ids_by_price.clear();
321        self.queue_excess.clear();
322        self.queue_id_scratch.clear();
323        self.queue_pending_scratch.clear();
324        self.queue_stale_scratch.clear();
325        self.queue_entry_scratch.clear();
326        self.prev_bid_price_raw = 0;
327        self.prev_ask_price_raw = 0;
328        self.tob_initialized = false;
329        self.last_quote_bid = None;
330        self.last_quote_ask = None;
331        self.last_bar_bid = None;
332        self.last_bar_ask = None;
333        self.precision_mismatch_streak = 0;
334        self.instrument_close = None;
335        self.market_status = MarketStatus::Open;
336        self.pending_resolution = false;
337        self.expiration_processed = false;
338        self.option_settlement_failed = false;
339        self.option_settlement_warning = None;
340        self.option_expiration_orders_canceled = false;
341        self.fill_at_market = true;
342        self.ids_generator.reset();
343
344        log::info!("Reset {}", self.instrument.id());
345    }
346
347    fn apply_liquidity_consumption(
348        &mut self,
349        mut fills: Vec<(Price, Quantity)>,
350        order_side: OrderSide,
351        leaves_qty: Quantity,
352        book_prices: Option<&[Price]>,
353        trade_size: Option<QuantityRaw>,
354        mut journal: Option<&mut ConsumptionJournal>,
355    ) -> Vec<(Price, Quantity)> {
356        if !self.config.liquidity_consumption {
357            return fills;
358        }
359
360        let consumption = match order_side {
361            OrderSide::Buy => &mut self.ask_consumption,
362            OrderSide::Sell => &mut self.bid_consumption,
363        };
364
365        let mut adjusted_len = 0;
366        let mut remaining_qty = trade_size.map_or(leaves_qty.raw(), |trade_size| {
367            leaves_qty
368                .raw()
369                .min(trade_size.saturating_sub(self.trade_consumption))
370        });
371
372        for fill_idx in 0..fills.len() {
373            if remaining_qty == 0 {
374                break;
375            }
376
377            let (price, qty) = fills[fill_idx];
378
379            // Use book_price for consumption tracking (original price before MAKER adjustment),
380            // but use price (potentially adjusted) for the output fill.
381            let book_price = book_prices
382                .and_then(|bp| bp.get(fill_idx).copied())
383                .unwrap_or(price);
384
385            let book_price_raw = book_price.raw();
386            let level_size = self
387                .book
388                .get_quantity_at_level(book_price, order_side, qty.precision);
389
390            if let Some(journal) = journal.as_deref_mut() {
391                journal
392                    .levels
393                    .push((book_price_raw, consumption.get(&book_price_raw).copied()));
394            }
395
396            let (original_size, consumed) = consumption
397                .entry(book_price_raw)
398                .or_insert((level_size.raw(), 0));
399
400            // Reset consumption when book size changes (fresh data)
401            if *original_size != level_size.raw() {
402                *original_size = level_size.raw();
403                *consumed = 0;
404            }
405
406            let available = original_size.saturating_sub(*consumed);
407            if available == 0 {
408                continue;
409            }
410
411            let adjusted_qty_raw = min(min(qty.raw(), available), remaining_qty);
412            if adjusted_qty_raw == 0 {
413                continue;
414            }
415
416            *consumed += adjusted_qty_raw;
417            remaining_qty -= adjusted_qty_raw;
418
419            if trade_size.is_some() {
420                self.trade_consumption += adjusted_qty_raw;
421            }
422
423            let adjusted_qty = Quantity::from_raw(adjusted_qty_raw, qty.precision);
424            fills[adjusted_len] = (price, adjusted_qty);
425            adjusted_len += 1;
426        }
427
428        fills.truncate(adjusted_len);
429        fills
430    }
431
432    /// Starts a journal of the liquidity consumption a FOK fill attempt changes.
433    ///
434    /// Consumption is applied while fills are determined, before FOK completeness
435    /// is known, so a canceled FOK must revert the journal to leave the liquidity
436    /// available for subsequent orders. The journal records only the levels the
437    /// attempt crosses, so its cost does not grow with the consumption maps.
438    fn fok_consumption_journal(&self, order: &OrderAny) -> Option<ConsumptionJournal> {
439        if !self.config.liquidity_consumption || order.time_in_force() != TimeInForce::Fok {
440            return None;
441        }
442
443        Some(ConsumptionJournal {
444            order_side: order.order_side(),
445            trade_consumption: self.trade_consumption,
446            levels: Vec::new(),
447        })
448    }
449
450    fn revert_consumption_journal(&mut self, journal: ConsumptionJournal) {
451        self.trade_consumption = journal.trade_consumption;
452
453        let consumption = match journal.order_side {
454            OrderSide::Buy => &mut self.ask_consumption,
455            OrderSide::Sell => &mut self.bid_consumption,
456        };
457
458        // Undo in reverse so the earliest prior value of each level wins, and levels
459        // the attempt inserted are removed from the end, restoring the original order
460        for (price_raw, prior) in journal.levels.into_iter().rev() {
461            match prior {
462                Some(prior) => {
463                    consumption.insert(price_raw, prior);
464                }
465                None => {
466                    consumption.shift_remove(&price_raw);
467                }
468            }
469        }
470    }
471
472    /// Returns whether `fills` cannot fill a FOK order's entire leaves quantity.
473    fn is_fok_unfillable(&self, order: &OrderAny, fills: &[(Price, Quantity)]) -> bool {
474        if order.time_in_force() != TimeInForce::Fok {
475            return false;
476        }
477
478        let mut total_size = Quantity::zero(order.quantity().precision);
479
480        for &(fill_px, fill_qty) in fills {
481            if self
482                .normalize_price_for_current_instrument(fill_px)
483                .is_some()
484                && let Some(fill_qty) = self.normalize_quantity_for_current_instrument(fill_qty)
485            {
486                total_size = total_size.add(fill_qty);
487            }
488        }
489
490        order.leaves_qty() > total_size
491    }
492
493    fn seed_trade_consumption(
494        &mut self,
495        trade_price_raw: PriceRaw,
496        trade_size_raw: QuantityRaw,
497        trade_ts_event: UnixNanos,
498        aggressor_side: AggressorSide,
499    ) {
500        if trade_size_raw == 0 {
501            return;
502        }
503
504        // If the book was updated after the trade's event time, depth deltas
505        // already reflect this trade's consumed volume, skip to avoid double-counting
506        if self.book.ts_last > trade_ts_event {
507            return;
508        }
509
510        let book = &self.book;
511        let consumption = match aggressor_side {
512            AggressorSide::Buy => &mut self.ask_consumption,
513            AggressorSide::Sell => &mut self.bid_consumption,
514            AggressorSide::NoAggressor => return,
515        };
516
517        let mut remaining = trade_size_raw;
518
519        match aggressor_side {
520            AggressorSide::Buy => {
521                for level in book
522                    .asks(None)
523                    .take_while(|level| level.price.value.raw() <= trade_price_raw)
524                {
525                    Self::consume_trade_level(consumption, &mut remaining, level);
526                    if remaining == 0 {
527                        break;
528                    }
529                }
530            }
531            AggressorSide::Sell => {
532                for level in book
533                    .bids(None)
534                    .take_while(|level| level.price.value.raw() >= trade_price_raw)
535                {
536                    Self::consume_trade_level(consumption, &mut remaining, level);
537                    if remaining == 0 {
538                        break;
539                    }
540                }
541            }
542            AggressorSide::NoAggressor => unreachable!(),
543        }
544    }
545
546    fn consume_trade_level(
547        consumption: &mut IndexMap<PriceRaw, (QuantityRaw, QuantityRaw)>,
548        remaining: &mut QuantityRaw,
549        level: &BookLevel,
550    ) {
551        let level_size = level.size_raw();
552        let entry = consumption
553            .entry(level.price.value.raw())
554            .or_insert((level_size, 0));
555
556        // Reconcile stale level size to prevent reset in apply_liquidity_consumption
557        if entry.0 != level_size {
558            entry.0 = level_size;
559            entry.1 = 0;
560        }
561
562        let available = level_size.saturating_sub(entry.1);
563        let consume = min(*remaining, available);
564        entry.1 += consume;
565        *remaining -= consume;
566    }
567
568    /// Sets the fill model for the matching engine.
569    pub fn set_fill_model(&mut self, fill_model: FillModelHandle) {
570        self.core
571            .set_fill_limit_inside_spread(Self::fill_limit_inside_spread_or_false(&fill_model));
572        self.fill_model = fill_model;
573    }
574
575    fn fill_limit_inside_spread_or_false(fill_model: &FillModelHandle) -> bool {
576        fill_model.fill_limit_inside_spread().unwrap_or_else(|e| {
577            log::error!("Failed to query fill model spread behavior: {e}");
578            false
579        })
580    }
581
582    fn snapshot_queue_position(&mut self, order: &OrderAny, price: Price) {
583        if !self.config.queue_position {
584            return;
585        }
586        let size_prec = self.instrument.size_precision();
587
588        // Pass opposite side because get_quantity_at_level flips internally
589        // (BUY reads asks, SELL reads bids). We want the resting side depth.
590        let qty_ahead = self.book.get_quantity_at_level(
591            price,
592            OrderCore::opposite_side(order.order_side()),
593            size_prec,
594        );
595
596        let client_order_id = order.client_order_id();
597
598        self.remove_queue_position(client_order_id);
599        self.queue_ids_by_price
600            .entry(price.raw())
601            .or_default()
602            .insert(client_order_id);
603
604        // For L1 books, levels behind the BBO have no visible depth. Track
605        // these orders separately so fills are blocked until the BBO reaches
606        // this price. Only truly behind-BBO prices are pending (BUY below
607        // best bid / SELL above best ask); inside-spread and no-book keep 0.
608        if self.book_type == BookType::L1_MBP && qty_ahead.is_zero() {
609            let behind_bbo = match order.order_side() {
610                OrderSide::Buy => self.book.best_bid_price().is_some_and(|bid| price < bid),
611                OrderSide::Sell => self.book.best_ask_price().is_some_and(|ask| price > ask),
612            };
613
614            if behind_bbo {
615                self.queue_pending.insert(client_order_id, price.raw());
616                return;
617            }
618        }
619
620        self.queue_ahead_total
621            .insert(client_order_id, (price.raw(), qty_ahead.raw()));
622
623        // L3 books identify orders, so track which specific orders are ahead
624        if self.book_type == BookType::L3_MBO {
625            let orders_ahead: IndexMap<OrderId, QuantityRaw> = self
626                .book
627                .get_orders_at_level(price, OrderCore::opposite_side(order.order_side()))
628                .iter()
629                .map(|book_order| (book_order.order_id, book_order.size.raw()))
630                .collect();
631            self.queue_ahead_orders
632                .insert(client_order_id, orders_ahead);
633        }
634    }
635
636    fn remove_queue_position(&mut self, client_order_id: ClientOrderId) {
637        let pending_price = self.queue_pending.shift_remove(&client_order_id);
638        let ahead_price = self
639            .queue_ahead_total
640            .shift_remove(&client_order_id)
641            .map(|(price_raw, _)| price_raw);
642        self.queue_ahead_orders.shift_remove(&client_order_id);
643        self.queue_excess.shift_remove(&client_order_id);
644
645        for price_raw in [pending_price, ahead_price].into_iter().flatten() {
646            let remove_price = self
647                .queue_ids_by_price
648                .get_mut(&price_raw)
649                .is_some_and(|ids| {
650                    ids.shift_remove(&client_order_id);
651                    ids.is_empty()
652                });
653
654            if remove_price {
655                self.queue_ids_by_price.shift_remove(&price_raw);
656            }
657        }
658    }
659
660    fn take_queue_ids_at_price(&mut self, price_raw: PriceRaw) -> Vec<ClientOrderId> {
661        let mut ids = Self::take_cleared(&mut self.queue_id_scratch);
662        if let Some(tracked_ids) = self.queue_ids_by_price.get(&price_raw) {
663            ids.extend(tracked_ids.iter().copied());
664        }
665
666        ids
667    }
668
669    fn decrement_queue_on_trade(
670        &mut self,
671        price_raw: PriceRaw,
672        trade_size_raw: QuantityRaw,
673        aggressor_side: AggressorSide,
674    ) {
675        if !self.config.queue_position {
676            return;
677        }
678
679        self.queue_excess.clear();
680
681        let keys = self.take_queue_ids_at_price(price_raw);
682        let mut entries = Self::take_cleared(&mut self.queue_entry_scratch);
683        let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
684
685        for client_order_id in keys.iter().copied() {
686            let (order_price_raw, ahead_raw) =
687                match self.queue_ahead_total.get(&client_order_id).copied() {
688                    Some(v) => v,
689                    None => continue,
690                };
691
692            let cache = self.cache.borrow();
693            let order_info = cache.order(&client_order_id).and_then(|order| {
694                if order.is_closed() {
695                    return None;
696                }
697
698                let has_pending_updates =
699                    self.pending_updates.borrow().contains_key(&client_order_id);
700                let has_pending_fills = self
701                    .cached_filled_qty
702                    .get(&client_order_id)
703                    .is_some_and(|filled_qty| *filled_qty != order.filled_qty());
704                let snapshot;
705                let order = if has_pending_updates || has_pending_fills {
706                    snapshot = self.order_snapshot(client_order_id)?;
707                    &snapshot
708                } else {
709                    &order
710                };
711
712                Some((order.order_side(), order.leaves_qty().raw()))
713            });
714            drop(cache);
715
716            let Some((order_side, leaves_raw)) = order_info else {
717                stale.push(client_order_id);
718                continue;
719            };
720
721            if order_price_raw != price_raw || ahead_raw == 0 {
722                continue;
723            }
724
725            let should_decrement = matches!(aggressor_side, AggressorSide::NoAggressor)
726                || (aggressor_side == AggressorSide::Buy && order_side == OrderSide::Sell)
727                || (aggressor_side == AggressorSide::Sell && order_side == OrderSide::Buy);
728
729            if should_decrement {
730                entries.push((client_order_id, ahead_raw, leaves_raw));
731            }
732        }
733
734        for id in stale.drain(..) {
735            self.remove_queue_position(id);
736        }
737
738        // Sort by queue position (earliest first) for shared budget allocation
739        entries.sort_by_key(|&(_, ahead, _)| ahead);
740
741        let mut remaining = trade_size_raw;
742        let mut prev_position: QuantityRaw = 0;
743
744        for (client_order_id, ahead_raw, leaves_raw) in &entries {
745            if remaining == 0 {
746                let new_ahead = ahead_raw.saturating_sub(trade_size_raw);
747                self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, new_ahead);
748                if new_ahead == 0 {
749                    // Queue cleared but no trade volume left for this order
750                    self.queue_excess.insert(*client_order_id, 0);
751                }
752                continue;
753            }
754
755            // Consume the gap between previous position and this order's depth
756            let gap = ahead_raw.saturating_sub(prev_position);
757            let queue_consumed = remaining.min(gap);
758            remaining -= queue_consumed;
759
760            if remaining == 0 && queue_consumed < gap {
761                let new_ahead = ahead_raw.saturating_sub(trade_size_raw);
762                self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, new_ahead);
763                continue;
764            }
765
766            self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, 0);
767            let excess = remaining.min(*leaves_raw);
768            self.queue_excess.insert(*client_order_id, excess);
769            remaining -= excess;
770            prev_position = ahead_raw + excess;
771        }
772
773        self.queue_id_scratch = keys;
774        self.queue_entry_scratch = entries;
775        self.queue_stale_scratch = stale;
776    }
777
778    /// Reduces an order's quantity ahead, front-consuming its tracked orders by
779    /// the same amount so the pair stays in sync and later granular deltas for
780    /// consumed orders cannot advance the queue again.
781    fn reduce_queue_ahead(
782        &mut self,
783        client_order_id: ClientOrderId,
784        price_raw: PriceRaw,
785        ahead_raw: QuantityRaw,
786        new_ahead_raw: QuantityRaw,
787    ) {
788        self.queue_ahead_total
789            .insert(client_order_id, (price_raw, new_ahead_raw));
790        self.consume_queue_ahead_orders(client_order_id, ahead_raw.saturating_sub(new_ahead_raw));
791    }
792
793    /// Front-consumes (FIFO) the tracked orders in step with `queue_ahead_total`.
794    fn consume_queue_ahead_orders(
795        &mut self,
796        client_order_id: ClientOrderId,
797        mut amount_raw: QuantityRaw,
798    ) {
799        let Some(orders_ahead) = self.queue_ahead_orders.get_mut(&client_order_id) else {
800            return;
801        };
802
803        while amount_raw > 0 {
804            let Some((&book_order_id, &size_raw)) = orders_ahead.get_index(0) else {
805                break;
806            };
807
808            if size_raw <= amount_raw {
809                orders_ahead.shift_remove(&book_order_id);
810                amount_raw -= size_raw;
811            } else {
812                orders_ahead.insert(book_order_id, size_raw - amount_raw);
813                amount_raw = 0;
814            }
815        }
816    }
817
818    fn determine_trade_fill_qty(&self, order: &OrderAny) -> Option<QuantityRaw> {
819        if !self.config.queue_position {
820            return Some(order.leaves_qty().raw());
821        }
822
823        let client_order_id = order.client_order_id();
824
825        // Block fills for L1 orders pending a deferred snapshot
826        if self.queue_pending.contains_key(&client_order_id) {
827            return None;
828        }
829
830        if let Some(&(tracked_price_raw, ahead_raw)) = self.queue_ahead_total.get(&client_order_id)
831            && let Some(order_price) = order.price()
832            && order_price.raw() == tracked_price_raw
833            && ahead_raw > 0
834        {
835            return None;
836        }
837
838        let leaves_raw = order.leaves_qty().raw();
839        if leaves_raw == 0 {
840            return None;
841        }
842
843        let mut available_raw = leaves_raw;
844
845        // Cap by remaining trade volume and queue excess (only during trade processing)
846        if let Some(trade_size) = self.last_trade_size {
847            let remaining = trade_size.raw().saturating_sub(self.trade_consumption);
848            available_raw = available_raw.min(remaining);
849
850            if let Some(&excess_raw) = self.queue_excess.get(&client_order_id) {
851                if excess_raw == 0 {
852                    return None;
853                }
854                available_raw = available_raw.min(excess_raw);
855            }
856        }
857
858        if available_raw == 0 {
859            return None;
860        }
861
862        Some(available_raw)
863    }
864
865    /// Rebases queue positions after a full book replacement.
866    ///
867    /// A snapshot does not imply that all displayed liquidity ahead of a
868    /// simulated order disappeared. Preserve the old estimate, capped by the
869    /// newly visible quantity at that price. For L3 books, retain only the
870    /// previously tracked orders that are still present in the replacement.
871    fn rebase_queue_positions(&mut self) {
872        if !self.config.queue_position {
873            return;
874        }
875
876        let tracked: Vec<_> = self
877            .queue_ahead_total
878            .iter()
879            .map(|(&client_order_id, &(price_raw, ahead_raw))| {
880                (client_order_id, price_raw, ahead_raw)
881            })
882            .collect();
883        let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
884        let size_precision = self.instrument.size_precision();
885        let price_precision = self.instrument.price_precision();
886
887        for (client_order_id, price_raw, ahead_raw) in tracked {
888            let order_side = self
889                .cache
890                .borrow()
891                .order(&client_order_id)
892                .and_then(|order| {
893                    if order.is_closed() {
894                        None
895                    } else {
896                        Some(order.order_side())
897                    }
898                });
899
900            let Some(order_side) = order_side else {
901                stale.push(client_order_id);
902                continue;
903            };
904
905            let price = Price::from_raw(price_raw, price_precision);
906            let visible_raw = self
907                .book
908                .get_quantity_at_level(price, OrderCore::opposite_side(order_side), size_precision)
909                .raw();
910            let rebased_raw = ahead_raw.min(visible_raw);
911
912            if self.book_type == BookType::L3_MBO {
913                let previous_orders = self
914                    .queue_ahead_orders
915                    .get(&client_order_id)
916                    .cloned()
917                    .unwrap_or_default();
918                let mut orders_ahead = IndexMap::new();
919                let mut total_raw = 0;
920
921                for book_order in self
922                    .book
923                    .get_orders_at_level(price, OrderCore::opposite_side(order_side))
924                {
925                    if !previous_orders.contains_key(&book_order.order_id) {
926                        continue;
927                    }
928
929                    let previous_size_raw = previous_orders[&book_order.order_id];
930                    let size_raw = previous_size_raw.min(book_order.size.raw());
931                    orders_ahead.insert(book_order.order_id, size_raw);
932                    total_raw += size_raw;
933                }
934
935                self.queue_ahead_orders
936                    .insert(client_order_id, orders_ahead);
937                self.queue_ahead_total
938                    .insert(client_order_id, (price_raw, total_raw));
939            } else {
940                self.queue_ahead_total
941                    .insert(client_order_id, (price_raw, rebased_raw));
942            }
943        }
944
945        for client_order_id in stale.drain(..) {
946            self.remove_queue_position(client_order_id);
947        }
948
949        self.queue_stale_scratch = stale;
950    }
951
952    fn adjust_queue_for_delta(&mut self, delta: &OrderBookDelta) {
953        if delta.action == BookAction::Delete {
954            if self.is_order_granular_delta(delta.flags) {
955                self.advance_l3_queue_on_delete(delta.order.order_id);
956            } else {
957                self.clear_queue_on_delete(delta.order.price.raw(), delta.order.side);
958            }
959        } else if delta.action == BookAction::Update {
960            if self.is_order_granular_delta(delta.flags) {
961                self.adjust_l3_queue_on_update(&delta.order);
962            } else {
963                self.cap_queue_ahead(
964                    delta.order.price.raw(),
965                    delta.order.size.raw(),
966                    delta.order.side,
967                );
968            }
969        }
970    }
971
972    fn clear_queue_on_delete(
973        &mut self,
974        deleted_price_raw: PriceRaw,
975        deleted_side: Option<OrderSide>,
976    ) {
977        let keys = self.take_queue_ids_at_price(deleted_price_raw);
978        for client_order_id in keys.iter().copied() {
979            if let Some(&(order_price_raw, ahead_raw)) =
980                self.queue_ahead_total.get(&client_order_id)
981                && order_price_raw == deleted_price_raw
982            {
983                let matches_side = self
984                    .cache
985                    .borrow()
986                    .order(&client_order_id)
987                    .is_some_and(|o| Some(o.order_side()) == deleted_side);
988
989                if matches_side {
990                    self.reduce_queue_ahead(client_order_id, order_price_raw, ahead_raw, 0);
991                }
992            }
993        }
994
995        self.queue_id_scratch = keys;
996    }
997
998    /// Returns `true` when the delta identifies a single book order (pure MBO);
999    /// TOB/MBP-flagged deltas use level-wide handling instead.
1000    fn is_order_granular_delta(&self, flags: u8) -> bool {
1001        self.book_type == BookType::L3_MBO
1002            && !RecordFlag::F_TOB.matches(flags)
1003            && !RecordFlag::F_MBP.matches(flags)
1004    }
1005
1006    fn advance_l3_queue_on_delete(&mut self, book_order_id: OrderId) {
1007        for (client_order_id, orders_ahead) in &mut self.queue_ahead_orders {
1008            let Some(size_raw) = orders_ahead.shift_remove(&book_order_id) else {
1009                continue;
1010            };
1011
1012            if let Some((_, ahead_raw)) = self.queue_ahead_total.get_mut(client_order_id) {
1013                *ahead_raw = ahead_raw.saturating_sub(size_raw);
1014            }
1015        }
1016    }
1017
1018    /// Adjusts tracked queues for a per-order update. A size decrease retains
1019    /// time priority and advances the queue by the difference. A size increase
1020    /// keeps its book FIFO slot, so it stays ahead with the larger size
1021    /// (pessimistic versus venues that demote, but consistent with the book
1022    /// that later snapshots read). A price move leaves the level.
1023    fn adjust_l3_queue_on_update(&mut self, book_order: &BookOrder) {
1024        for (client_order_id, orders_ahead) in &mut self.queue_ahead_orders {
1025            let Some(&tracked_size_raw) = orders_ahead.get(&book_order.order_id) else {
1026                continue;
1027            };
1028            let Some((tracked_price_raw, ahead_raw)) =
1029                self.queue_ahead_total.get_mut(client_order_id)
1030            else {
1031                continue;
1032            };
1033
1034            if book_order.price.raw() != *tracked_price_raw {
1035                *ahead_raw = ahead_raw.saturating_sub(tracked_size_raw);
1036                orders_ahead.shift_remove(&book_order.order_id);
1037            } else if book_order.size.raw() < tracked_size_raw {
1038                // Size decrease retains time priority
1039                *ahead_raw = ahead_raw.saturating_sub(tracked_size_raw - book_order.size.raw());
1040                orders_ahead.insert(book_order.order_id, book_order.size.raw());
1041            } else if book_order.size.raw() > tracked_size_raw {
1042                *ahead_raw = ahead_raw.saturating_add(book_order.size.raw() - tracked_size_raw);
1043                orders_ahead.insert(book_order.order_id, book_order.size.raw());
1044            }
1045        }
1046    }
1047
1048    fn cap_queue_ahead(
1049        &mut self,
1050        price_raw: PriceRaw,
1051        size_raw: QuantityRaw,
1052        order_side: Option<OrderSide>,
1053    ) {
1054        let keys = self.take_queue_ids_at_price(price_raw);
1055        let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
1056
1057        for client_order_id in keys.iter().copied() {
1058            let (order_price_raw, ahead_raw) =
1059                match self.queue_ahead_total.get(&client_order_id).copied() {
1060                    Some(v) => v,
1061                    None => continue,
1062                };
1063
1064            if order_price_raw != price_raw || ahead_raw <= size_raw {
1065                continue;
1066            }
1067
1068            let cache = self.cache.borrow();
1069            let order_info = cache.order(&client_order_id).and_then(|order| {
1070                if order.is_closed() {
1071                    None
1072                } else {
1073                    Some(order.order_side())
1074                }
1075            });
1076            drop(cache);
1077
1078            let Some(side) = order_info else {
1079                stale.push(client_order_id);
1080                continue;
1081            };
1082
1083            if Some(side) != order_side {
1084                continue;
1085            }
1086
1087            self.reduce_queue_ahead(client_order_id, order_price_raw, ahead_raw, size_raw);
1088        }
1089
1090        for id in stale.drain(..) {
1091            self.remove_queue_position(id);
1092        }
1093
1094        self.queue_id_scratch = keys;
1095        self.queue_stale_scratch = stale;
1096    }
1097
1098    fn seed_tob_baseline(&mut self) {
1099        let bid = self.book.best_bid_price();
1100        let ask = self.book.best_ask_price();
1101        self.prev_bid_price_raw = bid.map_or(0, |p| p.raw());
1102        self.prev_ask_price_raw = ask.map_or(0, |p| p.raw());
1103        self.tob_initialized = bid.is_some() || ask.is_some();
1104    }
1105
1106    fn decrement_l1_queue_on_quote(
1107        &mut self,
1108        bid_price_raw: PriceRaw,
1109        bid_size_raw: QuantityRaw,
1110        ask_price_raw: PriceRaw,
1111        ask_size_raw: QuantityRaw,
1112    ) {
1113        if !self.config.queue_position {
1114            return;
1115        }
1116
1117        // Price-move detection requires a valid prior TOB snapshot
1118        if self.tob_initialized {
1119            // BID side (BUY limit orders): handle price drops (crossed/snapshot)
1120            if bid_price_raw < self.prev_bid_price_raw {
1121                self.adjust_l1_queue_on_price_move(bid_price_raw, bid_size_raw, OrderSide::Buy);
1122            }
1123
1124            // ASK side (SELL limit orders): handle price rises (crossed/snapshot)
1125            if ask_price_raw > self.prev_ask_price_raw {
1126                self.adjust_l1_queue_on_price_move(ask_price_raw, ask_size_raw, OrderSide::Sell);
1127            }
1128        }
1129
1130        // Resolve pending snapshots when BBO reaches a tracked order's price
1131        self.resolve_pending_l1_snapshots(bid_price_raw, bid_size_raw, ask_price_raw, ask_size_raw);
1132        self.cap_queue_ahead(bid_price_raw, bid_size_raw, Some(OrderSide::Buy));
1133        self.cap_queue_ahead(ask_price_raw, ask_size_raw, Some(OrderSide::Sell));
1134    }
1135
1136    fn adjust_l1_queue_on_price_move(
1137        &mut self,
1138        new_price_raw: PriceRaw,
1139        new_size_raw: QuantityRaw,
1140        order_side: OrderSide,
1141    ) {
1142        let mut keys = Self::take_cleared(&mut self.queue_id_scratch);
1143        keys.extend(self.queue_ahead_total.keys().copied());
1144        let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
1145
1146        for client_order_id in keys.iter().copied() {
1147            let Some(&(order_price_raw, ahead_raw)) = self.queue_ahead_total.get(&client_order_id)
1148            else {
1149                continue;
1150            };
1151
1152            let cache = self.cache.borrow();
1153            let order_info = cache.order(&client_order_id).and_then(|order| {
1154                if order.is_closed() {
1155                    None
1156                } else {
1157                    Some(order.order_side())
1158                }
1159            });
1160            drop(cache);
1161
1162            let Some(side) = order_info else {
1163                stale.push(client_order_id);
1164                continue;
1165            };
1166
1167            if side != order_side {
1168                continue;
1169            }
1170
1171            // BUY orders crossed when bid drops below order price
1172            // SELL orders crossed when ask rises above order price
1173            let crossed = match order_side {
1174                OrderSide::Buy => order_price_raw > new_price_raw,
1175                _ => order_price_raw < new_price_raw,
1176            };
1177
1178            if crossed {
1179                self.queue_ahead_total
1180                    .insert(client_order_id, (order_price_raw, 0));
1181            } else if order_price_raw == new_price_raw && ahead_raw > new_size_raw {
1182                self.queue_ahead_total
1183                    .insert(client_order_id, (order_price_raw, new_size_raw));
1184            }
1185        }
1186
1187        for id in stale.drain(..) {
1188            self.remove_queue_position(id);
1189        }
1190
1191        let mut pending = Self::take_cleared(&mut self.queue_pending_scratch);
1192        pending.extend(
1193            self.queue_pending
1194                .iter()
1195                .map(|(&client_order_id, &price_raw)| (client_order_id, price_raw)),
1196        );
1197
1198        for (client_order_id, order_price_raw) in pending.iter().copied() {
1199            let cache = self.cache.borrow();
1200            let order_info = cache.order(&client_order_id).and_then(|order| {
1201                if order.is_closed() {
1202                    None
1203                } else {
1204                    Some(order.order_side())
1205                }
1206            });
1207            drop(cache);
1208
1209            let Some(side) = order_info else {
1210                stale.push(client_order_id);
1211                continue;
1212            };
1213
1214            if side != order_side {
1215                continue;
1216            }
1217
1218            let crossed = match order_side {
1219                OrderSide::Buy => order_price_raw > new_price_raw,
1220                _ => order_price_raw < new_price_raw,
1221            };
1222
1223            if crossed {
1224                self.queue_pending.shift_remove(&client_order_id);
1225                self.queue_ahead_total
1226                    .insert(client_order_id, (order_price_raw, 0));
1227            } else if order_price_raw == new_price_raw {
1228                self.queue_pending.shift_remove(&client_order_id);
1229                self.queue_ahead_total
1230                    .insert(client_order_id, (order_price_raw, new_size_raw));
1231            }
1232        }
1233
1234        for id in stale.drain(..) {
1235            self.remove_queue_position(id);
1236        }
1237
1238        self.queue_id_scratch = keys;
1239        self.queue_pending_scratch = pending;
1240        self.queue_stale_scratch = stale;
1241    }
1242
1243    fn resolve_pending_l1_snapshots(
1244        &mut self,
1245        bid_price_raw: PriceRaw,
1246        bid_size_raw: QuantityRaw,
1247        ask_price_raw: PriceRaw,
1248        ask_size_raw: QuantityRaw,
1249    ) {
1250        let mut keys = self.take_queue_ids_at_price(bid_price_raw);
1251        if ask_price_raw != bid_price_raw
1252            && let Some(ask_ids) = self.queue_ids_by_price.get(&ask_price_raw)
1253        {
1254            keys.extend(ask_ids.iter().copied());
1255        }
1256
1257        let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
1258
1259        for client_order_id in keys.iter().copied() {
1260            let Some(&order_price_raw) = self.queue_pending.get(&client_order_id) else {
1261                continue;
1262            };
1263
1264            let cache = self.cache.borrow();
1265            let order_info = cache.order(&client_order_id).and_then(|order| {
1266                if order.is_closed() {
1267                    None
1268                } else {
1269                    Some(order.order_side())
1270                }
1271            });
1272            drop(cache);
1273
1274            let Some(side) = order_info else {
1275                stale.push(client_order_id);
1276                continue;
1277            };
1278
1279            // Initialize snapshot when BBO reaches the order's price level
1280            let matched_size = match side {
1281                OrderSide::Buy if order_price_raw == bid_price_raw => Some(bid_size_raw),
1282                OrderSide::Sell if order_price_raw == ask_price_raw => Some(ask_size_raw),
1283                _ => None,
1284            };
1285
1286            if let Some(size) = matched_size {
1287                self.queue_pending.shift_remove(&client_order_id);
1288                self.queue_ahead_total
1289                    .insert(client_order_id, (order_price_raw, size));
1290            }
1291        }
1292
1293        for id in stale.drain(..) {
1294            self.remove_queue_position(id);
1295        }
1296
1297        self.queue_id_scratch = keys;
1298        self.queue_stale_scratch = stale;
1299    }
1300
1301    fn resolve_pending_on_trade(&mut self, trade_price_raw: PriceRaw) {
1302        let mut keys = Self::take_cleared(&mut self.queue_id_scratch);
1303        keys.extend(self.queue_pending.keys().copied());
1304        let mut stale = Self::take_cleared(&mut self.queue_stale_scratch);
1305
1306        for client_order_id in keys.iter().copied() {
1307            let Some(&order_price_raw) = self.queue_pending.get(&client_order_id) else {
1308                continue;
1309            };
1310
1311            let cache = self.cache.borrow();
1312            let order_side = cache.order(&client_order_id).and_then(|order| {
1313                if order.is_closed() {
1314                    None
1315                } else {
1316                    Some(order.order_side())
1317                }
1318            });
1319            drop(cache);
1320
1321            let Some(side) = order_side else {
1322                stale.push(client_order_id);
1323                continue;
1324            };
1325
1326            // Trade through a pending level proves the queue was crossed
1327            let crossed = match side {
1328                OrderSide::Buy => trade_price_raw < order_price_raw,
1329                OrderSide::Sell => trade_price_raw > order_price_raw,
1330            };
1331
1332            if crossed {
1333                self.queue_pending.shift_remove(&client_order_id);
1334                self.queue_ahead_total
1335                    .insert(client_order_id, (order_price_raw, 0));
1336            }
1337        }
1338
1339        for id in stale.drain(..) {
1340            self.remove_queue_position(id);
1341        }
1342
1343        self.queue_id_scratch = keys;
1344        self.queue_stale_scratch = stale;
1345    }
1346
1347    fn take_cleared<T>(buf: &mut Vec<T>) -> Vec<T> {
1348        let mut items = mem::take(buf);
1349        items.clear();
1350        items
1351    }
1352
1353    #[must_use]
1354    /// Returns the best bid price from the order book.
1355    pub fn best_bid_price(&self) -> Option<Price> {
1356        self.book.best_bid_price()
1357    }
1358
1359    #[must_use]
1360    /// Returns the best ask price from the order book.
1361    pub fn best_ask_price(&self) -> Option<Price> {
1362        self.book.best_ask_price()
1363    }
1364
1365    #[must_use]
1366    /// Returns a reference to the internal order book.
1367    pub const fn get_book(&self) -> &OrderBook {
1368        &self.book
1369    }
1370
1371    #[must_use]
1372    /// Returns all open bid orders managed by the matching core.
1373    pub fn get_open_bid_orders(&self) -> Vec<RestingOrder> {
1374        self.core.get_orders_bid()
1375    }
1376
1377    #[must_use]
1378    /// Returns all open ask orders managed by the matching core.
1379    pub fn get_open_ask_orders(&self) -> Vec<RestingOrder> {
1380        self.core.get_orders_ask()
1381    }
1382
1383    #[must_use]
1384    /// Returns all open orders from both bid and ask sides.
1385    pub fn get_open_orders(&self) -> Vec<RestingOrder> {
1386        self.core.get_orders()
1387    }
1388
1389    #[must_use]
1390    /// Returns true if an order with the given client order ID exists in the matching engine.
1391    pub fn order_exists(&self, client_order_id: ClientOrderId) -> bool {
1392        self.core.order_exists(client_order_id)
1393    }
1394
1395    #[must_use]
1396    /// Returns the number of partial-fill counters tracked by the engine.
1397    pub fn cached_filled_qty_len(&self) -> usize {
1398        self.cached_filled_qty.len()
1399    }
1400
1401    #[must_use]
1402    pub const fn get_core(&self) -> &OrderMatchingCore {
1403        &self.core
1404    }
1405
1406    pub fn set_fill_at_market(&mut self, value: bool) {
1407        self.fill_at_market = value;
1408    }
1409
1410    /// Updates the instrument definition used by this matching engine.
1411    ///
1412    /// # Errors
1413    ///
1414    /// Returns an error if `instrument.id()` does not match this engines instrument ID.
1415    pub fn update_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> {
1416        if instrument.id() != self.instrument.id() {
1417            anyhow::bail!(
1418                "Cannot update instrument {} with {}",
1419                self.instrument.id(),
1420                instrument.id()
1421            );
1422        }
1423
1424        let changed = instrument.price_increment() != self.instrument.price_increment()
1425            || instrument.price_precision() != self.instrument.price_precision()
1426            || instrument.size_precision() != self.instrument.size_precision();
1427
1428        if changed {
1429            self.core
1430                .update_price_increment(instrument.price_increment());
1431            self.book.reset();
1432            self.l1_trade_size = None;
1433            self.trade_consumption = 0;
1434            self.bid_consumption.clear();
1435            self.ask_consumption.clear();
1436            self.queue_pending.clear();
1437            self.queue_ahead_orders.clear();
1438            self.queue_ahead_total.clear();
1439            self.queue_ids_by_price.clear();
1440            self.queue_excess.clear();
1441            self.prev_bid_price_raw = 0;
1442            self.prev_ask_price_raw = 0;
1443            self.tob_initialized = false;
1444            self.last_quote_bid = None;
1445            self.last_quote_ask = None;
1446            self.precision_mismatch_streak = 0;
1447            self.target_bid = None;
1448            self.target_ask = None;
1449            self.target_last = None;
1450            self.last_bar_bid = None;
1451            self.last_bar_ask = None;
1452            self.core.bid = None;
1453            self.core.ask = None;
1454            self.core.last = None;
1455            log::info!(
1456                "Updated instrument {} (price_precision={} size_precision={})",
1457                instrument.id(),
1458                instrument.price_precision(),
1459                instrument.size_precision()
1460            );
1461        }
1462
1463        self.instrument = instrument;
1464
1465        if changed {
1466            self.drop_incompatible_core_orders();
1467        }
1468
1469        Ok(())
1470    }
1471
1472    fn check_price_precision(&self, actual: u8, field: &str) -> anyhow::Result<()> {
1473        let expected = self.instrument.price_precision();
1474        if actual != expected {
1475            anyhow::bail!(
1476                "Invalid {field} precision {actual}, expected {expected} for {}",
1477                self.instrument.id()
1478            );
1479        }
1480        Ok(())
1481    }
1482
1483    fn check_size_precision(&self, actual: u8, field: &str) -> anyhow::Result<()> {
1484        let expected = self.instrument.size_precision();
1485        if actual != expected {
1486            anyhow::bail!(
1487                "Invalid {field} precision {actual}, expected {expected} for {}",
1488                self.instrument.id()
1489            );
1490        }
1491        Ok(())
1492    }
1493
1494    fn log_precision_mismatch(
1495        &mut self,
1496        data_type: &str,
1497        instrument_id: InstrumentId,
1498        err: &anyhow::Error,
1499    ) {
1500        self.precision_mismatch_streak = self.precision_mismatch_streak.saturating_add(1);
1501        let streak = self.precision_mismatch_streak;
1502
1503        if streak <= 3 || streak.is_multiple_of(100) {
1504            log::warn!(
1505                "Skipping {data_type} for {instrument_id}: {err} \
1506                 (consecutive_precision_mismatches={streak})"
1507            );
1508        }
1509
1510        if streak == 20 {
1511            log::error!(
1512                "Precision mismatches reached {streak} consecutive events for \
1513                 {instrument_id}; check instrument update flow and upstream market data"
1514            );
1515        }
1516    }
1517
1518    fn drop_incompatible_core_orders(&mut self) {
1519        let client_order_ids: Vec<ClientOrderId> = self
1520            .core
1521            .iter_orders()
1522            .filter(|order| {
1523                !self.resting_order_matches_current_instrument(order)
1524                    || !self.cached_order_matches_current_instrument(order.client_order_id)
1525            })
1526            .map(|order| order.client_order_id)
1527            .collect();
1528
1529        for client_order_id in client_order_ids {
1530            let order = self
1531                .cache
1532                .borrow()
1533                .order(&client_order_id)
1534                .map(|o| o.clone());
1535
1536            if let Some(order) = order
1537                && (order.is_inflight() || order.is_open())
1538            {
1539                log::warn!(
1540                    "Canceling order {client_order_id} after instrument update: \
1541                     price, trigger price, or quantity is not compatible with {}",
1542                    self.instrument.id()
1543                );
1544                self.cancel_order(&order, None);
1545            } else {
1546                self.delete_core_order(client_order_id);
1547                self.cached_filled_qty.swap_remove(&client_order_id);
1548            }
1549        }
1550    }
1551
1552    fn cached_order_matches_current_instrument(&self, client_order_id: ClientOrderId) -> bool {
1553        self.cache
1554            .borrow()
1555            .order(&client_order_id)
1556            .is_none_or(|order| {
1557                Self::quantity_matches_precision(order.quantity(), self.instrument.size_precision())
1558            })
1559    }
1560
1561    fn resting_order_matches_current_instrument(&self, order: &RestingOrder) -> bool {
1562        order
1563            .limit_price
1564            .is_none_or(|price| self.price_matches_current_instrument(price))
1565            && order
1566                .trigger_price
1567                .is_none_or(|price| self.price_matches_current_instrument(price))
1568    }
1569
1570    fn price_matches_current_instrument(&self, price: Price) -> bool {
1571        Self::price_matches_precision(price, self.instrument.price_precision())
1572            && Self::price_matches_tick(price, self.instrument.price_increment())
1573    }
1574
1575    fn price_matches_precision(price: Price, precision: u8) -> bool {
1576        let precision_diff = FIXED_PRECISION.saturating_sub(precision);
1577        let scale = PriceRaw::pow(10, u32::from(precision_diff));
1578        price.raw() % scale == 0
1579    }
1580
1581    fn price_matches_tick(price: Price, increment: Price) -> bool {
1582        let increment_raw = increment.raw().abs();
1583        increment_raw == 0 || price.raw() % increment_raw == 0
1584    }
1585
1586    fn quantity_matches_precision(quantity: Quantity, precision: u8) -> bool {
1587        let precision_diff = FIXED_PRECISION.saturating_sub(precision);
1588        let scale = QuantityRaw::pow(10, u32::from(precision_diff));
1589        quantity.raw().is_multiple_of(scale)
1590    }
1591
1592    fn normalize_price_for_current_instrument(&self, price: Price) -> Option<Price> {
1593        if !self.price_matches_current_instrument(price) {
1594            return None;
1595        }
1596
1597        Some(Price::from_raw(
1598            price.raw(),
1599            self.instrument.price_precision(),
1600        ))
1601    }
1602
1603    fn normalize_quantity_for_current_instrument(&self, quantity: Quantity) -> Option<Quantity> {
1604        let precision = self.instrument.size_precision();
1605        if !Self::quantity_matches_precision(quantity, precision) {
1606            return None;
1607        }
1608
1609        Some(Quantity::from_raw(quantity.raw(), precision))
1610    }
1611
1612    /// Process the venues market for the given order book delta.
1613    ///
1614    /// # Errors
1615    ///
1616    /// Returns an error if:
1617    /// - Delta order price precision does not match the instrument (for Add/Update actions).
1618    /// - Delta order size precision does not match the instrument (for Add/Update actions).
1619    /// - Applying the delta to the book fails.
1620    pub fn process_order_book_delta(&mut self, delta: &OrderBookDelta) -> anyhow::Result<()> {
1621        log::debug!("Processing {delta}");
1622
1623        // Validate precision for Add and Update actions (Delete/Clear may have NULL_ORDER)
1624        if matches!(delta.action, BookAction::Add | BookAction::Update) {
1625            self.check_price_precision(delta.order.price.precision, "delta order price")?;
1626            self.check_size_precision(delta.order.size.precision, "delta order size")?;
1627        }
1628
1629        // L1 books are driven by top-of-book data only, ignore deltas
1630        if self.book_type == BookType::L1_MBP {
1631            self.iterate(delta.ts_init, AggressorSide::NoAggressor);
1632            return Ok(());
1633        }
1634
1635        self.book.apply_delta(delta)?;
1636
1637        let is_snapshot = RecordFlag::F_SNAPSHOT.matches(delta.flags);
1638        let is_last = RecordFlag::F_LAST.matches(delta.flags);
1639        let is_clear = delta.action == BookAction::Clear;
1640        let snapshot_complete = is_last && (is_snapshot || self.queue_snapshot_in_progress);
1641
1642        if self.config.queue_position {
1643            if is_snapshot && !is_last {
1644                // Snapshot deltas can arrive as a clear followed by multiple
1645                // adds. Rebase only after the final delta so partial snapshots
1646                // do not discard the old queue estimate.
1647                self.queue_snapshot_in_progress = true;
1648            }
1649
1650            if snapshot_complete {
1651                self.queue_snapshot_in_progress = false;
1652                self.rebase_queue_positions();
1653            } else if is_clear && !is_snapshot {
1654                self.rebase_queue_positions();
1655            } else if !self.queue_snapshot_in_progress {
1656                self.adjust_queue_for_delta(delta);
1657            }
1658        }
1659
1660        if self.config.queue_position && (snapshot_complete || (is_clear && !is_snapshot)) {
1661            self.seed_tob_baseline();
1662        }
1663
1664        self.iterate(delta.ts_init, AggressorSide::NoAggressor);
1665        Ok(())
1666    }
1667
1668    /// Process the venues market for the given order book deltas.
1669    ///
1670    /// # Errors
1671    ///
1672    /// Returns an error if:
1673    /// - Any delta order price precision does not match the instrument (for Add/Update actions).
1674    /// - Any delta order size precision does not match the instrument (for Add/Update actions).
1675    /// - Applying the deltas to the book fails.
1676    pub fn process_order_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
1677        log::debug!("Processing {deltas}");
1678
1679        // Validate precision for Add and Update actions (Delete/Clear may have NULL_ORDER)
1680        for delta in &deltas.deltas {
1681            if matches!(delta.action, BookAction::Add | BookAction::Update) {
1682                self.check_price_precision(delta.order.price.precision, "delta order price")?;
1683                self.check_size_precision(delta.order.size.precision, "delta order size")?;
1684            }
1685        }
1686
1687        // L1 books are driven by top-of-book data only, ignore deltas
1688        if self.book_type == BookType::L1_MBP {
1689            self.iterate(deltas.ts_init, AggressorSide::NoAggressor);
1690            return Ok(());
1691        }
1692
1693        self.book.apply_deltas(deltas)?;
1694
1695        let mut has_snapshot_or_clear = false;
1696
1697        if self.config.queue_position {
1698            for delta in &deltas.deltas {
1699                if RecordFlag::F_SNAPSHOT.matches(delta.flags) || delta.action == BookAction::Clear
1700                {
1701                    has_snapshot_or_clear = true;
1702                    break;
1703                }
1704                self.adjust_queue_for_delta(delta);
1705            }
1706        }
1707
1708        if self.config.queue_position && has_snapshot_or_clear {
1709            self.queue_snapshot_in_progress = false;
1710            self.rebase_queue_positions();
1711            self.seed_tob_baseline();
1712        }
1713
1714        self.iterate(deltas.ts_init, AggressorSide::NoAggressor);
1715        Ok(())
1716    }
1717
1718    /// Process the venues market for the given order book depth.
1719    ///
1720    /// # Errors
1721    ///
1722    /// Returns an error if:
1723    /// - Any bid/ask price precision does not match the instrument.
1724    /// - Any bid/ask size precision does not match the instrument.
1725    /// - Applying the depth to the book fails.
1726    /// - Updating the L1 order book with the top-of-book quote fails.
1727    pub fn process_order_book_depth(&mut self, depth: &OrderBookDepth) -> anyhow::Result<()> {
1728        log::debug!("Processing OrderBookDepth for {}", depth.instrument_id);
1729
1730        // Validate precision for non-padding entries
1731        for order in &depth.bids {
1732            if order.side.is_none() || !order.size.is_positive() {
1733                continue;
1734            }
1735            self.check_price_precision(order.price.precision, "bid price")?;
1736            self.check_size_precision(order.size.precision, "bid size")?;
1737        }
1738
1739        for order in &depth.asks {
1740            if order.side.is_none() || !order.size.is_positive() {
1741                continue;
1742            }
1743            self.check_price_precision(order.price.precision, "ask price")?;
1744            self.check_size_precision(order.size.precision, "ask size")?;
1745        }
1746
1747        let top_bid = Self::first_valid_depth_order(&depth.bids, OrderSide::Buy);
1748        let top_ask = Self::first_valid_depth_order(&depth.asks, OrderSide::Sell);
1749
1750        // For L1 books, only apply top-of-book to avoid mispricing
1751        // against worst-level entries when full depth is applied
1752        if self.book_type == BookType::L1_MBP {
1753            let quote = QuoteTick::new(
1754                depth.instrument_id,
1755                Self::depth_quote_price(top_bid, self.instrument.price_precision()),
1756                Self::depth_quote_price(top_ask, self.instrument.price_precision()),
1757                Self::depth_quote_size(top_bid, self.instrument.size_precision()),
1758                Self::depth_quote_size(top_ask, self.instrument.size_precision()),
1759                depth.ts_event,
1760                depth.ts_init,
1761            );
1762            self.book.update_quote_tick(&quote)?;
1763            self.reset_trade_liquidity(quote.ts_event);
1764            self.last_quote_bid = top_bid.map(|order| order.price);
1765            self.last_quote_ask = top_ask.map(|order| order.price);
1766        } else {
1767            self.book.apply_depth(depth)?;
1768        }
1769
1770        // Depth always replaces the full book via apply_depth regardless of flags
1771        if self.config.queue_position {
1772            self.rebase_queue_positions();
1773            let bid_price_raw = top_bid.map_or(0, |order| order.price.raw());
1774            let bid_size_raw = top_bid.map_or(0, |order| order.size.raw());
1775            let ask_price_raw = top_ask.map_or(0, |order| order.price.raw());
1776            let ask_size_raw = top_ask.map_or(0, |order| order.size.raw());
1777
1778            self.decrement_l1_queue_on_quote(
1779                bid_price_raw,
1780                bid_size_raw,
1781                ask_price_raw,
1782                ask_size_raw,
1783            );
1784
1785            self.prev_bid_price_raw = bid_price_raw;
1786            self.prev_ask_price_raw = ask_price_raw;
1787            self.tob_initialized = true;
1788        }
1789
1790        self.iterate(depth.ts_init, AggressorSide::NoAggressor);
1791        Ok(())
1792    }
1793
1794    fn first_valid_depth_order(orders: &[BookOrder], side: OrderSide) -> Option<BookOrder> {
1795        orders
1796            .iter()
1797            .copied()
1798            .find(|order| order.side == Some(side) && order.size.is_positive())
1799    }
1800
1801    fn depth_quote_price(order: Option<BookOrder>, price_precision: u8) -> Price {
1802        order.map_or_else(|| Price::zero(price_precision), |order| order.price)
1803    }
1804
1805    fn depth_quote_size(order: Option<BookOrder>, size_precision: u8) -> Quantity {
1806        order.map_or_else(|| Quantity::zero(size_precision), |order| order.size)
1807    }
1808
1809    /// Processes a quote tick to update the market state.
1810    pub fn process_quote_tick(&mut self, quote: &QuoteTick) {
1811        log::debug!("Processing {quote}");
1812
1813        if let Err(e) = self.check_price_precision(quote.bid_price.precision, "bid_price") {
1814            self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1815            return;
1816        }
1817
1818        if let Err(e) = self.check_price_precision(quote.ask_price.precision, "ask_price") {
1819            self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1820            return;
1821        }
1822
1823        if let Err(e) = self.check_size_precision(quote.bid_size.precision, "bid_size") {
1824            self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1825            return;
1826        }
1827
1828        if let Err(e) = self.check_size_precision(quote.ask_size.precision, "ask_size") {
1829            self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1830            return;
1831        }
1832
1833        self.precision_mismatch_streak = 0;
1834
1835        if self.book_type == BookType::L1_MBP {
1836            // Stale update: skip book mutation and cache updates
1837            if quote.ts_event < self.book.ts_last {
1838                log::warn!(
1839                    "Skipping stale quote: ts_event {} < book.ts_last {} for {}",
1840                    quote.ts_event,
1841                    self.book.ts_last,
1842                    self.book.instrument_id,
1843                );
1844                self.iterate(quote.ts_init, AggressorSide::NoAggressor);
1845                return;
1846            }
1847
1848            if !self.update_quote_tick_or_skip(quote, "quote tick") {
1849                return;
1850            }
1851
1852            if self.config.queue_position {
1853                self.decrement_l1_queue_on_quote(
1854                    quote.bid_price.raw(),
1855                    quote.bid_size.raw(),
1856                    quote.ask_price.raw(),
1857                    quote.ask_size.raw(),
1858                );
1859                self.prev_bid_price_raw = quote.bid_price.raw();
1860                self.prev_ask_price_raw = quote.ask_price.raw();
1861                self.tob_initialized = true;
1862            }
1863            self.last_quote_bid = Some(quote.bid_price);
1864            self.last_quote_ask = Some(quote.ask_price);
1865        }
1866
1867        self.iterate(quote.ts_init, AggressorSide::NoAggressor);
1868    }
1869
1870    /// Processes a bar and simulates market dynamics by creating synthetic ticks.
1871    ///
1872    /// For L1 books with bar execution enabled, generates synthetic trade or quote
1873    /// ticks from bar OHLC data to drive order matching.
1874    ///
1875    /// # Panics
1876    ///
1877    /// Panics if:
1878    /// - The bar type configuration is missing a time delta.
1879    pub fn process_bar(&mut self, bar: &Bar) {
1880        log::debug!("Processing {bar}");
1881
1882        debug_assert!(
1883            bar.high >= bar.open
1884                && bar.high >= bar.low
1885                && bar.high >= bar.close
1886                && bar.low <= bar.open
1887                && bar.low <= bar.close,
1888            "OHLC invariant violated for {bar}"
1889        );
1890
1891        // Check if configured for bar execution can only process an L1 book with bars
1892        if !self.config.bar_execution || self.book_type != BookType::L1_MBP {
1893            return;
1894        }
1895
1896        let bar_type = bar.bar_type;
1897
1898        // Do not process internally aggregated bars
1899        if bar_type.aggregation_source() == AggregationSource::Internal {
1900            return;
1901        }
1902
1903        if let Err(e) = self.check_price_precision(bar.open.precision, "bar open") {
1904            self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1905            return;
1906        }
1907
1908        if let Err(e) = self.check_price_precision(bar.high.precision, "bar high") {
1909            self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1910            return;
1911        }
1912
1913        if let Err(e) = self.check_price_precision(bar.low.precision, "bar low") {
1914            self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1915            return;
1916        }
1917
1918        if let Err(e) = self.check_price_precision(bar.close.precision, "bar close") {
1919            self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1920            return;
1921        }
1922
1923        if let Err(e) = self.check_size_precision(bar.volume.precision, "bar volume") {
1924            self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1925            return;
1926        }
1927
1928        self.precision_mismatch_streak = 0;
1929
1930        let price_type = bar_type.spec().price_type;
1931        if price_type == PriceType::Mark {
1932            log::warn!(
1933                "Cannot process bar for {} with `PriceType::Mark`, mark price bars are not supported for bar execution",
1934                bar.instrument_id(),
1935            );
1936            return;
1937        }
1938
1939        let execution_bar_type =
1940            if let Some(execution_bar_type) = self.execution_bar_types.get(&bar.instrument_id()) {
1941                execution_bar_type.to_owned()
1942            } else {
1943                self.execution_bar_types
1944                    .insert(bar.instrument_id(), bar_type);
1945                self.execution_bar_deltas
1946                    .insert(bar_type, bar_type.spec().timedelta());
1947                bar_type
1948            };
1949
1950        if execution_bar_type != bar_type {
1951            let mut bar_type_timedelta = self.execution_bar_deltas.get(&bar_type).copied();
1952            if bar_type_timedelta.is_none() {
1953                bar_type_timedelta = Some(bar_type.spec().timedelta());
1954                self.execution_bar_deltas
1955                    .insert(bar_type, bar_type_timedelta.unwrap());
1956            }
1957
1958            if self.execution_bar_deltas.get(&execution_bar_type).unwrap()
1959                >= &bar_type_timedelta.unwrap()
1960            {
1961                self.execution_bar_types
1962                    .insert(bar_type.instrument_id(), bar_type);
1963            } else {
1964                return;
1965            }
1966        }
1967
1968        match price_type {
1969            PriceType::Last | PriceType::Mid => self.process_trade_ticks_from_bar(bar),
1970            PriceType::Bid => {
1971                self.last_bar_bid = Some(bar.to_owned());
1972                self.process_quote_ticks_from_bar();
1973            }
1974            PriceType::Ask => {
1975                self.last_bar_ask = Some(bar.to_owned());
1976                self.process_quote_ticks_from_bar();
1977            }
1978            PriceType::Mark => {
1979                unreachable!("PriceType::Mark bars return before execution bar state updates")
1980            }
1981        }
1982    }
1983
1984    fn process_trade_ticks_from_bar(&mut self, bar: &Bar) {
1985        let sizes = BarTickSizes::from_volume(bar.volume, self.instrument.size_increment());
1986
1987        let aggressor_side = if self.core.last.is_none_or(|last| bar.open > last) {
1988            AggressorSide::Buy
1989        } else {
1990            AggressorSide::Sell
1991        };
1992
1993        // Open: fill at market price (gap from previous bar)
1994        if self.core.last.is_none() {
1995            self.fill_at_market = true;
1996
1997            if !self.process_bar_trade_tick(
1998                bar,
1999                bar.open,
2000                sizes.open,
2001                aggressor_side,
2002                "bar open trade tick",
2003            ) {
2004                return;
2005            }
2006        } else if self.core.last.is_some_and(|last| bar.open != last) {
2007            // Gap between previous close and this bar's open
2008            self.fill_at_market = true;
2009
2010            if !self.process_bar_trade_tick(
2011                bar,
2012                bar.open,
2013                sizes.open,
2014                aggressor_side,
2015                "bar gap-open trade tick",
2016            ) {
2017                return;
2018            }
2019        }
2020
2021        // Determine high/low processing order.
2022        // Default: O > H > L > C. With adaptive ordering, swap if low is closer to open.
2023        let high_first = self.bar_high_first(bar);
2024
2025        if high_first {
2026            self.process_bar_high(bar, sizes.high);
2027            self.process_bar_low(bar, sizes.low);
2028        } else {
2029            self.process_bar_low(bar, sizes.low);
2030            self.process_bar_high(bar, sizes.high);
2031        }
2032
2033        // Close: fill at trigger price (market moving through prices)
2034        if self.core.last.is_some_and(|last| bar.close != last) {
2035            self.fill_at_market = false;
2036
2037            let aggressor_side = if bar.close > self.core.last.unwrap() {
2038                AggressorSide::Buy
2039            } else {
2040                AggressorSide::Sell
2041            };
2042
2043            if !self.process_bar_trade_tick(
2044                bar,
2045                bar.close,
2046                sizes.close,
2047                aggressor_side,
2048                "bar close trade tick",
2049            ) {
2050                return;
2051            }
2052        }
2053
2054        self.fill_at_market = true;
2055    }
2056
2057    fn process_bar_high(&mut self, bar: &Bar, size: Quantity) {
2058        if self.core.last.is_some_and(|last| bar.high > last) {
2059            self.fill_at_market = false;
2060
2061            self.process_bar_trade_tick(
2062                bar,
2063                bar.high,
2064                size,
2065                AggressorSide::Buy,
2066                "bar high trade tick",
2067            );
2068        }
2069    }
2070
2071    fn process_bar_low(&mut self, bar: &Bar, size: Quantity) {
2072        if self.core.last.is_some_and(|last| bar.low < last) {
2073            self.fill_at_market = false;
2074
2075            self.process_bar_trade_tick(
2076                bar,
2077                bar.low,
2078                size,
2079                AggressorSide::Sell,
2080                "bar low trade tick",
2081            );
2082        }
2083    }
2084
2085    fn process_bar_trade_tick(
2086        &mut self,
2087        bar: &Bar,
2088        price: Price,
2089        size: Quantity,
2090        aggressor_side: AggressorSide,
2091        context: &str,
2092    ) -> bool {
2093        if size.is_zero() {
2094            self.core.set_last_raw(price);
2095            return true;
2096        }
2097
2098        let trade_tick = TradeTick::new(
2099            bar.instrument_id(),
2100            price,
2101            size,
2102            aggressor_side,
2103            self.ids_generator.generate_trade_id(bar.ts_init),
2104            bar.ts_init,
2105            bar.ts_init,
2106        );
2107
2108        if !self.update_trade_tick_or_skip(&trade_tick, context) {
2109            return false;
2110        }
2111
2112        // Set last before matching so last-price triggers see this leg, as for trade ticks
2113        self.core.set_last_raw(price);
2114        self.iterate(trade_tick.ts_init, AggressorSide::NoAggressor);
2115        true
2116    }
2117
2118    fn process_quote_ticks_from_bar(&mut self) {
2119        // Wait for next bar
2120        if self.last_bar_bid.is_none()
2121            || self.last_bar_ask.is_none()
2122            || self.last_bar_bid.unwrap().ts_init != self.last_bar_ask.unwrap().ts_init
2123        {
2124            return;
2125        }
2126        let bid_bar = self.last_bar_bid.unwrap();
2127        let ask_bar = self.last_bar_ask.unwrap();
2128
2129        let size_increment = self.instrument.size_increment();
2130        let bid_sizes = BarTickSizes::from_volume(bid_bar.volume, size_increment);
2131        let ask_sizes = BarTickSizes::from_volume(ask_bar.volume, size_increment);
2132        let mut has_current_bid = false;
2133        let mut has_current_ask = false;
2134
2135        let mut quote_tick = QuoteTick::new(
2136            self.book.instrument_id,
2137            bid_bar.open,
2138            ask_bar.open,
2139            bid_sizes.open,
2140            ask_sizes.open,
2141            bid_bar.ts_init,
2142            bid_bar.ts_init,
2143        );
2144
2145        // Open: fill at market price (gap from previous bar)
2146        self.fill_at_market = true;
2147
2148        if !self.process_bar_quote_tick(
2149            &quote_tick,
2150            "bar open quote tick",
2151            &mut has_current_bid,
2152            &mut has_current_ask,
2153        ) {
2154            return;
2155        }
2156
2157        // Determine high/low processing order from the bid bar (v1 parity).
2158        // Default: O > H > L > C. With adaptive ordering, swap if low is closer to open
2159        let high_first = self.bar_high_first(&bid_bar);
2160
2161        let high_leg = (
2162            bid_bar.high,
2163            ask_bar.high,
2164            bid_sizes.high,
2165            ask_sizes.high,
2166            "bar high quote tick",
2167        );
2168        let low_leg = (
2169            bid_bar.low,
2170            ask_bar.low,
2171            bid_sizes.low,
2172            ask_sizes.low,
2173            "bar low quote tick",
2174        );
2175        let legs = if high_first {
2176            [high_leg, low_leg]
2177        } else {
2178            [low_leg, high_leg]
2179        };
2180
2181        // High/low: fill at trigger price (market moving through prices)
2182        for (bid_price, ask_price, bid_size, ask_size, context) in legs {
2183            self.fill_at_market = false;
2184            quote_tick.bid_price = bid_price;
2185            quote_tick.ask_price = ask_price;
2186            quote_tick.bid_size = bid_size;
2187            quote_tick.ask_size = ask_size;
2188
2189            if !self.process_bar_quote_tick(
2190                &quote_tick,
2191                context,
2192                &mut has_current_bid,
2193                &mut has_current_ask,
2194            ) {
2195                return;
2196            }
2197        }
2198
2199        // Close: fill at trigger price (market moving through prices)
2200        self.fill_at_market = false;
2201        quote_tick.bid_price = bid_bar.close;
2202        quote_tick.ask_price = ask_bar.close;
2203        quote_tick.bid_size = bid_sizes.close;
2204        quote_tick.ask_size = ask_sizes.close;
2205
2206        if !self.process_bar_quote_tick(
2207            &quote_tick,
2208            "bar close quote tick",
2209            &mut has_current_bid,
2210            &mut has_current_ask,
2211        ) {
2212            return;
2213        }
2214
2215        self.last_bar_bid = None;
2216        self.last_bar_ask = None;
2217        self.fill_at_market = true;
2218    }
2219
2220    fn process_bar_quote_tick(
2221        &mut self,
2222        quote: &QuoteTick,
2223        context: &str,
2224        has_current_bid: &mut bool,
2225        has_current_ask: &mut bool,
2226    ) -> bool {
2227        let has_bid_size = quote.bid_size.non_zero();
2228        let has_ask_size = quote.ask_size.non_zero();
2229        let mut book_changed = false;
2230        let mut bid_cleared = false;
2231        let mut ask_cleared = false;
2232
2233        match (has_bid_size, has_ask_size) {
2234            (true, true) => {
2235                if !self.update_quote_tick_or_skip(quote, context) {
2236                    return false;
2237                }
2238                *has_current_bid = true;
2239                *has_current_ask = true;
2240                book_changed = true;
2241            }
2242            _ => {
2243                if has_bid_size {
2244                    self.update_bar_quote_bid(quote);
2245                    *has_current_bid = true;
2246                    book_changed = true;
2247                } else if !*has_current_bid {
2248                    self.clear_bar_quote_bid(quote);
2249                    *has_current_bid = true;
2250                    book_changed = true;
2251                    bid_cleared = true;
2252                }
2253
2254                if has_ask_size {
2255                    self.update_bar_quote_ask(quote);
2256                    *has_current_ask = true;
2257                    book_changed = true;
2258                } else if !*has_current_ask {
2259                    self.clear_bar_quote_ask(quote);
2260                    *has_current_ask = true;
2261                    book_changed = true;
2262                    ask_cleared = true;
2263                }
2264            }
2265        }
2266
2267        if book_changed
2268            && let (Some(best_bid), Some(best_ask)) =
2269                (self.book.best_bid_price(), self.book.best_ask_price())
2270            && best_bid > best_ask
2271        {
2272            if has_bid_size && !has_ask_size {
2273                self.clear_bar_quote_ask(quote);
2274                ask_cleared = true;
2275            } else if has_ask_size && !has_bid_size {
2276                self.clear_bar_quote_bid(quote);
2277                bid_cleared = true;
2278            }
2279        }
2280
2281        if has_bid_size {
2282            self.last_quote_bid = Some(quote.bid_price);
2283        } else if bid_cleared {
2284            self.last_quote_bid = None;
2285        }
2286
2287        if has_ask_size {
2288            self.last_quote_ask = Some(quote.ask_price);
2289        } else if ask_cleared {
2290            self.last_quote_ask = None;
2291        }
2292
2293        if !book_changed {
2294            return true;
2295        }
2296
2297        self.reset_trade_liquidity(quote.ts_event);
2298
2299        self.iterate(quote.ts_init, AggressorSide::NoAggressor);
2300        true
2301    }
2302
2303    fn bar_high_first(&self, bar: &Bar) -> bool {
2304        !self.config.bar_adaptive_high_low_ordering || bar.high - bar.open < bar.open - bar.low
2305    }
2306
2307    fn update_bar_quote_bid(&mut self, quote: &QuoteTick) {
2308        let bid = BookOrder::new(
2309            OrderSide::Buy,
2310            quote.bid_price,
2311            quote.bid_size,
2312            OrderSide::Buy as u64,
2313        );
2314        self.book
2315            .add(bid, 0, self.book.sequence.saturating_add(1), quote.ts_event);
2316    }
2317
2318    fn clear_bar_quote_bid(&mut self, quote: &QuoteTick) {
2319        self.book
2320            .clear_bids(self.book.sequence.saturating_add(1), quote.ts_event);
2321    }
2322
2323    fn update_bar_quote_ask(&mut self, quote: &QuoteTick) {
2324        let ask = BookOrder::new(
2325            OrderSide::Sell,
2326            quote.ask_price,
2327            quote.ask_size,
2328            OrderSide::Sell as u64,
2329        );
2330        self.book
2331            .add(ask, 0, self.book.sequence.saturating_add(1), quote.ts_event);
2332    }
2333
2334    fn clear_bar_quote_ask(&mut self, quote: &QuoteTick) {
2335        self.book
2336            .clear_asks(self.book.sequence.saturating_add(1), quote.ts_event);
2337    }
2338
2339    /// Processes a trade tick to update the market state.
2340    ///
2341    /// For accepted L1 ticks, updates the order book to maintain market state. When
2342    /// `trade_execution` is disabled, the L1 path syncs matching prices from the book and
2343    /// returns; a later quote tick or executable bar drives matching and maintenance.
2344    /// Accepted L2/L3 ticks still advance `LastPrice` and run trailing-stop maintenance
2345    /// for all trigger types, enabled GTD expiry, and instrument-expiration checks. They
2346    /// can trigger `LastPrice` stop orders, which fill against book liquidity. The trade
2347    /// tick does not match resting limit orders or trigger stops that use other trigger
2348    /// types.
2349    pub fn process_trade_tick(&mut self, trade: &TradeTick) {
2350        log::debug!("Processing {trade}");
2351
2352        if let Err(e) = self.check_price_precision(trade.price.precision, "trade price") {
2353            self.log_precision_mismatch("trade tick", trade.instrument_id, &e);
2354            return;
2355        }
2356
2357        if let Err(e) = self.check_size_precision(trade.size.precision, "trade size") {
2358            self.log_precision_mismatch("trade tick", trade.instrument_id, &e);
2359            return;
2360        }
2361
2362        self.precision_mismatch_streak = 0;
2363
2364        let price_raw = trade.price.raw();
2365
2366        if self.book_type == BookType::L1_MBP {
2367            // Stale update: skip book mutation and trade execution
2368            if trade.ts_event < self.book.ts_last {
2369                log::warn!(
2370                    "Skipping stale trade: ts_event {} < book.ts_last {} for {}",
2371                    trade.ts_event,
2372                    self.book.ts_last,
2373                    self.book.instrument_id,
2374                );
2375                self.iterate(trade.ts_init, AggressorSide::NoAggressor);
2376                return;
2377            }
2378
2379            if !self.update_trade_tick_or_skip(trade, "trade tick") {
2380                return;
2381            }
2382        }
2383
2384        self.core.set_last_raw(trade.price);
2385
2386        if !self.config.trade_execution {
2387            if self.book_type == BookType::L1_MBP {
2388                if let Some(bid) = self.book.best_bid_price() {
2389                    self.core.set_bid_raw(bid);
2390                }
2391
2392                if let Some(ask) = self.book.best_ask_price() {
2393                    self.core.set_ask_raw(ask);
2394                }
2395            } else {
2396                self.iterate_with_mode(
2397                    trade.ts_init,
2398                    AggressorSide::NoAggressor,
2399                    OrderMatchMode::LastPriceStopTriggers,
2400                );
2401            }
2402            return;
2403        }
2404
2405        let aggressor_side = trade.aggressor_side;
2406
2407        match aggressor_side {
2408            AggressorSide::Buy => {
2409                // Buyer lifted the ask: ask was at trade.price, post-trade
2410                // ask is at least this level (only widen)
2411                if self.core.ask.is_none_or(|ask| trade.price > ask) {
2412                    self.core.set_ask_raw(trade.price);
2413                }
2414
2415                // Initialize bid from first trade if needed
2416                if self.core.bid.is_none() {
2417                    self.core.set_bid_raw(trade.price);
2418                }
2419            }
2420            AggressorSide::Sell => {
2421                // Seller hit the bid: bid was at trade.price, post-trade
2422                // bid is at most this level (only narrow)
2423                if self.core.bid.is_none_or(|bid| trade.price < bid) {
2424                    self.core.set_bid_raw(trade.price);
2425                }
2426
2427                // Initialize ask from first trade if needed
2428                if self.core.ask.is_none() {
2429                    self.core.set_ask_raw(trade.price);
2430                }
2431            }
2432            AggressorSide::NoAggressor => {
2433                if self.core.bid.is_none_or(|bid| trade.price <= bid) {
2434                    self.core.set_bid_raw(trade.price);
2435                }
2436
2437                if self.core.ask.is_none_or(|ask| trade.price >= ask) {
2438                    self.core.set_ask_raw(trade.price);
2439                }
2440            }
2441        }
2442
2443        let original_bid = self.core.bid;
2444        let original_ask = self.core.ask;
2445
2446        match aggressor_side {
2447            AggressorSide::Sell => {
2448                if original_ask.is_some_and(|ask| trade.price < ask) {
2449                    self.core.set_ask_raw(trade.price);
2450                }
2451            }
2452            AggressorSide::Buy => {
2453                if original_bid.is_some_and(|bid| trade.price > bid) {
2454                    self.core.set_bid_raw(trade.price);
2455                }
2456            }
2457            AggressorSide::NoAggressor => {
2458                // No directional information, so both sides take the trade price
2459                self.core.set_bid_raw(trade.price);
2460                self.core.set_ask_raw(trade.price);
2461            }
2462        }
2463
2464        self.start_trade_iteration(trade.size);
2465
2466        if self.config.liquidity_consumption && self.book_type != BookType::L1_MBP {
2467            self.seed_trade_consumption(
2468                price_raw,
2469                trade.size.raw(),
2470                trade.ts_event,
2471                aggressor_side,
2472            );
2473        }
2474
2475        self.resolve_pending_on_trade(price_raw);
2476        self.decrement_queue_on_trade(price_raw, trade.size.raw(), aggressor_side);
2477
2478        self.iterate(trade.ts_init, aggressor_side);
2479
2480        self.finish_trade_iteration();
2481
2482        // Restore the non-aggressor side after temporary trade price override.
2483        // For L2/L3 books the book has independent depth so restore from originals.
2484        // For L1_MBP restore from the last quote values (not originals, which are
2485        // polluted by iterate's L1 book sync). Without quotes, skip the restore
2486        // so the core tracks the latest trade price.
2487        if self.book_type == BookType::L1_MBP {
2488            match aggressor_side {
2489                AggressorSide::Sell => {
2490                    if let Some(ask) = self.last_quote_ask {
2491                        self.core.ask = Some(ask);
2492                    }
2493                }
2494                AggressorSide::Buy => {
2495                    if let Some(bid) = self.last_quote_bid {
2496                        self.core.bid = Some(bid);
2497                    }
2498                }
2499                AggressorSide::NoAggressor => {}
2500            }
2501        } else {
2502            match aggressor_side {
2503                AggressorSide::Sell => {
2504                    if let Some(ask) = original_ask
2505                        && trade.price < ask
2506                    {
2507                        self.core.ask = Some(ask);
2508                    }
2509                }
2510                AggressorSide::Buy => {
2511                    if let Some(bid) = original_bid
2512                        && trade.price > bid
2513                    {
2514                        self.core.bid = Some(bid);
2515                    }
2516                }
2517                AggressorSide::NoAggressor => {}
2518            }
2519        }
2520    }
2521
2522    fn start_trade_iteration(&mut self, size: Quantity) {
2523        self.last_trade_size = Some(size);
2524        self.trade_consumption = 0;
2525        self.l1_trade_size = (self.book_type == BookType::L1_MBP
2526            && self.config.liquidity_consumption)
2527            .then_some(size.raw());
2528    }
2529
2530    fn finish_trade_iteration(&mut self) {
2531        self.last_trade_size = None;
2532
2533        // L1 limit settlement shares this trade's budget until fresh market data replaces it
2534        if self.l1_trade_size.is_none() {
2535            self.trade_consumption = 0;
2536        }
2537    }
2538
2539    fn reset_trade_liquidity(&mut self, timestamp: UnixNanos) {
2540        if timestamp >= self.book.ts_last {
2541            self.l1_trade_size = None;
2542            self.trade_consumption = 0;
2543        }
2544    }
2545
2546    fn update_quote_tick_or_skip(&mut self, quote: &QuoteTick, context: &str) -> bool {
2547        if let Err(e) = self.book.update_quote_tick(quote) {
2548            log::warn!(
2549                "Skipping {context} for {}: update_quote_tick failed: {e}",
2550                quote.instrument_id,
2551            );
2552            return false;
2553        }
2554
2555        self.reset_trade_liquidity(quote.ts_event);
2556        true
2557    }
2558
2559    fn update_trade_tick_or_skip(&mut self, trade: &TradeTick, context: &str) -> bool {
2560        if let Err(e) = self.book.update_trade_tick(trade) {
2561            log::warn!(
2562                "Skipping {context} for {}: update_trade_tick failed: {e}",
2563                trade.instrument_id,
2564            );
2565            return false;
2566        }
2567
2568        self.reset_trade_liquidity(trade.ts_event);
2569        true
2570    }
2571
2572    /// Processes a market status action to update the market state.
2573    pub fn process_status(&mut self, action: MarketStatusAction) {
2574        log::debug!("Processing {action}");
2575
2576        match action {
2577            MarketStatusAction::Trading | MarketStatusAction::PreOpen
2578                if matches!(
2579                    self.market_status,
2580                    MarketStatus::Closed | MarketStatus::Paused | MarketStatus::Suspended
2581                ) =>
2582            {
2583                self.market_status = MarketStatus::Open;
2584            }
2585            MarketStatusAction::Pause if self.market_status == MarketStatus::Open => {
2586                self.market_status = MarketStatus::Paused;
2587            }
2588            MarketStatusAction::Suspend if self.market_status == MarketStatus::Open => {
2589                self.market_status = MarketStatus::Suspended;
2590            }
2591            MarketStatusAction::Halt | MarketStatusAction::Close
2592                if self.market_status == MarketStatus::Open =>
2593            {
2594                self.market_status = MarketStatus::Closed;
2595            }
2596            _ => {}
2597        }
2598    }
2599
2600    /// Processes an instrument close event.
2601    ///
2602    /// For `ContractExpired` close types, stores the close and triggers expiration
2603    /// processing which cancels all open orders and closes all open positions.
2604    pub fn process_instrument_close(&mut self, close: InstrumentClose) {
2605        if close.instrument_id != self.instrument.id() {
2606            log::warn!(
2607                "Received instrument close for unknown instrument_id: {}",
2608                close.instrument_id
2609            );
2610            return;
2611        }
2612
2613        if close.close_type == InstrumentCloseType::ContractExpired {
2614            self.instrument_close = Some(close);
2615            self.iterate(close.ts_init, AggressorSide::NoAggressor);
2616        }
2617    }
2618
2619    /// Processes instrument expiration at the given timestamp.
2620    pub fn process_instrument_expiration(&mut self, timestamp_ns: UnixNanos) {
2621        self.check_instrument_expiration(timestamp_ns, false, &[]);
2622    }
2623
2624    /// Returns whether instrument expiration has already been processed.
2625    #[must_use]
2626    pub const fn is_expiration_processed(&self) -> bool {
2627        self.expiration_processed
2628    }
2629
2630    fn requires_pending_resolution(&self) -> bool {
2631        matches!(self.instrument, InstrumentAny::BinaryOption(_))
2632    }
2633
2634    fn cancel_open_orders_for_expiration(&mut self, excluded: &[ClientOrderId]) {
2635        // Build a single de-duplicated cancellation set across the matching
2636        // core and cache. Resting orders may still only be represented in the
2637        // core while inflight orders can remain cache-only during the
2638        // submitted/pending transition window.
2639        let instrument_id = self.instrument.id();
2640        let expiration_order_ids: IndexSet<ClientOrderId> = {
2641            let cache = self.cache.borrow();
2642            let mut order_ids = IndexSet::new();
2643
2644            for order_info in self.get_open_orders() {
2645                order_ids.insert(order_info.client_order_id);
2646            }
2647
2648            for order in cache.orders(None, Some(&instrument_id), None, None, None) {
2649                if order.is_open() || order.is_inflight() {
2650                    order_ids.insert(order.client_order_id());
2651                }
2652            }
2653
2654            order_ids.retain(|client_order_id| !excluded.contains(client_order_id));
2655            order_ids
2656        };
2657
2658        for client_order_id in expiration_order_ids {
2659            let order = {
2660                let cache = self.cache.borrow();
2661                cache.order(&client_order_id).map(|order| order.clone())
2662            };
2663
2664            if let Some(order) = order {
2665                self.cancel_order_excluding(&order, None, excluded);
2666            }
2667        }
2668    }
2669
2670    fn enter_pending_resolution(&mut self, excluded: &[ClientOrderId]) {
2671        if self.pending_resolution {
2672            return;
2673        }
2674
2675        self.pending_resolution = true;
2676        self.market_status = MarketStatus::Closed;
2677        self.cancel_open_orders_for_expiration(excluded);
2678        log::info!(
2679            "{} expired and is now pending resolution; open orders canceled and new orders blocked",
2680            self.instrument.id()
2681        );
2682    }
2683
2684    fn check_instrument_expiration(
2685        &mut self,
2686        timestamp_ns: UnixNanos,
2687        defer_settlement: bool,
2688        excluded: &[ClientOrderId],
2689    ) {
2690        if self.expiration_processed || self.option_settlement_failed {
2691            return;
2692        }
2693
2694        let timestamp_triggered = self
2695            .instrument
2696            .expiration_ns()
2697            .is_some_and(|ns| timestamp_ns >= ns);
2698
2699        if !timestamp_triggered && self.instrument_close.is_none() {
2700            return;
2701        }
2702
2703        if self.instrument_close.is_none()
2704            && timestamp_triggered
2705            && self.requires_pending_resolution()
2706        {
2707            self.enter_pending_resolution(excluded);
2708            return;
2709        }
2710
2711        if matches!(
2712            self.instrument,
2713            InstrumentAny::OptionContract(_) | InstrumentAny::CryptoOption(_)
2714        ) {
2715            // `iterate` matches resting orders ahead of this check, so enter
2716            // pending resolution at the first trigger. Latched because a queuing
2717            // handler leaves the cached status behind the cancellation dispatch.
2718            if !self.option_expiration_orders_canceled {
2719                self.option_expiration_orders_canceled = true;
2720                self.enter_pending_resolution(excluded);
2721            }
2722
2723            // The expiry timer settles after all same-timestamp market data,
2724            // while order cancellation and market closure still happen inline.
2725            if defer_settlement
2726                && self.instrument_close.is_none()
2727                && self.instrument.expiration_ns() == Some(timestamp_ns)
2728            {
2729                return;
2730            }
2731
2732            match self.process_option_expiry(timestamp_ns) {
2733                Ok(true) => {
2734                    self.expiration_processed = true;
2735                    self.pending_resolution = false;
2736                    self.instrument_close.take();
2737                    self.option_settlement_warning = None;
2738                    log::info!("{} reached expiration", self.instrument.id());
2739                }
2740                Ok(false) => {}
2741                Err(e) => {
2742                    self.option_settlement_failed = true;
2743                    log::error!(
2744                        "Option settlement failed terminally for {}: {e}",
2745                        self.instrument.id()
2746                    );
2747                }
2748            }
2749            return;
2750        }
2751
2752        self.expiration_processed = true;
2753        self.pending_resolution = false;
2754        let close = self.instrument_close.take();
2755        log::info!("{} reached expiration", self.instrument.id());
2756        self.cancel_open_orders_for_expiration(excluded);
2757
2758        let instrument_id = self.instrument.id();
2759        let positions: Vec<(
2760            TraderId,
2761            StrategyId,
2762            AccountId,
2763            PositionId,
2764            OrderSide,
2765            Quantity,
2766        )> = {
2767            let cache = self.cache.borrow();
2768            cache
2769                .positions_open(None, Some(&instrument_id), None, None, None)
2770                .into_iter()
2771                .filter_map(|pos| {
2772                    OrderCore::closing_side(pos.side).map(|closing_side| {
2773                        (
2774                            pos.trader_id,
2775                            pos.strategy_id,
2776                            pos.account_id,
2777                            pos.id,
2778                            closing_side,
2779                            pos.quantity,
2780                        )
2781                    })
2782                })
2783                .collect()
2784        };
2785
2786        let ts_now = self.clock.borrow().timestamp_ns();
2787        let close_price = close.as_ref().map(|close| close.close_price);
2788
2789        for (trader_id, strategy_id, account_id, position_id, closing_side, quantity) in positions {
2790            let client_order_id =
2791                ClientOrderId::from(format!("EXPIRATION-{}-{}", self.venue, UUID4::new()).as_str());
2792            let mut order = OrderAny::Market(MarketOrder::new(
2793                trader_id,
2794                strategy_id,
2795                instrument_id,
2796                client_order_id,
2797                closing_side,
2798                quantity,
2799                TimeInForce::Gtc,
2800                UUID4::new(),
2801                ts_now,
2802                true, // reduce_only
2803                false,
2804                None,
2805                None,
2806                None,
2807                None,
2808                None,
2809                None,
2810                None,
2811                Some(vec![Ustr::from(&format!(
2812                    "EXPIRATION_{}_CLOSE",
2813                    self.venue
2814                ))]),
2815            ));
2816            order.set_liquidity_side(LiquiditySide::Taker);
2817
2818            let add_result =
2819                self.cache
2820                    .borrow_mut()
2821                    .add_order(order.clone(), Some(position_id), None, false);
2822            if add_result.is_err() {
2823                log::debug!("Expiration order already in cache: {client_order_id}");
2824            } else {
2825                self.publish_order_initialized(&order);
2826            }
2827
2828            let venue_order_id = self.ids_generator.get_venue_order_id(&order).unwrap();
2829
2830            // A restored position can expire with no order processed this
2831            // session, leaving the account unindexed.
2832            self.account_ids.insert(trader_id, account_id);
2833            self.generate_order_accepted(&order, venue_order_id);
2834
2835            if let Some(fill_price) = close_price {
2836                if let Err(e) = self.apply_fills(
2837                    &order,
2838                    &[(fill_price, quantity)],
2839                    LiquiditySide::Taker,
2840                    Some(position_id),
2841                    None,
2842                    None,
2843                    false,
2844                ) {
2845                    log::error!("Cannot fill expiration order {client_order_id}: {e}");
2846                }
2847            } else {
2848                self.fill_market_order(client_order_id);
2849            }
2850        }
2851    }
2852
2853    /// Liquidates all open positions for this instrument.
2854    ///
2855    /// Cancels open orders if `cancel_open_orders` is true, then closes every open
2856    /// position at best bid/ask, emitting accepted and filled
2857    /// events for each synthetic close order.
2858    ///
2859    /// # Panics
2860    ///
2861    /// Panics if the venue order ID generator cannot produce an ID for the synthetic
2862    /// liquidation order (internal state inconsistency).
2863    ///
2864    /// Only positions whose instrument settles in `settlement_currency` are closed.
2865    /// Matching engines for other settlement currencies are skipped, scoping
2866    /// liquidation to the currency whose margin account breached the threshold.
2867    pub fn liquidate_open_positions(
2868        &mut self,
2869        ts_now: UnixNanos,
2870        cancel_open_orders: bool,
2871        settlement_currency: Currency,
2872    ) {
2873        // Only liquidate positions settled in the breached currency.
2874        if self.instrument.settlement_currency() != settlement_currency {
2875            return;
2876        }
2877
2878        if cancel_open_orders {
2879            let open_orders: Vec<RestingOrder> = self.get_open_orders();
2880            for order_info in &open_orders {
2881                let order = {
2882                    let cache = self.cache.borrow();
2883                    cache.order_owned(&order_info.client_order_id)
2884                };
2885
2886                if let Some(order) = order {
2887                    self.cancel_order(&order, None);
2888                }
2889            }
2890        }
2891
2892        let instrument_id = self.instrument.id();
2893        let positions: Vec<(
2894            TraderId,
2895            StrategyId,
2896            AccountId,
2897            PositionId,
2898            OrderSide,
2899            Quantity,
2900        )> = {
2901            let cache = self.cache.borrow();
2902            cache
2903                .positions_open(None, Some(&instrument_id), None, None, None)
2904                .into_iter()
2905                .filter_map(|pos| {
2906                    OrderCore::closing_side(pos.side).map(|closing_side| {
2907                        (
2908                            pos.trader_id,
2909                            pos.strategy_id,
2910                            pos.account_id,
2911                            pos.id,
2912                            closing_side,
2913                            pos.quantity,
2914                        )
2915                    })
2916                })
2917                .collect()
2918        };
2919
2920        for (trader_id, strategy_id, account_id, position_id, closing_side, quantity) in positions {
2921            // Pre-check: ensure a price source is available before emitting events.
2922            let has_price = if closing_side == OrderSide::Sell {
2923                self.best_bid_price().is_some()
2924            } else {
2925                self.best_ask_price().is_some()
2926            };
2927
2928            if !has_price {
2929                log::warn!(
2930                    "LIQUIDATION: no price available for {instrument_id} position {position_id}, skipping"
2931                );
2932                continue;
2933            }
2934
2935            let client_order_id = ClientOrderId::from(
2936                format!("LIQUIDATION-{}-{}", self.venue, UUID4::new()).as_str(),
2937            );
2938            let order = OrderAny::Market(MarketOrder::new(
2939                trader_id,
2940                strategy_id,
2941                instrument_id,
2942                client_order_id,
2943                closing_side,
2944                quantity,
2945                TimeInForce::Ioc,
2946                UUID4::new(),
2947                ts_now,
2948                true, // reduce_only
2949                false,
2950                None,
2951                None,
2952                None,
2953                None,
2954                None,
2955                None,
2956                None,
2957                Some(vec![Ustr::from(&format!(
2958                    "LIQUIDATION_{}_CLOSE",
2959                    self.venue
2960                ))]),
2961            ));
2962
2963            let venue_order_id = self.ids_generator.get_venue_order_id(&order).unwrap();
2964            {
2965                let mut cache = self.cache.borrow_mut();
2966                if let Err(e) = cache.add_order(order.clone(), Some(position_id), None, false) {
2967                    log::debug!("Liquidation order already in cache: {e}");
2968                } else {
2969                    drop(cache);
2970                    self.publish_order_initialized(&order);
2971                    self.cache
2972                        .borrow_mut()
2973                        .add_venue_order_id(&client_order_id, &venue_order_id, false)
2974                        .ok();
2975                }
2976            }
2977
2978            // Route through the normal market-order fill machinery (fill model,
2979            // book depth consumption, slippage) instead of apply_fills directly.
2980            self.account_ids.insert(trader_id, account_id);
2981            self.generate_order_submitted(&order, account_id);
2982            self.generate_order_accepted(&order, venue_order_id);
2983            self.fill_market_order(client_order_id);
2984        }
2985    }
2986
2987    /// Processes a new order submission.
2988    ///
2989    /// Validates the order against instrument precision, expiration, and contingency
2990    /// rules before accepting or rejecting it. The order must already be in the cache;
2991    /// an order that is not is logged as an error and ignored.
2992    ///
2993    /// # Panics
2994    ///
2995    /// Panics if an OTO child order references a missing or non-OTO parent.
2996    pub fn process_order(&mut self, order: &mut OrderAny, account_id: AccountId) {
2997        // Idempotent: OTO children may be re-routed via `fill_order`
2998        if self.core.order_exists(order.client_order_id()) {
2999            return;
3000        }
3001
3002        if !self.cache.borrow().order_exists(&order.client_order_id()) {
3003            log::error!(
3004                "Cannot process order {}: not found in cache",
3005                order.client_order_id()
3006            );
3007            return;
3008        }
3009
3010        // Ensure expiration semantics are enforced even when no fresh market-data
3011        // tick arrives for this instrument after expiry (e.g. after rotation).
3012        // The order being processed is left for validation to reject rather than canceled
3013        let ts_now = self.clock.borrow().timestamp_ns();
3014        self.check_instrument_expiration(
3015            ts_now,
3016            self.config.defer_option_settlement,
3017            &[order.client_order_id()],
3018        );
3019
3020        // Validate inside a cache borrow scope, collecting any rejection
3021        // reason rather than emitting events while the borrow is held.
3022        // This avoids RefCell re-entrancy panics from synchronous event
3023        // dispatch that calls back into the execution engine.
3024        let reject_reason: Option<Ustr> = 'validate: {
3025            let cache_borrow = self.cache.as_ref().borrow();
3026
3027            // Index identifiers
3028            self.account_ids.insert(order.trader_id(), account_id);
3029
3030            if self.pending_resolution {
3031                break 'validate Some(
3032                    format!(
3033                        "Contract {} has expired and is pending resolution",
3034                        self.instrument.id()
3035                    )
3036                    .into(),
3037                );
3038            }
3039
3040            if self.market_status != MarketStatus::Open {
3041                break 'validate Some(
3042                    format!(
3043                        "Market {} is {}, cannot accept order {}",
3044                        self.instrument.id(),
3045                        self.market_status,
3046                        order.client_order_id()
3047                    )
3048                    .into(),
3049                );
3050            }
3051
3052            // Check for instrument expiration or activation
3053            if self.instrument.has_expiration() {
3054                if let Some(activation_ns) = self.instrument.activation_ns()
3055                    && self.clock.borrow().timestamp_ns() < activation_ns
3056                {
3057                    break 'validate Some(
3058                        format!(
3059                            "Contract {} is not yet active, activation {activation_ns}",
3060                            self.instrument.id(),
3061                        )
3062                        .into(),
3063                    );
3064                }
3065
3066                if let Some(expiration_ns) = self.instrument.expiration_ns()
3067                    && self.clock.borrow().timestamp_ns() >= expiration_ns
3068                {
3069                    break 'validate Some(
3070                        format!(
3071                            "Contract {} has expired, expiration {expiration_ns}",
3072                            self.instrument.id(),
3073                        )
3074                        .into(),
3075                    );
3076                }
3077            }
3078
3079            // An explicit close can settle the contract before its configured expiration
3080            if self.expiration_processed {
3081                break 'validate Some(
3082                    format!("Contract {} has expired", self.instrument.id()).into(),
3083                );
3084            }
3085
3086            // Contingent orders checks
3087            if self.config.support_contingent_orders {
3088                if let Some(parent_order_id) = order.parent_order_id() {
3089                    // Parked again below while the parent has not triggered it
3090                    self.pending_oto_order_ids
3091                        .swap_remove(&order.client_order_id());
3092
3093                    let parent_order = match self.order_snapshot(parent_order_id) {
3094                        Some(o) if o.contingency_type() == Some(ContingencyType::Oto) => o,
3095                        _ => panic!("OTO parent not found"),
3096                    };
3097                    let parent_filled_qty = parent_order.filled_qty();
3098
3099                    // The rejection of a parent in the same command can still be deferred
3100                    if parent_order.status() == OrderStatus::Rejected
3101                        || self
3102                            .rejected_oto_parent_ids
3103                            .borrow()
3104                            .contains(&parent_order_id)
3105                    {
3106                        break 'validate Some(
3107                            format!("Rejected OTO order from {parent_order_id}").into(),
3108                        );
3109                    } else if parent_filled_qty.is_zero()
3110                        || (self.config.oto_full_trigger
3111                            && parent_filled_qty < parent_order.quantity())
3112                    {
3113                        log::info!(
3114                            "Pending OTO order {} triggers from {parent_order_id}",
3115                            order.client_order_id(),
3116                        );
3117
3118                        // A parent on another instrument triggers the child from its own engine
3119                        if parent_order.instrument_id() == self.instrument.id() {
3120                            self.pending_oto_order_ids.insert(order.client_order_id());
3121                        }
3122                        return;
3123                    }
3124
3125                    // This engine matches a child of another instrument that its parent releases
3126                    if order.instrument_id() != self.instrument.id() {
3127                        self.other_instrument_oto_order_ids
3128                            .insert(order.client_order_id());
3129                    }
3130                }
3131
3132                if let Some(linked_order_ids) = order.linked_order_ids() {
3133                    let contingency_type = order.contingency_type();
3134                    for client_order_id in linked_order_ids {
3135                        match cache_borrow.order(client_order_id) {
3136                            Some(contingent_order)
3137                                if matches!(
3138                                    contingency_type,
3139                                    Some(ContingencyType::Oco | ContingencyType::Ouo)
3140                                ) && !order.is_closed()
3141                                    && contingent_order.is_closed() =>
3142                            {
3143                                break 'validate Some(
3144                                    format!("Contingent order {client_order_id} already closed")
3145                                        .into(),
3146                                );
3147                            }
3148                            None => panic!("Cannot find contingent order for {client_order_id}"),
3149                            _ => {}
3150                        }
3151                    }
3152                }
3153            }
3154
3155            // Check for valid order quantity precision
3156            if !order_precision_valid(order.quantity().precision, self.instrument.size_precision())
3157            {
3158                break 'validate Some(
3159                    format!(
3160                        "Invalid order quantity precision for order {}, was {} when {} size precision is {}",
3161                        order.client_order_id(),
3162                        order.quantity().precision,
3163                        self.instrument.id(),
3164                        self.instrument.size_precision()
3165                    )
3166                    .into(),
3167                );
3168            }
3169
3170            // Check for valid order display quantity precision
3171            if let Some(display_qty) = order.display_qty()
3172                && !order_precision_valid(display_qty.precision, self.instrument.size_precision())
3173            {
3174                break 'validate Some(
3175                    format!(
3176                        "Invalid order display quantity precision for order {}, was {} when {} size precision is {}",
3177                        order.client_order_id(),
3178                        display_qty.precision,
3179                        self.instrument.id(),
3180                        self.instrument.size_precision()
3181                    )
3182                    .into(),
3183                );
3184            }
3185
3186            // Check order prices for precision and the price increment, off-tick prices never fill
3187            if let Some(reason) = order
3188                .price()
3189                .and_then(|price| self.order_price_rejection(order, "price", price))
3190            {
3191                break 'validate Some(reason);
3192            }
3193
3194            if let Some(reason) = order
3195                .trigger_price()
3196                .and_then(|price| self.order_price_rejection(order, "trigger price", price))
3197            {
3198                break 'validate Some(reason);
3199            }
3200
3201            if order.is_reduce_only() && !self.config.use_reduce_only {
3202                break 'validate Some(
3203                    "Reduce-only orders are not supported by this matching engine".into(),
3204                );
3205            }
3206
3207            None
3208        };
3209
3210        if let Some(reason) = reject_reason {
3211            self.generate_order_rejected(order, reason);
3212            return;
3213        }
3214
3215        if let Some(reason) = self
3216            .order_position_rejection(order)
3217            .unwrap_or_else(|e| Some(e.to_string().into()))
3218        {
3219            self.generate_order_rejected(order, reason);
3220            return;
3221        }
3222
3223        // Convert quote-denominated quantity to base quantity for non-inverse instruments.
3224        // Mirrors live venue semantics where the quote notional is settled into a base
3225        // quantity before the order enters normal fill and state handling. Without this
3226        // conversion the book simulation would treat the quote notional as base size.
3227        // Only applies to order types with a reliable reference price at submission;
3228        // trigger-style market orders and trailing orders are left untouched so they
3229        // convert at fill time from the actual (possibly-trailed) price.
3230        if order.is_quote_quantity()
3231            && !self.instrument.is_inverse()
3232            && !matches!(
3233                order.order_type(),
3234                OrderType::TrailingStopLimit | OrderType::TrailingStopMarket,
3235            )
3236            && (order.price().is_some()
3237                || matches!(
3238                    order.order_type(),
3239                    OrderType::Market | OrderType::MarketToLimit,
3240                ))
3241            && !self.convert_quote_to_base_quantity(order)
3242        {
3243            return;
3244        }
3245
3246        match order.order_type() {
3247            OrderType::Market => self.process_market_order(order),
3248            OrderType::Limit => self.process_limit_order(order),
3249            OrderType::MarketToLimit => self.process_market_to_limit_order(order),
3250            OrderType::StopMarket => self.process_stop_market_order(order),
3251            OrderType::StopLimit => self.process_stop_limit_order(order),
3252            OrderType::MarketIfTouched => self.process_market_if_touched_order(order),
3253            OrderType::LimitIfTouched => self.process_limit_if_touched_order(order),
3254            OrderType::TrailingStopMarket => self.process_trailing_stop_order(order),
3255            OrderType::TrailingStopLimit => self.process_trailing_stop_order(order),
3256        }
3257    }
3258
3259    fn order_position_rejection(&mut self, order: &OrderAny) -> anyhow::Result<Option<Ustr>> {
3260        let checks_reduce_only = self.config.use_reduce_only
3261            && order.is_reduce_only()
3262            && !order.is_closed()
3263            && !(self.restricts_short_selling() && order.is_sell());
3264
3265        if self.restricts_short_selling() || checks_reduce_only {
3266            self.purge_applied_fills();
3267        }
3268
3269        let cache = self.cache.borrow();
3270        let position = self.position_for_order_in_cache(&cache, order);
3271
3272        if let Some(reason) = self.cash_sell_rejection(&cache, order, position.as_ref())? {
3273            return Ok(Some(reason));
3274        }
3275
3276        if checks_reduce_only {
3277            // Includes fills the cache has not applied yet, so under deferred dispatch the exits
3278            // of an entry that filled earlier in the same submission see its position
3279            let quantity = self.position_quantity(&cache, order, position.as_ref())?;
3280
3281            if (order.is_buy() && quantity >= Decimal::ZERO)
3282                || (order.is_sell() && quantity <= Decimal::ZERO)
3283            {
3284                return Ok(Some(
3285                    format!(
3286                        "Reduce-only order {} ({}-{}) would have increased position",
3287                        order.client_order_id(),
3288                        order.order_type().to_string().to_uppercase(),
3289                        order.order_side().to_string().to_uppercase()
3290                    )
3291                    .into(),
3292                ));
3293            }
3294        }
3295
3296        Ok(None)
3297    }
3298
3299    fn restricts_short_selling(&self) -> bool {
3300        self.account_type != AccountType::Margin
3301            && matches!(
3302                self.instrument,
3303                InstrumentAny::Equity(_) | InstrumentAny::BinaryOption(_)
3304            )
3305    }
3306
3307    fn cash_sell_rejection(
3308        &self,
3309        cache: &Cache,
3310        order: &OrderAny,
3311        position: Option<&Position>,
3312    ) -> anyhow::Result<Option<Ustr>> {
3313        if !self.restricts_short_selling() || !order.is_sell() {
3314            return Ok(None);
3315        }
3316
3317        let quantity = if order.is_quote_quantity() {
3318            let reference_price = order.price().or(self.core.bid).ok_or_else(|| {
3319                anyhow::anyhow!(
3320                    "No market for {} to convert quote quantity to base",
3321                    order.instrument_id()
3322                )
3323            })?;
3324
3325            self.instrument
3326                .try_calculate_base_quantity(order.leaves_qty(), reference_price)?
3327        } else {
3328            order.leaves_qty()
3329        };
3330
3331        let position_quantity = self.position_quantity(cache, order, position)?;
3332
3333        if quantity.as_decimal() <= position_quantity {
3334            return Ok(None);
3335        }
3336
3337        let position_string = position.map_or("None".to_string(), |pos| pos.id.to_string());
3338        Ok(Some(
3339            format!(
3340                "Short selling not permitted on a CASH account with position {position_string} and order {order}",
3341            )
3342            .into(),
3343        ))
3344    }
3345
3346    fn order_price_rejection(&self, order: &OrderAny, field: &str, price: Price) -> Option<Ustr> {
3347        let price_precision = self.instrument.price_precision();
3348        if !order_precision_valid(price.precision, price_precision) {
3349            return Some(
3350                format!(
3351                    "Invalid order {field} precision for order {}, was {} when {} price precision is {price_precision}",
3352                    order.client_order_id(),
3353                    price.precision,
3354                    self.instrument.id(),
3355                )
3356                .into(),
3357            );
3358        }
3359
3360        let price_increment = self.instrument.price_increment();
3361        if !Self::price_matches_tick(price, price_increment) {
3362            return Some(
3363                format!(
3364                    "Invalid order {field} {price} for order {}, not a multiple of {} price increment {price_increment}",
3365                    order.client_order_id(),
3366                    self.instrument.id(),
3367                )
3368                .into(),
3369            );
3370        }
3371
3372        None
3373    }
3374
3375    fn convert_quote_to_base_quantity(&self, order: &mut OrderAny) -> bool {
3376        // Pick a reference price to convert the quote notional into a base quantity.
3377        // Priced orders use their own price (worst-case execution); marketable orders
3378        // use the best opposing book level.
3379        let reference_price = if let Some(price) = order.price() {
3380            Some(price)
3381        } else {
3382            match order.order_side() {
3383                OrderSide::Buy => self.core.ask,
3384                OrderSide::Sell => self.core.bid,
3385            }
3386        };
3387
3388        let Some(reference_price) = reference_price else {
3389            self.generate_order_rejected(
3390                order,
3391                format!(
3392                    "No market for {} to convert quote quantity to base",
3393                    order.instrument_id(),
3394                )
3395                .into(),
3396            );
3397            return false;
3398        };
3399
3400        let base_quantity = self
3401            .instrument
3402            .calculate_base_quantity(order.quantity(), reference_price);
3403
3404        let ts_now = self.clock.borrow().timestamp_ns();
3405        let update = OrderUpdated::new(
3406            order.trader_id(),
3407            order.strategy_id(),
3408            order.instrument_id(),
3409            order.client_order_id(),
3410            base_quantity,
3411            UUID4::new(),
3412            ts_now,
3413            ts_now,
3414            false,
3415            order.venue_order_id(),
3416            order.account_id(),
3417            None,
3418            None,
3419            None,
3420            false,
3421        );
3422        let event = OrderEventAny::Updated(update);
3423
3424        // Apply the update to the local order so subsequent dispatch uses the base
3425        // quantity immediately (the event is also dispatched to the execution engine
3426        // for cache reconciliation).
3427        if let Err(e) = order.apply(event.clone()) {
3428            log::error!(
3429                "Failed to apply quote-to-base update for {}: {e}",
3430                order.client_order_id(),
3431            );
3432            return false;
3433        }
3434
3435        // Later snapshots show the base quantity while a deferring handler holds the event
3436        self.pending_updates
3437            .borrow_mut()
3438            .entry(order.client_order_id())
3439            .or_default()
3440            .push(update);
3441        self.dispatch_order_event(event);
3442        true
3443    }
3444
3445    /// Restores an already-working order into the matching core without emitting
3446    /// acceptance events or running submission-time validation.
3447    ///
3448    /// Intended for repopulating the engine from a durable cache after a restart,
3449    /// where the order is already alive on the venue and its status (for example
3450    /// `Accepted`, `PartiallyFilled`, or `Triggered`) must be preserved. Ensures
3451    /// the cached order carries `LiquiditySide::Maker` when it would otherwise be
3452    /// unset so the fee model has a liquidity side to key on, and seeds
3453    /// `cached_filled_qty` with the existing filled quantity so subsequent fills
3454    /// stop at the remaining leaves.
3455    pub fn restore_open_order(&mut self, order: &OrderAny, account_id: AccountId) {
3456        if self.core.order_exists(order.client_order_id()) {
3457            return;
3458        }
3459
3460        if order.is_closed() {
3461            return;
3462        }
3463
3464        self.account_ids.insert(order.trader_id(), account_id);
3465
3466        if let Err(e) = self.ids_generator.get_venue_order_id(order) {
3467            log::error!(
3468                "Failed to resolve venue order ID restoring {}: {e}",
3469                order.client_order_id(),
3470            );
3471            return;
3472        }
3473
3474        // Persist Maker on the cached copy so the fill path (which reads via
3475        // order_snapshot) sees the liquidity side. Skip when persisted fills
3476        // already recorded Maker or Taker.
3477        if let Some(mut cached) = self.cache.borrow_mut().order_mut(&order.client_order_id())
3478            && !matches!(
3479                cached.liquidity_side(),
3480                Some(LiquiditySide::Maker | LiquiditySide::Taker)
3481            )
3482        {
3483            cached.set_liquidity_side(LiquiditySide::Maker);
3484        }
3485
3486        let filled_qty = order.filled_qty();
3487        if !filled_qty.is_zero() {
3488            self.cached_filled_qty
3489                .insert(order.client_order_id(), filled_qty);
3490        }
3491
3492        let match_info = Self::matching_core_entry(order);
3493        self.track_post_match_order(order);
3494        self.core.add_order(match_info);
3495    }
3496
3497    /// Processes an order modify command to update quantity, price, or trigger price.
3498    pub fn process_modify(&mut self, command: &ModifyOrder, account_id: AccountId) {
3499        if !self.core.order_exists(command.client_order_id) {
3500            self.generate_order_modify_rejected(
3501                command.trader_id,
3502                command.strategy_id,
3503                command.instrument_id,
3504                command.client_order_id,
3505                Ustr::from(format!("Order {} not found", command.client_order_id).as_str()),
3506                command.venue_order_id,
3507                Some(account_id),
3508            );
3509            return;
3510        }
3511
3512        let order = match self.order_snapshot(command.client_order_id) {
3513            Some(order) => order,
3514            None => {
3515                log::error!(
3516                    "Cannot modify order: order {} not found in cache",
3517                    command.client_order_id
3518                );
3519                return;
3520            }
3521        };
3522
3523        let update_success = self.update_order(
3524            &order,
3525            command.quantity,
3526            command.price,
3527            command.trigger_price,
3528            None,
3529        );
3530
3531        if !update_success {
3532            return;
3533        }
3534
3535        if !self.core.order_exists(command.client_order_id) {
3536            return;
3537        }
3538
3539        let Some(refreshed) = self.resync_core_entry(command.client_order_id) else {
3540            return;
3541        };
3542
3543        // Skip queue reset on rejected modifies to preserve accrued position
3544        let price_changed = refreshed.price() != order.price()
3545            || refreshed.trigger_price() != order.trigger_price();
3546
3547        if price_changed
3548            && refreshed.is_open()
3549            && self.config.queue_position
3550            && let Some(new_price) = refreshed.price()
3551        {
3552            self.snapshot_queue_position(&refreshed, new_price);
3553            self.queue_excess.swap_remove(&refreshed.client_order_id());
3554        }
3555    }
3556
3557    /// Processes an order cancel command.
3558    pub fn process_cancel(&mut self, command: &CancelOrder, account_id: AccountId) {
3559        // A parked OTO child is held by this engine without being in the core
3560        if !self.core.order_exists(command.client_order_id)
3561            && !self
3562                .pending_oto_order_ids
3563                .contains(&command.client_order_id)
3564        {
3565            // Live orders of this account that the engine does not hold, such as market order
3566            // remainders and OTO children of a parent on another instrument, cancel from the
3567            // cache as in `process_cancel_all`.
3568            let order = self
3569                .order_snapshot(command.client_order_id)
3570                .filter(|order| {
3571                    order.account_id() == Some(account_id)
3572                        && (order.is_inflight() || order.is_open())
3573                        && !self.inflight_orders.contains(order.client_order_id())
3574                });
3575
3576            if let Some(order) = order {
3577                self.cancel_order(&order, None);
3578                return;
3579            }
3580
3581            self.generate_order_cancel_rejected(
3582                command.trader_id,
3583                command.strategy_id,
3584                account_id,
3585                command.instrument_id,
3586                command.client_order_id,
3587                command.venue_order_id,
3588                Ustr::from(format!("Order {} not found", command.client_order_id).as_str()),
3589            );
3590            return;
3591        }
3592
3593        let mut order = match self.order_snapshot(command.client_order_id) {
3594            Some(order) => order,
3595            None => {
3596                log::error!(
3597                    "Cannot cancel order: order {} not found in cache",
3598                    command.client_order_id
3599                );
3600                return;
3601            }
3602        };
3603
3604        // Leave the order resting rather than purge it without an event
3605        if let Err(e) = self.apply_deferred_submission(&mut order, false) {
3606            self.generate_order_cancel_rejected(
3607                command.trader_id,
3608                command.strategy_id,
3609                account_id,
3610                command.instrument_id,
3611                command.client_order_id,
3612                command.venue_order_id,
3613                Ustr::from(e.to_string().as_str()),
3614            );
3615            return;
3616        }
3617
3618        if !order.is_inflight() && !order.is_open() {
3619            self.purge_stale_core_entry(command.client_order_id);
3620            return;
3621        }
3622
3623        self.cancel_order(&order, None);
3624    }
3625
3626    /// Processes a cancel all orders command for an instrument.
3627    ///
3628    /// Orders still awaiting venue receipt are left untouched.
3629    pub fn process_cancel_all(&mut self, command: &CancelAllOrders, account_id: AccountId) {
3630        self.process_cancel_all_excluding(command, account_id, &[]);
3631    }
3632
3633    /// Processes a cancel all orders command for an instrument, leaving `excluded` untouched,
3634    /// including when canceling an order cascades into its contingent orders.
3635    pub fn process_cancel_all_excluding(
3636        &mut self,
3637        command: &CancelAllOrders,
3638        account_id: AccountId,
3639        excluded: &[ClientOrderId],
3640    ) {
3641        let instrument_id = command.instrument_id;
3642        let order_side = command.order_side;
3643
3644        let mut client_order_ids: Vec<ClientOrderId> = {
3645            let cache = self.cache.borrow();
3646            cache
3647                .orders_open_refs(
3648                    None,
3649                    Some(&instrument_id),
3650                    None,
3651                    Some(&account_id),
3652                    order_side,
3653                )
3654                .into_iter()
3655                .chain(cache.orders_inflight_refs(
3656                    None,
3657                    Some(&instrument_id),
3658                    None,
3659                    Some(&account_id),
3660                    order_side,
3661                ))
3662                .map(|order| order.client_order_id())
3663                .filter(|client_order_id| !excluded.contains(client_order_id))
3664                .collect()
3665        };
3666        client_order_ids.sort_unstable();
3667        client_order_ids.dedup();
3668
3669        for client_order_id in client_order_ids {
3670            let order = match self
3671                .cache
3672                .borrow()
3673                .order(&client_order_id)
3674                .map(|o| o.clone())
3675            {
3676                Some(order) => order,
3677                None => continue,
3678            };
3679
3680            if !order.is_inflight() && !order.is_open() {
3681                self.purge_stale_core_entry(client_order_id);
3682                continue;
3683            }
3684
3685            self.cancel_order_excluding(&order, None, excluded);
3686        }
3687    }
3688
3689    // Removes a closed order's stale entry from the matching core so the next
3690    // `iterate_bids/asks` does not produce a spurious fill action.
3691    fn purge_stale_core_entry(&mut self, client_order_id: ClientOrderId) {
3692        if self.core.order_exists(client_order_id) {
3693            self.delete_core_order(client_order_id);
3694        }
3695
3696        self.remove_queue_position(client_order_id);
3697        self.cached_filled_qty.swap_remove(&client_order_id);
3698    }
3699
3700    fn resync_core_entry(&mut self, client_order_id: ClientOrderId) -> Option<OrderAny> {
3701        let order = self.order_snapshot(client_order_id)?;
3702
3703        // Gate on `is_closed`, not `is_open`: cache may transiently hold the
3704        // order in `Submitted` (process_limit_order accepts before cache add)
3705        if order.is_closed() {
3706            self.delete_core_order(client_order_id);
3707            self.remove_queue_position(client_order_id);
3708            return Some(order);
3709        }
3710
3711        let new_match_info = Self::matching_core_entry(&order);
3712
3713        // Skip the delete+add when unchanged to preserve FIFO at the level
3714        let unchanged = self
3715            .core
3716            .get_order(client_order_id)
3717            .is_some_and(|existing| *existing == new_match_info);
3718
3719        if unchanged {
3720            self.track_post_match_order(&order);
3721            return Some(order);
3722        }
3723
3724        self.delete_core_order(client_order_id);
3725        self.track_post_match_order(&order);
3726        self.core.add_order(new_match_info);
3727        Some(order)
3728    }
3729
3730    fn order_snapshot(&self, client_order_id: ClientOrderId) -> Option<OrderAny> {
3731        let mut order = self.cache.borrow().order(&client_order_id)?.clone();
3732        let mut pending = self.pending_updates.borrow_mut();
3733
3734        if order.is_closed() {
3735            pending.swap_remove(&client_order_id);
3736            return Some(order);
3737        }
3738
3739        if let Some(updates) = pending.get_mut(&client_order_id) {
3740            Self::retain_unapplied_updates(&order, updates);
3741
3742            for update in updates.iter() {
3743                if let Err(e) = order.apply(OrderEventAny::Updated(*update)) {
3744                    log::error!("Cannot apply pending update for {client_order_id}: {e}");
3745                    return None;
3746                }
3747            }
3748
3749            if updates.is_empty() {
3750                pending.swap_remove(&client_order_id);
3751            }
3752        }
3753
3754        if let Some(filled_qty) = self.cached_filled_qty.get(&client_order_id) {
3755            write_filled_qty(&mut order, *filled_qty);
3756            order.set_leaves_qty(order.quantity().saturating_sub(*filled_qty));
3757        }
3758
3759        Some(order)
3760    }
3761
3762    fn purge_applied_updates(&self) {
3763        let cache = self.cache.borrow();
3764        self.pending_updates.borrow_mut().retain(|id, updates| {
3765            let Some(order) = cache.order(id) else {
3766                return false;
3767            };
3768
3769            Self::retain_unapplied_updates(&order, updates);
3770            !updates.is_empty()
3771        });
3772    }
3773
3774    fn purge_applied_rejections(&mut self) {
3775        let cache = self.cache.borrow();
3776        self.rejected_oto_parent_ids.get_mut().retain(|id| {
3777            cache
3778                .order(id)
3779                .is_some_and(|order| order.status() != OrderStatus::Rejected)
3780        });
3781    }
3782
3783    fn purge_applied_cancels(&mut self) {
3784        let cache = self.cache.borrow();
3785        self.canceled_oto_order_ids
3786            .retain(|id| cache.order(id).is_some_and(|order| !order.is_closed()));
3787    }
3788
3789    fn retain_unapplied_updates(order: &OrderAny, updates: &mut Vec<OrderUpdated>) {
3790        if order.is_closed() {
3791            updates.clear();
3792            return;
3793        }
3794
3795        let events = order.events();
3796        updates.retain(|update| {
3797            !events.iter().any(|event| {
3798                matches!(event, OrderEventAny::Updated(applied) if applied.event_id == update.event_id)
3799            })
3800        });
3801    }
3802
3803    /// Processes a batch cancel orders command.
3804    pub fn process_batch_cancel(&mut self, command: &BatchCancelOrders, account_id: AccountId) {
3805        for order in &command.cancels {
3806            self.process_cancel(order, account_id);
3807        }
3808    }
3809
3810    /// Processes a batch modify orders command.
3811    pub fn process_batch_modify(&mut self, command: &BatchModifyOrders, account_id: AccountId) {
3812        for order in &command.modifies {
3813            self.process_modify(order, account_id);
3814        }
3815    }
3816
3817    fn process_market_order(&mut self, order: &OrderAny) {
3818        if order.time_in_force() == TimeInForce::AtTheOpen
3819            || order.time_in_force() == TimeInForce::AtTheClose
3820        {
3821            self.generate_order_rejected(
3822                order,
3823                format!(
3824                    "time in force {} is not currently supported",
3825                    order.time_in_force()
3826                )
3827                .into(),
3828            );
3829            return;
3830        }
3831
3832        // Check if market exists
3833        if (order.order_side() == OrderSide::Buy && self.core.ask.is_none())
3834            || (order.order_side() == OrderSide::Sell && self.core.bid.is_none())
3835        {
3836            self.generate_order_rejected(
3837                order,
3838                format!("No market for {}", order.instrument_id()).into(),
3839            );
3840            return;
3841        }
3842
3843        if self.config.use_market_order_acks {
3844            let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
3845            self.generate_order_accepted(order, venue_order_id);
3846        }
3847
3848        self.fill_market_order(order.client_order_id());
3849    }
3850
3851    fn process_limit_order(&mut self, order: &mut OrderAny) {
3852        if order.time_in_force() == TimeInForce::AtTheOpen
3853            || order.time_in_force() == TimeInForce::AtTheClose
3854        {
3855            self.generate_order_rejected(
3856                order,
3857                format!(
3858                    "time in force {} is not currently supported",
3859                    order.time_in_force()
3860                )
3861                .into(),
3862            );
3863            return;
3864        }
3865
3866        let limit_px = order.price().expect("Limit order must have a price");
3867        if order.is_post_only() && self.core.is_limit_matched(order.order_side(), limit_px) {
3868            self.generate_order_rejected(
3869                order,
3870                format!(
3871                    "POST_ONLY {} {} order limit px of {} would have been a TAKER: bid={}, ask={}",
3872                    order.order_type(),
3873                    order.order_side(),
3874                    order.price().unwrap(),
3875                    self.core
3876                        .bid
3877                        .map_or_else(|| "None".to_string(), |p| p.to_string()),
3878                    self.core
3879                        .ask
3880                        .map_or_else(|| "None".to_string(), |p| p.to_string())
3881                )
3882                .into(),
3883            );
3884            return;
3885        }
3886
3887        // Order is valid and accepted
3888        self.accept_order(order);
3889
3890        // Check for immediate fill
3891        if self.core.is_limit_matched(order.order_side(), limit_px) {
3892            // Filling as liquidity taker
3893            order.set_liquidity_side(LiquiditySide::Taker);
3894            self.fill_limit_order_with_snapshot(order.clone());
3895
3896            // If fill didn't execute (e.g. all liquidity consumed), revert to
3897            // maker so the fill model check applies on subsequent iterations
3898            if self.core.order_exists(order.client_order_id())
3899                && let Some(mut order) = self.cache.borrow_mut().order_mut(&order.client_order_id())
3900            {
3901                order.set_liquidity_side(LiquiditySide::Maker);
3902            }
3903        } else if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc) {
3904            self.cancel_order(order, None);
3905        } else {
3906            if let Some(price) = order.price() {
3907                self.snapshot_queue_position(order, price);
3908            }
3909
3910            // `order` is the caller's copy, so record Maker on the cached order
3911            if let Some(mut order) = self.cache.borrow_mut().order_mut(&order.client_order_id())
3912                && !matches!(
3913                    order.liquidity_side(),
3914                    Some(LiquiditySide::Maker | LiquiditySide::Taker)
3915                )
3916            {
3917                order.set_liquidity_side(LiquiditySide::Maker);
3918            }
3919        }
3920    }
3921
3922    fn process_market_to_limit_order(&mut self, order: &OrderAny) {
3923        // Check that market exists
3924        if (order.order_side() == OrderSide::Buy && self.core.ask.is_none())
3925            || (order.order_side() == OrderSide::Sell && self.core.bid.is_none())
3926        {
3927            self.generate_order_rejected(
3928                order,
3929                format!("No market for {}", order.instrument_id()).into(),
3930            );
3931            return;
3932        }
3933
3934        if self.config.use_market_order_acks {
3935            let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
3936            self.generate_order_accepted(order, venue_order_id);
3937        }
3938
3939        // Immediately fill marketable order
3940        let client_order_id = order.client_order_id();
3941        self.fill_market_order(client_order_id);
3942
3943        // Check for remaining quantity to rest as limit order
3944        let filled_qty = self
3945            .cached_filled_qty
3946            .get(&client_order_id)
3947            .copied()
3948            .unwrap_or_default();
3949        let leaves_qty = order.quantity().saturating_sub(filled_qty);
3950        if leaves_qty.is_zero() {
3951            self.purge_cached_filled_qty_if_closed(client_order_id);
3952            return;
3953        }
3954
3955        if let Some(mut updated_order) = self.order_snapshot(client_order_id) {
3956            self.accept_order(&mut updated_order);
3957        }
3958    }
3959
3960    fn process_stop_market_order(&mut self, order: &mut OrderAny) {
3961        let stop_px = order
3962            .trigger_price()
3963            .expect("Stop order must have a trigger price");
3964
3965        if self.core.is_stop_matched_with_trigger_type(
3966            order.order_side(),
3967            stop_px,
3968            order.trigger_type().unwrap_or(TriggerType::Default),
3969        ) {
3970            if self.config.reject_stop_orders {
3971                self.generate_order_rejected(
3972                    order,
3973                    format!(
3974                        "{} {} order stop px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3975                        order.order_type(),
3976                        order.order_side(),
3977                        order.trigger_price().unwrap(),
3978                        self.core
3979                            .bid
3980                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
3981                        self.core
3982                            .ask
3983                            .map_or_else(|| "None".to_string(), |p| p.to_string())
3984                    ).into(),
3985                );
3986                return;
3987            }
3988
3989            self.fill_market_order(order.client_order_id());
3990            return;
3991        }
3992
3993        // order is not matched but is valid and we accept it
3994        self.accept_order(order);
3995    }
3996
3997    fn process_stop_limit_order(&mut self, order: &mut OrderAny) {
3998        let stop_px = order
3999            .trigger_price()
4000            .expect("Stop order must have a trigger price");
4001
4002        if self.core.is_stop_matched_with_trigger_type(
4003            order.order_side(),
4004            stop_px,
4005            order.trigger_type().unwrap_or(TriggerType::Default),
4006        ) {
4007            if self.config.reject_stop_orders {
4008                self.generate_order_rejected(
4009                    order,
4010                    format!(
4011                        "{} {} order stop px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
4012                        order.order_type(),
4013                        order.order_side(),
4014                        order.trigger_price().unwrap(),
4015                        self.core
4016                            .bid
4017                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
4018                        self.core
4019                            .ask
4020                            .map_or_else(|| "None".to_string(), |p| p.to_string())
4021                    ).into(),
4022                );
4023                return;
4024            }
4025
4026            self.accept_triggered_limit_style_order(order);
4027            return;
4028        }
4029
4030        self.accept_order(order);
4031    }
4032
4033    fn process_market_if_touched_order(&mut self, order: &mut OrderAny) {
4034        if self.core.is_touch_triggered_with_trigger_type(
4035            order.order_side(),
4036            order.trigger_price().unwrap(),
4037            order.trigger_type().unwrap_or(TriggerType::Default),
4038        ) {
4039            if self.config.reject_stop_orders {
4040                self.generate_order_rejected(
4041                    order,
4042                    format!(
4043                        "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
4044                        order.order_type(),
4045                        order.order_side(),
4046                        order.trigger_price().unwrap(),
4047                        self.core
4048                            .bid
4049                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
4050                        self.core
4051                            .ask
4052                            .map_or_else(|| "None".to_string(), |p| p.to_string())
4053                    ).into(),
4054                );
4055                return;
4056            }
4057
4058            self.fill_market_order(order.client_order_id());
4059            return;
4060        }
4061
4062        // Order is valid and accepted
4063        self.accept_order(order);
4064    }
4065
4066    fn process_limit_if_touched_order(&mut self, order: &mut OrderAny) {
4067        if self.core.is_touch_triggered_with_trigger_type(
4068            order.order_side(),
4069            order.trigger_price().unwrap(),
4070            order.trigger_type().unwrap_or(TriggerType::Default),
4071        ) {
4072            if self.config.reject_stop_orders {
4073                self.generate_order_rejected(
4074                    order,
4075                    format!(
4076                        "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
4077                        order.order_type(),
4078                        order.order_side(),
4079                        order.trigger_price().unwrap(),
4080                        self.core
4081                            .bid
4082                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
4083                        self.core
4084                            .ask
4085                            .map_or_else(|| "None".to_string(), |p| p.to_string())
4086                    ).into(),
4087                );
4088                return;
4089            }
4090            self.accept_triggered_limit_style_order(order);
4091            return;
4092        }
4093
4094        // Order is valid and accepted
4095        self.accept_order(order);
4096    }
4097
4098    fn accept_triggered_limit_style_order(&mut self, order: &mut OrderAny) {
4099        self.accept_order(order);
4100        self.trigger_limit_style_stop_order(order.client_order_id(), order.clone());
4101
4102        if let Some(cached_order) = self
4103            .cache
4104            .borrow()
4105            .order(&order.client_order_id())
4106            .map(|order| order.clone())
4107        {
4108            *order = cached_order;
4109        }
4110    }
4111
4112    fn process_trailing_stop_order(&mut self, order: &mut OrderAny) {
4113        let side = order.order_side();
4114        let trigger_type = order.trigger_type().unwrap_or(TriggerType::Default);
4115
4116        if let Some(activation_price) = order.activation_price()
4117            && self.core.is_touch_triggered(side, activation_price)
4118            && self.config.reject_stop_orders
4119        {
4120            self.generate_order_rejected(
4121                order,
4122                format!(
4123                    "{} {} order activation px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
4124                    order.order_type(),
4125                    side,
4126                    activation_price,
4127                    self.core
4128                        .bid
4129                        .map_or_else(|| "None".to_string(), |p| p.to_string()),
4130                    self.core
4131                        .ask
4132                        .map_or_else(|| "None".to_string(), |p| p.to_string())
4133                )
4134                .into(),
4135            );
4136            return;
4137        }
4138
4139        if self.trailing_stop_activates_now(order)
4140            && let Some(trigger_price) = order.trigger_price()
4141            && self
4142                .core
4143                .is_stop_matched_with_trigger_type(side, trigger_price, trigger_type)
4144        {
4145            self.generate_order_rejected(
4146                order,
4147                format!(
4148                    "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
4149                    order.order_type(),
4150                    side,
4151                    trigger_price,
4152                    self.core
4153                        .bid
4154                        .map_or_else(|| "None".to_string(), |p| p.to_string()),
4155                    self.core
4156                        .ask
4157                        .map_or_else(|| "None".to_string(), |p| p.to_string())
4158                )
4159                .into(),
4160            );
4161            return;
4162        }
4163
4164        // Set Maker before `accept_order` so trail-on-accept's cache write
4165        // captures it (a later `set_liquidity_side` would be dropped by the
4166        // `add_order` no-op below).
4167        order.set_liquidity_side(LiquiditySide::Maker);
4168
4169        self.accept_order(order);
4170    }
4171
4172    /// Returns whether a trailing stop activates at the current market: its activation price is
4173    /// touched, or it has none and a market price to activate at is available.
4174    fn trailing_stop_activates_now(&self, order: &OrderAny) -> bool {
4175        let side = order.order_side();
4176        match order.activation_price() {
4177            Some(activation_price) => self.core.is_touch_triggered(side, activation_price),
4178            None => self
4179                .get_trailing_activation_price(
4180                    order.trigger_type().unwrap_or(TriggerType::Default),
4181                    side,
4182                    self.core.bid,
4183                    self.core.ask,
4184                    self.core.last,
4185                )
4186                .is_some(),
4187        }
4188    }
4189
4190    /// Iterate the matching engine by processing the bid and ask order sides
4191    /// and advancing time up to the given UNIX `timestamp_ns`.
4192    ///
4193    /// The `aggressor_side` parameter is used for trade execution processing.
4194    /// When not `NoAggressor`, the book-based bid/ask reset is skipped to preserve
4195    /// transient trade price overrides.
4196    pub fn iterate(&mut self, timestamp_ns: UnixNanos, aggressor_side: AggressorSide) {
4197        self.iterate_with_mode(timestamp_ns, aggressor_side, OrderMatchMode::All);
4198    }
4199
4200    fn iterate_with_mode(
4201        &mut self,
4202        timestamp_ns: UnixNanos,
4203        aggressor_side: AggressorSide,
4204        match_mode: OrderMatchMode,
4205    ) {
4206        // TODO implement correct clock fixed time setting self.clock.set_time(ts_now);
4207        self.purge_closed_cached_filled_qty();
4208        self.purge_applied_updates();
4209        self.purge_applied_fills();
4210        self.purge_applied_rejections();
4211        self.purge_applied_cancels();
4212
4213        // Only reset bid/ask from book when not processing trade execution
4214        // (preserves transient trade price override for L2/L3 books). The
4215        // `last_trade_size` gate covers the no-aggressor trade-tick path
4216        // where `process_trade_tick` overrides both sides to the trade
4217        // price; without it the override is undone here.
4218        if aggressor_side == AggressorSide::NoAggressor && self.last_trade_size.is_none() {
4219            if self.book_type == BookType::L1_MBP {
4220                if let Some(bid) = self.book.best_bid_price() {
4221                    self.core.set_bid_raw(bid);
4222                }
4223
4224                if let Some(ask) = self.book.best_ask_price() {
4225                    self.core.set_ask_raw(ask);
4226                }
4227            } else {
4228                // L2/L3 books are authoritative. Assigning the complete options
4229                // propagates an empty side before matching and prevents fills
4230                // or triggers from a stale touch.
4231                self.core.bid = self.book.best_bid_price();
4232                self.core.ask = self.book.best_ask_price();
4233            }
4234        }
4235
4236        let mut matched_order = false;
4237
4238        if self.market_status == MarketStatus::Open {
4239            // Process bid actions before snapshotting asks so cross-side
4240            // contingencies (OCO/OUO) mutate state between sides
4241            for action in self.core.iterate_bids() {
4242                if !self.should_process_match_action(action, match_mode) {
4243                    continue;
4244                }
4245
4246                matched_order = true;
4247
4248                match action {
4249                    MatchAction::FillLimit(id) => self.fill_resting_limit_order(id),
4250                    MatchAction::TriggerStop(id) => self.trigger_stop_order(id),
4251                }
4252            }
4253
4254            for action in self.core.iterate_asks() {
4255                if !self.should_process_match_action(action, match_mode) {
4256                    continue;
4257                }
4258
4259                matched_order = true;
4260
4261                match action {
4262                    MatchAction::FillLimit(id) => self.fill_resting_limit_order(id),
4263                    MatchAction::TriggerStop(id) => self.trigger_stop_order(id),
4264                }
4265            }
4266        }
4267
4268        let order_ids: Vec<ClientOrderId> = if matched_order {
4269            self.core.iter_orders().map(|m| m.client_order_id).collect()
4270        } else if self.post_match_order_ids.is_empty() {
4271            Vec::new()
4272        } else {
4273            self.core
4274                .iter_orders()
4275                .filter_map(|order| {
4276                    self.post_match_order_ids
4277                        .contains(&order.client_order_id)
4278                        .then_some(order.client_order_id)
4279                })
4280                .collect()
4281        };
4282
4283        let support_gtd_orders = self.config.support_gtd_orders;
4284
4285        for client_order_id in order_ids {
4286            let (action, keep_tracking) = {
4287                let cache = self.cache.borrow();
4288                let Some(order) = cache.order(&client_order_id) else {
4289                    self.post_match_order_ids.swap_remove(&client_order_id);
4290                    continue;
4291                };
4292
4293                (
4294                    post_match_order_action(&order, support_gtd_orders, timestamp_ns, |order| {
4295                        self.order_snapshot(client_order_id)
4296                            .unwrap_or_else(|| order.clone())
4297                    }),
4298                    Self::requires_post_match_maintenance(&order),
4299                )
4300            };
4301
4302            match action {
4303                PostMatchOrderAction::RemoveClosed => {
4304                    self.delete_core_order(client_order_id);
4305                    self.remove_queue_position(client_order_id);
4306                    self.cached_filled_qty.swap_remove(&client_order_id);
4307                    continue;
4308                }
4309                PostMatchOrderAction::Expire(order) => {
4310                    self.delete_core_order(client_order_id);
4311                    self.expire_order(&order);
4312                    self.purge_cached_filled_qty_if_closed(client_order_id);
4313                    continue;
4314                }
4315                PostMatchOrderAction::UpdateTrailing(mut order) => {
4316                    if self.maybe_activate_trailing_stop(
4317                        &mut order,
4318                        self.core.bid,
4319                        self.core.ask,
4320                        self.core.last,
4321                    ) {
4322                        self.update_trailing_stop_order(&order);
4323                        self.resync_core_entry(client_order_id);
4324                    }
4325                }
4326                PostMatchOrderAction::NoMaintenance => {
4327                    if !keep_tracking {
4328                        self.post_match_order_ids.swap_remove(&client_order_id);
4329                    }
4330                }
4331            }
4332
4333            // Single-shot: only the first order after a trigger fill sees
4334            // the mutated core; the restore clears the override here.
4335            if self.target_bid.is_some() || self.target_ask.is_some() || self.target_last.is_some()
4336            {
4337                if let Some(t) = self.target_bid.take() {
4338                    self.core.bid = Some(t);
4339                }
4340
4341                if let Some(t) = self.target_ask.take() {
4342                    self.core.ask = Some(t);
4343                }
4344
4345                if let Some(t) = self.target_last.take() {
4346                    self.core.last = Some(t);
4347                }
4348            }
4349        }
4350
4351        // Fallback for when the per-order loop hit no eligible order (e.g.,
4352        // all closed by the matching pass) so the fill override on
4353        // `core.last` cannot leak into the next iterate.
4354        if let Some(t) = self.target_bid.take() {
4355            self.core.bid = Some(t);
4356        }
4357
4358        if let Some(t) = self.target_ask.take() {
4359            self.core.ask = Some(t);
4360        }
4361
4362        if let Some(t) = self.target_last.take() {
4363            self.core.last = Some(t);
4364        }
4365
4366        // Restore core bid/ask to book values after iteration
4367        // (during trade execution, transient override was used for matching)
4368        self.core.bid = self.book.best_bid_price();
4369        self.core.ask = self.book.best_ask_price();
4370
4371        // Process instrument expiration last so orders at the expiration tick
4372        // get a chance to fill before positions are closed.
4373        self.check_instrument_expiration(timestamp_ns, self.config.defer_option_settlement, &[]);
4374        self.purge_closed_cached_filled_qty();
4375        self.purge_applied_updates();
4376        self.purge_applied_fills();
4377        self.purge_applied_rejections();
4378        self.purge_applied_cancels();
4379    }
4380
4381    fn fill_resting_limit_order(&mut self, client_order_id: ClientOrderId) {
4382        // A market-to-limit remainder rests as maker after its initial taker fill
4383        if self
4384            .core
4385            .get_order(client_order_id)
4386            .is_some_and(|order| order.order_type == OrderType::MarketToLimit)
4387            && let Some(mut order) = self.cache.borrow_mut().order_mut(&client_order_id)
4388        {
4389            order.set_liquidity_side(LiquiditySide::Maker);
4390        }
4391        self.fill_limit_order(client_order_id);
4392    }
4393
4394    fn should_process_match_action(&self, action: MatchAction, match_mode: OrderMatchMode) -> bool {
4395        let client_order_id = match action {
4396            MatchAction::FillLimit(id) | MatchAction::TriggerStop(id) => id,
4397        };
4398
4399        if !self.core.order_exists(client_order_id) {
4400            return false;
4401        }
4402
4403        match match_mode {
4404            OrderMatchMode::All => true,
4405            OrderMatchMode::LastPriceStopTriggers => match action {
4406                MatchAction::TriggerStop(client_order_id) => self
4407                    .core
4408                    .get_order(client_order_id)
4409                    .is_some_and(|order| order.trigger_type == Some(TriggerType::LastPrice)),
4410                MatchAction::FillLimit(_) => false,
4411            },
4412        }
4413    }
4414
4415    fn get_trailing_activation_price(
4416        &self,
4417        trigger_type: TriggerType,
4418        order_side: OrderSide,
4419        bid: Option<Price>,
4420        ask: Option<Price>,
4421        last: Option<Price>,
4422    ) -> Option<Price> {
4423        match trigger_type {
4424            TriggerType::LastPrice => last,
4425            TriggerType::LastOrBidAsk => last.or(match order_side {
4426                OrderSide::Buy => ask,
4427                OrderSide::Sell => bid,
4428            }),
4429
4430            // Default, BidAsk, DoubleBidAsk, DoubleLastPrice, IndexPrice, MarkPrice
4431            _ => match order_side {
4432                OrderSide::Buy => ask,
4433                OrderSide::Sell => bid,
4434            },
4435        }
4436    }
4437
4438    fn maybe_activate_trailing_stop(
4439        &self,
4440        order: &mut OrderAny,
4441        bid: Option<Price>,
4442        ask: Option<Price>,
4443        last: Option<Price>,
4444    ) -> bool {
4445        match order {
4446            OrderAny::TrailingStopMarket(inner) => {
4447                if inner.is_activated {
4448                    return true;
4449                }
4450
4451                if inner.activation_price.is_none() {
4452                    let px = self.get_trailing_activation_price(
4453                        inner.trigger_type,
4454                        inner.order_side(),
4455                        bid,
4456                        ask,
4457                        last,
4458                    );
4459
4460                    if let Some(p) = px {
4461                        inner.activation_price = Some(p);
4462                        inner.set_activated();
4463
4464                        store_trailing_stop_activation(&mut self.cache.borrow_mut(), order);
4465                        return true;
4466                    }
4467                    return false;
4468                }
4469
4470                let activation_price = inner.activation_price.unwrap();
4471                let hit = match inner.order_side() {
4472                    OrderSide::Buy => ask.is_some_and(|a| a <= activation_price),
4473                    OrderSide::Sell => bid.is_some_and(|b| b >= activation_price),
4474                };
4475
4476                if hit {
4477                    inner.set_activated();
4478
4479                    store_trailing_stop_activation(&mut self.cache.borrow_mut(), order);
4480                }
4481                hit
4482            }
4483            OrderAny::TrailingStopLimit(inner) => {
4484                if inner.is_activated {
4485                    return true;
4486                }
4487
4488                if inner.activation_price.is_none() {
4489                    let px = self.get_trailing_activation_price(
4490                        inner.trigger_type,
4491                        inner.order_side(),
4492                        bid,
4493                        ask,
4494                        last,
4495                    );
4496
4497                    if let Some(p) = px {
4498                        inner.activation_price = Some(p);
4499                        inner.set_activated();
4500
4501                        store_trailing_stop_activation(&mut self.cache.borrow_mut(), order);
4502                        return true;
4503                    }
4504                    return false;
4505                }
4506
4507                let activation_price = inner.activation_price.unwrap();
4508                let hit = match inner.order_side() {
4509                    OrderSide::Buy => ask.is_some_and(|a| a <= activation_price),
4510                    OrderSide::Sell => bid.is_some_and(|b| b >= activation_price),
4511                };
4512
4513                if hit {
4514                    inner.set_activated();
4515
4516                    store_trailing_stop_activation(&mut self.cache.borrow_mut(), order);
4517                }
4518                hit
4519            }
4520            _ => true,
4521        }
4522    }
4523
4524    fn determine_limit_price_and_volume(
4525        &mut self,
4526        order: &OrderAny,
4527        journal: Option<&mut ConsumptionJournal>,
4528    ) -> Vec<(Price, Quantity)> {
4529        match order.price() {
4530            Some(order_price) => {
4531                // When liquidity consumption is enabled, get ALL crossed levels so that
4532                // consumed levels can be filtered out while still finding valid ones.
4533                // Otherwise simulate_fills only returns enough levels to satisfy leaves_qty,
4534                // which may all be consumed, missing other valid crossed levels.
4535                let mut fills = if self.config.liquidity_consumption {
4536                    let size_prec = self.instrument.size_precision();
4537                    self.book
4538                        .get_all_crossed_levels(order.order_side(), order_price, size_prec)
4539                } else {
4540                    let book_order =
4541                        BookOrder::new(order.order_side(), order_price, order.quantity(), 1);
4542                    self.book.simulate_fills(&book_order)
4543                };
4544
4545                // L1 trade updates replace book levels, so use the per-trade budget
4546                if let Some(trade_size) = self.last_trade_size
4547                    && let Some(trade_price) = self.core.last
4548                {
4549                    let fills_at_trade_price = fills.iter().any(|(px, _)| *px == trade_price);
4550
4551                    if (self.book_type == BookType::L1_MBP || !fills_at_trade_price)
4552                        && self.core.is_limit_matched(order.order_side(), order_price)
4553                    {
4554                        // Fill model check for MAKER at limit is already handled in fill_limit_order,
4555                        // don't re-check here to avoid calling is_limit_filled() twice (p² probability).
4556                        let leaves_qty = order.leaves_qty();
4557                        let available_qty = if self.config.liquidity_consumption {
4558                            let remaining = trade_size.raw().saturating_sub(self.trade_consumption);
4559                            Quantity::from_raw(remaining, trade_size.precision)
4560                        } else {
4561                            trade_size
4562                        };
4563
4564                        let fill_qty = min(leaves_qty, available_qty);
4565
4566                        if fill_qty.non_zero() {
4567                            let fill_price = if self.book_type == BookType::L1_MBP
4568                                && fills_at_trade_price
4569                                && order.liquidity_side() == Some(LiquiditySide::Taker)
4570                            {
4571                                trade_price
4572                            } else {
4573                                order_price
4574                            };
4575
4576                            log::debug!(
4577                                "Trade execution fill: {} @ {} (trade_price={}, available: {}, book had {} fills)",
4578                                fill_qty,
4579                                fill_price,
4580                                trade_price,
4581                                available_qty,
4582                                fills.len()
4583                            );
4584
4585                            if self.config.liquidity_consumption {
4586                                self.trade_consumption += fill_qty.raw();
4587                            }
4588
4589                            // The trade budget already accounts for consumption, so bypass
4590                            // persistent book consumption for this event's liquidity.
4591                            return vec![(fill_price, fill_qty)];
4592                        }
4593
4594                        if self.book_type == BookType::L1_MBP {
4595                            return Vec::new();
4596                        }
4597                    }
4598                }
4599
4600                // Return immediately if no fills
4601                if fills.is_empty() {
4602                    return fills;
4603                }
4604
4605                // Save original book prices BEFORE any fill price modifications for consumption tracking,
4606                // since the MAKER loop below may adjust fill prices. Consumption should be
4607                // tracked against the original book price levels where liquidity was sourced from.
4608                let book_prices: Vec<Price> = if self.config.liquidity_consumption {
4609                    fills.iter().map(|(px, _)| *px).collect()
4610                } else {
4611                    Vec::new()
4612                };
4613
4614                let book_prices_ref: Option<&[Price]> = if book_prices.is_empty() {
4615                    None
4616                } else {
4617                    Some(&book_prices)
4618                };
4619
4620                // Filling as MAKER from trigger
4621                if order
4622                    .liquidity_side()
4623                    .is_some_and(|liquidity_side| liquidity_side == LiquiditySide::Maker)
4624                {
4625                    match order.order_side() {
4626                        OrderSide::Buy => {
4627                            let target_price = if order
4628                                .trigger_price()
4629                                .is_some_and(|trigger_price| order_price > trigger_price)
4630                            {
4631                                order.trigger_price().unwrap()
4632                            } else {
4633                                order_price
4634                            };
4635
4636                            for fill in &mut fills {
4637                                let last_px = fill.0;
4638                                if last_px < order_price {
4639                                    // Marketable BUY would have filled at limit
4640                                    self.target_bid = self.core.bid;
4641                                    self.target_ask = self.core.ask;
4642                                    self.target_last = self.core.last;
4643                                    self.core.set_ask_raw(target_price);
4644                                    self.core.set_last_raw(target_price);
4645                                    fill.0 = target_price;
4646                                }
4647                            }
4648                        }
4649                        OrderSide::Sell => {
4650                            let target_price = if order
4651                                .trigger_price()
4652                                .is_some_and(|trigger_price| order_price < trigger_price)
4653                            {
4654                                order.trigger_price().unwrap()
4655                            } else {
4656                                order_price
4657                            };
4658
4659                            for fill in &mut fills {
4660                                let last_px = fill.0;
4661                                if last_px > order_price {
4662                                    // Marketable SELL would have filled at limit
4663                                    self.target_bid = self.core.bid;
4664                                    self.target_ask = self.core.ask;
4665                                    self.target_last = self.core.last;
4666                                    self.core.set_bid_raw(target_price);
4667                                    self.core.set_last_raw(target_price);
4668                                    fill.0 = target_price;
4669                                }
4670                            }
4671                        }
4672                    }
4673                }
4674
4675                self.apply_liquidity_consumption(
4676                    fills,
4677                    order.order_side(),
4678                    order.leaves_qty(),
4679                    book_prices_ref,
4680                    self.l1_trade_size,
4681                    journal,
4682                )
4683            }
4684            None => panic!("Limit order must have a price"),
4685        }
4686    }
4687
4688    fn determine_market_price_and_volume(&self, order: &OrderAny) -> Vec<(Price, Quantity)> {
4689        let price = match order.order_side() {
4690            OrderSide::Buy => Price::max(FIXED_PRECISION),
4691            OrderSide::Sell => Price::min(FIXED_PRECISION),
4692        };
4693
4694        // When liquidity consumption is enabled, get ALL crossed levels so that
4695        // consumed levels can be filtered out while still finding valid ones.
4696        let mut fills = if self.config.liquidity_consumption {
4697            let size_prec = self.instrument.size_precision();
4698            self.book
4699                .get_all_crossed_levels(order.order_side(), price, size_prec)
4700        } else {
4701            let book_order = BookOrder::new(order.order_side(), price, order.quantity(), 0);
4702            self.book.simulate_fills(&book_order)
4703        };
4704
4705        // For stop market and market-if-touched orders during bar H/L/C processing, fill at trigger price
4706        // (market moved through the trigger). For gaps/immediate triggers, fill at market.
4707        if !self.fill_at_market
4708            && self.book_type == BookType::L1_MBP
4709            && !fills.is_empty()
4710            && matches!(
4711                order.order_type(),
4712                OrderType::StopMarket | OrderType::TrailingStopMarket | OrderType::MarketIfTouched
4713            )
4714            && let Some(trigger_price) = order.trigger_price()
4715        {
4716            fills[0] = (trigger_price, fills[0].1);
4717
4718            // Skip liquidity consumption for trigger price fills (gap price may not exist in book).
4719            let mut remaining_qty = order.leaves_qty();
4720            let mut capped_fills = Vec::with_capacity(fills.len());
4721
4722            for (price, qty) in fills {
4723                if remaining_qty.is_zero() {
4724                    break;
4725                }
4726
4727                let mut capped_qty = qty.min(remaining_qty);
4728                capped_qty.precision = qty.precision;
4729                if capped_qty.is_zero() {
4730                    continue;
4731                }
4732
4733                remaining_qty = remaining_qty - capped_qty;
4734                capped_fills.push((price, capped_qty));
4735            }
4736
4737            return capped_fills;
4738        }
4739
4740        fills
4741    }
4742
4743    fn determine_market_fill_model_price_and_volume(
4744        &mut self,
4745        order: &OrderAny,
4746    ) -> anyhow::Result<(Vec<(Price, Quantity)>, bool)> {
4747        if let Some(book) = self.fill_model.get_orderbook_for_fill_simulation(
4748            &self.instrument,
4749            order,
4750            self.core.bid,
4751            self.core.ask,
4752        )? {
4753            let price = match order.order_side() {
4754                OrderSide::Buy => Price::max(FIXED_PRECISION),
4755                OrderSide::Sell => Price::min(FIXED_PRECISION),
4756            };
4757            let book_order = BookOrder::new(order.order_side(), price, order.quantity(), 0);
4758            return Ok((book.simulate_fills(&book_order), true));
4759        }
4760        Ok((self.determine_market_price_and_volume(order), false))
4761    }
4762
4763    fn determine_limit_fill_model_price_and_volume(
4764        &mut self,
4765        order: &OrderAny,
4766        journal: Option<&mut ConsumptionJournal>,
4767    ) -> anyhow::Result<Vec<(Price, Quantity)>> {
4768        if let Some(book) = self.fill_model.get_orderbook_for_fill_simulation(
4769            &self.instrument,
4770            order,
4771            self.core.bid,
4772            self.core.ask,
4773        )? && let Some(limit_price) = order.price()
4774        {
4775            let book_order = BookOrder::new(order.order_side(), limit_price, order.quantity(), 0);
4776            return Ok(book.simulate_fills(&book_order));
4777        }
4778        Ok(self.determine_limit_price_and_volume(order, journal))
4779    }
4780
4781    /// Fills a market order against the current order book.
4782    ///
4783    /// The order is filled as a taker against available liquidity.
4784    /// Reduce-only orders are canceled if no position exists.
4785    pub fn fill_market_order(&mut self, client_order_id: ClientOrderId) {
4786        let mut order = match self.order_snapshot(client_order_id) {
4787            Some(order) => order,
4788            None => {
4789                log::error!("Cannot fill market order: order {client_order_id} not found in cache");
4790                return;
4791            }
4792        };
4793
4794        if order.is_closed() {
4795            self.purge_stale_core_entry(client_order_id);
4796            return;
4797        }
4798
4799        // Convert quote-denominated quantity at fill time for trigger-style market
4800        // orders that skipped conversion at submission. Idempotent: orders already
4801        // converted have `is_quote_quantity == false`.
4802        if order.is_quote_quantity()
4803            && !self.instrument.is_inverse()
4804            && !self.convert_quote_to_base_quantity(&mut order)
4805        {
4806            return;
4807        }
4808
4809        if let Some(filled_qty) = self.cached_filled_qty.get(&order.client_order_id())
4810            && filled_qty >= &order.quantity()
4811        {
4812            log::debug!(
4813                "Ignoring fill as already filled pending application of events: {:?}, {:?}, {:?}, {:?}",
4814                filled_qty,
4815                order.quantity(),
4816                order.filled_qty(),
4817                order.quantity()
4818            );
4819            return;
4820        }
4821
4822        let (venue_position_id, position) = self.fill_position_for_order(&order, Some(true));
4823
4824        if self.reduce_only_position_missing(&order, position.as_ref()) {
4825            log::warn!(
4826                "Canceling REDUCE_ONLY {} as would increase position",
4827                order.order_type()
4828            );
4829            self.cancel_order(&order, None);
4830            return;
4831        }
4832
4833        order.set_liquidity_side(LiquiditySide::Taker);
4834        let (mut fills, from_synthetic) =
4835            match self.determine_market_fill_model_price_and_volume(&order) {
4836                Ok(result) => result,
4837                Err(e) => {
4838                    log::error!(
4839                        "Cannot fill market order {}: fill model failed: {e}",
4840                        order.client_order_id()
4841                    );
4842                    return;
4843                }
4844            };
4845
4846        // Apply protection price filtering at fill time (trigger-time semantics for stops)
4847        let protection_price: Option<Price> = if let Some(protection_points) =
4848            self.config.price_protection_points
4849            && matches!(
4850                order.order_type(),
4851                OrderType::Market | OrderType::StopMarket
4852            ) {
4853            protection_price_calculate(
4854                self.instrument.price_increment(),
4855                &order,
4856                protection_points,
4857                self.core.bid,
4858                self.core.ask,
4859            )
4860            .ok()
4861        } else {
4862            None
4863        };
4864
4865        if let Some(protection_price) = protection_price {
4866            fills = self.filter_fills_by_protection(fills, &order, protection_price);
4867        }
4868
4869        // Skip consumption for synthetic fill-model books (prices may not exist
4870        // in the real book) and trigger price fills (gap price may not exist)
4871        let is_trigger_price_fill = !self.fill_at_market
4872            && self.book_type == BookType::L1_MBP
4873            && matches!(
4874                order.order_type(),
4875                OrderType::StopMarket | OrderType::TrailingStopMarket | OrderType::MarketIfTouched
4876            )
4877            && order.trigger_price().is_some();
4878
4879        if !from_synthetic && !is_trigger_price_fill {
4880            let mut journal = self.fok_consumption_journal(&order);
4881
4882            fills = self.apply_liquidity_consumption(
4883                fills,
4884                order.order_side(),
4885                order.leaves_qty(),
4886                None,
4887                None,
4888                journal.as_mut(),
4889            );
4890
4891            if let Some(journal) = journal
4892                && self.is_fok_unfillable(&order, &fills)
4893            {
4894                self.revert_consumption_journal(journal);
4895            }
4896        }
4897
4898        if let Err(e) = self.apply_fills(
4899            &order,
4900            &fills,
4901            LiquiditySide::Taker,
4902            if self.config.use_reduce_only && order.is_reduce_only() {
4903                venue_position_id
4904            } else {
4905                None
4906            },
4907            position.as_ref(),
4908            protection_price,
4909            from_synthetic,
4910        ) {
4911            log::error!("Cannot fill market order {}: {e}", order.client_order_id());
4912        }
4913    }
4914
4915    fn filter_fills_by_protection(
4916        &self,
4917        fills: Vec<(Price, Quantity)>,
4918        order: &OrderAny,
4919        protection_price: Price,
4920    ) -> Vec<(Price, Quantity)> {
4921        fills
4922            .into_iter()
4923            .filter(|(fill_price, _)| {
4924                match order.order_side() {
4925                    // BUY: only fill at prices <= protection_price
4926                    OrderSide::Buy => *fill_price <= protection_price,
4927
4928                    // SELL: only fill at prices >= protection_price
4929                    OrderSide::Sell => *fill_price >= protection_price,
4930                }
4931            })
4932            .collect()
4933    }
4934
4935    /// Attempts to fill a limit order against the current order book.
4936    ///
4937    /// Determines fill prices and quantities based on available liquidity,
4938    /// then applies the fills to the order.
4939    ///
4940    /// # Panics
4941    ///
4942    /// Panics if the order has no price (design error).
4943    pub fn fill_limit_order(&mut self, client_order_id: ClientOrderId) {
4944        let order = match self.order_snapshot(client_order_id) {
4945            Some(order) => order,
4946            None => {
4947                log::error!("Cannot fill limit order: order {client_order_id} not found in cache");
4948                return;
4949            }
4950        };
4951
4952        self.fill_limit_order_with_snapshot(order);
4953    }
4954
4955    fn fill_limit_order_with_snapshot(&mut self, mut order: OrderAny) {
4956        let client_order_id = order.client_order_id();
4957
4958        if order.is_closed() {
4959            self.purge_stale_core_entry(client_order_id);
4960            return;
4961        }
4962
4963        // Convert quote-denominated quantity at fill time for orders that entered
4964        // this path still carrying a quote notional (e.g. trailing-stop-limit with
4965        // a late-assigned price). Idempotent for already-converted orders.
4966        if order.is_quote_quantity()
4967            && !self.instrument.is_inverse()
4968            && !self.convert_quote_to_base_quantity(&mut order)
4969        {
4970            return;
4971        }
4972
4973        match order.price() {
4974            Some(order_price) => {
4975                let cached_filled_qty = self.cached_filled_qty.get(&order.client_order_id());
4976                if let Some(&qty) = cached_filled_qty
4977                    && qty >= order.quantity()
4978                {
4979                    log::debug!(
4980                        "Ignoring fill as already filled pending application of events: {}, {}, {}, {}",
4981                        qty,
4982                        order.quantity(),
4983                        order.filled_qty(),
4984                        order.leaves_qty(),
4985                    );
4986                    return;
4987                }
4988
4989                // Check fill model for MAKER orders at the limit price
4990                if order
4991                    .liquidity_side()
4992                    .is_some_and(|liquidity_side| liquidity_side == LiquiditySide::Maker)
4993                {
4994                    // For trade execution: check if trade price equals order price
4995                    // For quote updates: check if bid/ask equals order price
4996                    let at_limit = if self.last_trade_size.is_some() && self.core.last.is_some() {
4997                        self.core.last.is_some_and(|last| last == order_price)
4998                    } else if order.order_side() == OrderSide::Buy {
4999                        self.core.bid.is_some_and(|bid| bid == order_price)
5000                    } else {
5001                        self.core.ask.is_some_and(|ask| ask == order_price)
5002                    };
5003
5004                    if at_limit {
5005                        let is_limit_filled = match self.fill_model.is_limit_filled() {
5006                            Ok(value) => value,
5007                            Err(e) => {
5008                                log::error!(
5009                                    "Cannot fill limit order {}: fill model failed: {e}",
5010                                    order.client_order_id()
5011                                );
5012                                return;
5013                            }
5014                        };
5015
5016                        if !is_limit_filled {
5017                            return; // Not filled (simulates queue position)
5018                        }
5019                    }
5020                }
5021
5022                let queue_allowed_raw = if self.config.queue_position {
5023                    match self.determine_trade_fill_qty(&order) {
5024                        None | Some(0) => {
5025                            if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc)
5026                            {
5027                                self.cancel_order(&order, None);
5028                            }
5029                            return;
5030                        }
5031                        Some(allowed) => Some(allowed),
5032                    }
5033                } else {
5034                    None
5035                };
5036
5037                let (venue_position_id, position) = self.fill_position_for_order(&order, None);
5038
5039                if self.reduce_only_position_missing(&order, position.as_ref()) {
5040                    log::warn!(
5041                        "Canceling REDUCE_ONLY {} as would increase position",
5042                        order.order_type()
5043                    );
5044                    self.cancel_order(&order, None);
5045                    return;
5046                }
5047
5048                let mut journal = self.fok_consumption_journal(&order);
5049                let tc_before = self.trade_consumption;
5050                let mut fills = match self
5051                    .determine_limit_fill_model_price_and_volume(&order, journal.as_mut())
5052                {
5053                    Ok(fills) => fills,
5054                    Err(e) => {
5055                        log::error!(
5056                            "Cannot fill limit order {}: fill model failed: {e}",
5057                            order.client_order_id()
5058                        );
5059                        return;
5060                    }
5061                };
5062
5063                if let Some(allowed_raw) = queue_allowed_raw {
5064                    let size_prec = self.instrument.size_precision();
5065                    let mut remaining = allowed_raw;
5066                    fills = fills
5067                        .into_iter()
5068                        .filter_map(|(price, qty)| {
5069                            if remaining == 0 {
5070                                return None;
5071                            }
5072
5073                            let capped = qty.raw().min(remaining);
5074                            remaining -= capped;
5075                            Some((price, Quantity::from_raw(capped, size_prec)))
5076                        })
5077                        .collect();
5078
5079                    // Consume excess and reconcile trade budget after capping
5080                    let consumed: QuantityRaw = fills.iter().map(|(_, qty)| qty.raw()).sum();
5081
5082                    if let Some(excess) = self.queue_excess.get_mut(&order.client_order_id()) {
5083                        *excess = excess.saturating_sub(consumed);
5084                    }
5085                    self.trade_consumption = tc_before + consumed;
5086                }
5087
5088                if let Some(journal) = journal
5089                    && self.is_fok_unfillable(&order, &fills)
5090                {
5091                    self.revert_consumption_journal(journal);
5092                }
5093
5094                if fills.is_empty() {
5095                    log::debug!(
5096                        "Skipping fill for {}: no liquidity available",
5097                        order.client_order_id()
5098                    );
5099
5100                    if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc) {
5101                        self.cancel_order(&order, None);
5102                    }
5103
5104                    return;
5105                }
5106
5107                let liquidity_side = order.liquidity_side().unwrap();
5108                if let Err(e) = self.apply_fills(
5109                    &order,
5110                    &fills,
5111                    liquidity_side,
5112                    venue_position_id,
5113                    position.as_ref(),
5114                    None,
5115                    false,
5116                ) {
5117                    log::error!("Cannot fill limit order {}: {e}", order.client_order_id());
5118                }
5119            }
5120            None => panic!("Limit order must have a price"),
5121        }
5122    }
5123
5124    fn reduce_only_position_missing(
5125        &mut self,
5126        order: &OrderAny,
5127        position: Option<&Position>,
5128    ) -> bool {
5129        self.config.use_reduce_only
5130            && order.is_reduce_only()
5131            && position.is_none()
5132            && self
5133                .position_quantity_remaining(order, None)
5134                .map_or(true, |quantity| quantity.is_zero())
5135    }
5136
5137    fn fill_position_for_order(
5138        &mut self,
5139        order: &OrderAny,
5140        generate: Option<bool>,
5141    ) -> (Option<PositionId>, Option<Position>) {
5142        if self.restricts_short_selling()
5143            && self.oms_type == OmsType::Hedging
5144            && let Some(position) = self.position_for_order_in_cache(&self.cache.borrow(), order)
5145        {
5146            return (Some(position.id), Some(position));
5147        }
5148
5149        if self.restricts_short_selling()
5150            && self.oms_type == OmsType::Hedging
5151            && order.parent_order_id().is_some()
5152        {
5153            let cache = self.cache.borrow();
5154            return (Self::position_id_for_order(&cache, order), None);
5155        }
5156
5157        if self.oms_type == OmsType::Hedging
5158            && self.config.use_reduce_only
5159            && order.is_reduce_only()
5160        {
5161            let cache = self.cache.as_ref().borrow();
5162            if let Some(position_id) = Self::position_id_for_order(&cache, order) {
5163                let position = cache
5164                    .position(&position_id)
5165                    .map(|position| position.clone_without_events());
5166                return (Some(position_id), position);
5167            }
5168
5169            if self.config.support_contingent_orders && order.parent_order_id().is_some() {
5170                return (None, None);
5171            }
5172
5173            if let Some(position) = Self::open_position_reduced_by_order(&cache, order) {
5174                return (Some(position.id), Some(position));
5175            }
5176        }
5177
5178        let pending_position_id = if self.oms_type == OmsType::Hedging {
5179            self.pending_fills.values().find_map(|fill| {
5180                (fill.client_order_id == order.client_order_id())
5181                    .then_some(fill.position_id)
5182                    .flatten()
5183            })
5184        } else {
5185            None
5186        };
5187
5188        let venue_position_id =
5189            pending_position_id.or_else(|| self.ids_generator.get_position_id(order, generate));
5190
5191        let position = {
5192            let cache = self.cache.as_ref().borrow();
5193            venue_position_id
5194                .as_ref()
5195                .and_then(|position_id| cache.position(position_id))
5196                .map(|position| position.clone_without_events())
5197        };
5198
5199        (venue_position_id, position)
5200    }
5201
5202    fn position_for_order_in_cache(&self, cache: &Cache, order: &OrderAny) -> Option<Position> {
5203        if let Some(position_id) = Self::position_id_for_order(cache, order) {
5204            return cache
5205                .position(&position_id)
5206                .map(|position| position.clone_without_events());
5207        }
5208
5209        if self.oms_type == OmsType::Netting {
5210            let position_id = PositionId::new(
5211                format!("{}-{}", order.instrument_id(), order.strategy_id()).as_str(),
5212            );
5213            return cache
5214                .position(&position_id)
5215                .map(|position| position.clone_without_events());
5216        }
5217
5218        if self.oms_type == OmsType::Hedging
5219            && self.config.use_reduce_only
5220            && order.is_reduce_only()
5221        {
5222            if self.config.support_contingent_orders && order.parent_order_id().is_some() {
5223                return None;
5224            }
5225
5226            return Self::open_position_reduced_by_order(cache, order);
5227        }
5228
5229        None
5230    }
5231
5232    fn position_id_for_order(cache: &Cache, order: &OrderAny) -> Option<PositionId> {
5233        cache
5234            .position_id(&order.client_order_id())
5235            .copied()
5236            .or_else(|| {
5237                order
5238                    .parent_order_id()
5239                    .and_then(|parent_id| cache.position_id(&parent_id).copied())
5240            })
5241    }
5242
5243    fn open_position_reduced_by_order(cache: &Cache, order: &OrderAny) -> Option<Position> {
5244        cache
5245            .positions_open(
5246                None,
5247                Some(&order.instrument_id()),
5248                Some(&order.strategy_id()),
5249                None,
5250                None,
5251            )
5252            .into_iter()
5253            .find(|position| order.would_reduce_only(position.side, position.quantity))
5254            .map(|position| position.clone_without_events())
5255    }
5256
5257    #[allow(
5258        clippy::too_many_arguments,
5259        reason = "fill application needs order, position, pricing, and liquidity-source context"
5260    )]
5261    fn apply_fills(
5262        &mut self,
5263        order: &OrderAny,
5264        fills: &[(Price, Quantity)],
5265        liquidity_side: LiquiditySide,
5266        venue_position_id: Option<PositionId>,
5267        position: Option<&Position>,
5268        protection_price: Option<Price>,
5269        from_synthetic: bool,
5270    ) -> anyhow::Result<()> {
5271        if self.is_fok_unfillable(order, fills) {
5272            self.cancel_order(order, None);
5273            return Ok(());
5274        }
5275
5276        if fills.is_empty() {
5277            match (order.status(), order.time_in_force()) {
5278                (OrderStatus::Initialized | OrderStatus::Submitted, time_in_force)
5279                    if time_in_force != TimeInForce::Ioc =>
5280                {
5281                    self.generate_order_rejected(
5282                        order,
5283                        format!("No market for {}", order.instrument_id()).into(),
5284                    );
5285                }
5286                _ => self.cancel_order(order, None),
5287            }
5288
5289            return Ok(());
5290        }
5291
5292        // For netting mode, don't use venue position ID (use None instead)
5293        let venue_position_id = if self.oms_type == OmsType::Netting {
5294            None
5295        } else {
5296            venue_position_id
5297        };
5298
5299        let mut initial_market_to_limit_fill = false;
5300        let mut total_filled = self
5301            .cached_filled_qty
5302            .get(&order.client_order_id())
5303            .copied()
5304            .unwrap_or_else(|| order.filled_qty());
5305        let initial_total_filled = total_filled;
5306        let mut last_fill_px: Option<Price> = None;
5307        let mut reduce_only_remaining = None;
5308        let mut reduce_only_filled = None;
5309
5310        if let Some(remaining) = self.reduce_only_remaining(order, position)? {
5311            if remaining.is_zero() {
5312                self.cancel_order(order, None);
5313                return Ok(());
5314            }
5315
5316            reduce_only_remaining = Some(remaining);
5317            reduce_only_filled = Some(total_filled);
5318        }
5319
5320        for &(fill_px, fill_qty) in fills {
5321            let Some(mut fill_px) = self.normalize_fill_price(fill_px, order.client_order_id())
5322            else {
5323                continue;
5324            };
5325
5326            let Some(fill_qty) = self.normalize_fill_quantity(fill_qty, order.client_order_id())
5327            else {
5328                continue;
5329            };
5330
5331            if order.filled_qty() == Quantity::zero(order.filled_qty().precision)
5332                && order.order_type() == OrderType::MarketToLimit
5333            {
5334                self.generate_order_updated(order, order.quantity(), Some(fill_px), None, None);
5335                initial_market_to_limit_fill = true;
5336            }
5337
5338            if self.book_type == BookType::L1_MBP
5339                && self.fill_model.is_slipped()?
5340                && let Some(slipped_px) = self.slipped_price(fill_px, order.order_side())
5341            {
5342                fill_px = slipped_px;
5343            }
5344
5345            let mut effective_fill_qty = fill_qty;
5346
5347            if let Some(remaining) = reduce_only_remaining {
5348                if remaining.is_zero() {
5349                    return Ok(());
5350                }
5351
5352                if effective_fill_qty > remaining {
5353                    let precision = effective_fill_qty.precision;
5354                    effective_fill_qty = remaining;
5355                    effective_fill_qty.precision = precision;
5356                }
5357            }
5358
5359            if fill_qty.is_zero() {
5360                if fills.len() == 1 && order.status() == OrderStatus::Submitted {
5361                    self.generate_order_rejected(
5362                        order,
5363                        format!("No market for {}", order.instrument_id()).into(),
5364                    );
5365                }
5366                return Ok(());
5367            }
5368
5369            // Mirror `fill_order`'s leaves cap
5370            let capped_fill_qty = min(
5371                effective_fill_qty,
5372                order.quantity().saturating_sub(total_filled),
5373            );
5374            let reduce_only_exhausts_position =
5375                reduce_only_remaining.is_some_and(|remaining| capped_fill_qty >= remaining);
5376
5377            if reduce_only_exhausts_position {
5378                let mut reduce_only_target = reduce_only_filled
5379                    .unwrap_or(initial_total_filled)
5380                    .checked_add(capped_fill_qty)
5381                    .expect("Overflow occurred when adding reduce-only target quantity");
5382                reduce_only_target.precision = self.instrument.size_precision();
5383
5384                if order.quantity() != reduce_only_target {
5385                    self.generate_order_updated(order, reduce_only_target, None, None, None);
5386                }
5387            }
5388
5389            total_filled = total_filled.add(capped_fill_qty);
5390
5391            if let Some(remaining) = reduce_only_remaining.as_mut() {
5392                *remaining = *remaining - capped_fill_qty.min(*remaining);
5393            }
5394
5395            if let Some(filled) = reduce_only_filled.as_mut() {
5396                *filled = filled
5397                    .checked_add(capped_fill_qty)
5398                    .expect("Overflow occurred when adding reduce-only filled quantity");
5399            }
5400
5401            self.fill_order(
5402                order,
5403                fill_px,
5404                effective_fill_qty,
5405                liquidity_side,
5406                venue_position_id,
5407                position,
5408            )?;
5409            last_fill_px = Some(fill_px);
5410
5411            if order.order_type() == OrderType::MarketToLimit && initial_market_to_limit_fill {
5412                // Filled initial level
5413                return Ok(());
5414            }
5415
5416            if reduce_only_exhausts_position {
5417                self.purge_cached_filled_qty_if_closed(order.client_order_id());
5418                return Ok(());
5419            }
5420        }
5421
5422        let leaves_remaining = total_filled < order.quantity();
5423        let filled_in_loop = total_filled > initial_total_filled;
5424
5425        if order.time_in_force() == TimeInForce::Ioc && leaves_remaining {
5426            self.cancel_order(order, None);
5427            return Ok(());
5428        }
5429
5430        // `filled_in_loop` covers the just-partially-filled case where the
5431        // local clone's status has not seen the fill events yet.
5432        if leaves_remaining
5433            && !from_synthetic
5434            && (order.is_open() || filled_in_loop)
5435            && self.book_type == BookType::L1_MBP
5436            && matches!(
5437                order.order_type(),
5438                OrderType::Market
5439                    | OrderType::MarketIfTouched
5440                    | OrderType::StopMarket
5441                    | OrderType::TrailingStopMarket
5442            )
5443        {
5444            // Exhausted L1 volume: slip remainder by a single price increment
5445            let Some(last_fill_px) = last_fill_px else {
5446                return Ok(());
5447            };
5448
5449            let side = order.order_side();
5450
5451            let Some(slip_fill_px) = self.slipped_price(last_fill_px, side) else {
5452                return Ok(());
5453            };
5454
5455            if let Some(protection_price) = protection_price {
5456                let exceeds_boundary = match side {
5457                    OrderSide::Buy => slip_fill_px > protection_price,
5458                    OrderSide::Sell => slip_fill_px < protection_price,
5459                };
5460
5461                if exceeds_boundary {
5462                    return Ok(());
5463                }
5464            }
5465
5466            let mut leaves_qty = order.quantity().saturating_sub(total_filled);
5467
5468            if let Some(remaining) = reduce_only_remaining {
5469                if remaining.is_zero() {
5470                    return Ok(());
5471                }
5472
5473                if leaves_qty > remaining {
5474                    let precision = leaves_qty.precision;
5475                    leaves_qty = remaining;
5476                    leaves_qty.precision = precision;
5477                }
5478
5479                if leaves_qty >= remaining {
5480                    let mut reduce_only_target = reduce_only_filled
5481                        .unwrap_or(initial_total_filled)
5482                        .checked_add(leaves_qty)
5483                        .expect("Overflow occurred when adding reduce-only target quantity");
5484                    reduce_only_target.precision = self.instrument.size_precision();
5485
5486                    if order.quantity() != reduce_only_target {
5487                        self.generate_order_updated(order, reduce_only_target, None, None, None);
5488                    }
5489                }
5490            }
5491
5492            if leaves_qty.is_zero() {
5493                return Ok(());
5494            }
5495
5496            self.fill_order(
5497                order,
5498                slip_fill_px,
5499                leaves_qty,
5500                liquidity_side,
5501                venue_position_id,
5502                position,
5503            )?;
5504            self.purge_cached_filled_qty_if_closed(order.client_order_id());
5505        }
5506
5507        Ok(())
5508    }
5509
5510    // One tick worse than `price` for `side`, or `None` when that leaves the instrument's limits
5511    fn slipped_price(&self, price: Price, side: OrderSide) -> Option<Price> {
5512        let price_increment = self.instrument.price_increment();
5513
5514        let slipped = match side {
5515            OrderSide::Buy => price.add(price_increment),
5516            OrderSide::Sell => price.sub(price_increment),
5517        };
5518
5519        is_price_tradable(&self.instrument, slipped).then_some(slipped)
5520    }
5521
5522    fn normalize_fill_price(
5523        &self,
5524        fill_px: Price,
5525        client_order_id: ClientOrderId,
5526    ) -> Option<Price> {
5527        let normalized = self.normalize_price_for_current_instrument(fill_px);
5528        if normalized.is_none() {
5529            log::warn!(
5530                "Skipping fill for {client_order_id}: fill price {fill_px} is not compatible \
5531                 with {} price_precision={} price_increment={}",
5532                self.instrument.id(),
5533                self.instrument.price_precision(),
5534                self.instrument.price_increment()
5535            );
5536        }
5537        normalized
5538    }
5539
5540    fn normalize_fill_quantity(
5541        &self,
5542        fill_qty: Quantity,
5543        client_order_id: ClientOrderId,
5544    ) -> Option<Quantity> {
5545        let normalized = self.normalize_quantity_for_current_instrument(fill_qty);
5546        if normalized.is_none() {
5547            log::warn!(
5548                "Skipping fill for {client_order_id}: fill quantity {fill_qty} is not compatible \
5549                 with {} size_precision={}",
5550                self.instrument.id(),
5551                self.instrument.size_precision()
5552            );
5553        }
5554        normalized
5555    }
5556
5557    fn reduce_only_remaining(
5558        &mut self,
5559        order: &OrderAny,
5560        position: Option<&Position>,
5561    ) -> anyhow::Result<Option<Quantity>> {
5562        if self.config.use_reduce_only && order.is_reduce_only() {
5563            return Ok(Some(self.position_quantity_remaining(order, position)?));
5564        }
5565
5566        Ok(None)
5567    }
5568
5569    fn position_quantity_remaining(
5570        &mut self,
5571        order: &OrderAny,
5572        position: Option<&Position>,
5573    ) -> anyhow::Result<Quantity> {
5574        let precision = self.instrument.size_precision();
5575
5576        self.purge_applied_fills();
5577        let quantity = self.position_quantity(&self.cache.borrow(), order, position)?;
5578
5579        if (order.is_buy() && quantity >= Decimal::ZERO)
5580            || (order.is_sell() && quantity <= Decimal::ZERO)
5581        {
5582            return Ok(Quantity::zero(precision));
5583        }
5584
5585        Ok(Quantity::from_decimal_dp(quantity.abs(), precision)?)
5586    }
5587
5588    fn position_quantity(
5589        &self,
5590        cache: &Cache,
5591        order: &OrderAny,
5592        position: Option<&Position>,
5593    ) -> anyhow::Result<Decimal> {
5594        let position_id = position
5595            .map(|position| position.id)
5596            .or_else(|| Self::position_id_for_order(cache, order))
5597            .or_else(|| {
5598                (self.oms_type == OmsType::Netting).then(|| {
5599                    PositionId::new(format!("{}-{}", order.instrument_id(), order.strategy_id()))
5600                })
5601            });
5602
5603        let cached_position = position_id.as_ref().and_then(|id| cache.position(id));
5604        let position = position.or(cached_position.as_deref());
5605        let splits_virtual_position = self.restricts_short_selling()
5606            && self.oms_type == OmsType::Hedging
5607            && position_id.is_some_and(|id| id.is_virtual());
5608
5609        let mut quantity = position.map_or(Decimal::ZERO, |position| match position.side {
5610            PositionSide::Long => position.quantity.as_decimal(),
5611            PositionSide::Short => -position.quantity.as_decimal(),
5612            PositionSide::Flat => Decimal::ZERO,
5613        });
5614
5615        let mut flipped_orders = IndexSet::new();
5616
5617        for fill in self.pending_fills.values() {
5618            let matches_position = match position_id {
5619                Some(position_id) => fill.position_id == Some(position_id),
5620                None => order.parent_order_id().is_some_and(|parent_id| {
5621                    fill.client_order_id == parent_id
5622                        || cache
5623                            .order(&fill.client_order_id)
5624                            .is_some_and(|order| order.parent_order_id() == Some(parent_id))
5625                }),
5626            };
5627
5628            if !matches_position || flipped_orders.contains(&fill.client_order_id) {
5629                continue;
5630            }
5631
5632            let remaining = quantity
5633                .checked_add(fill.quantity_change)
5634                .ok_or_else(|| anyhow::anyhow!("Pending position quantity overflow"))?;
5635
5636            quantity = if splits_virtual_position
5637                && !quantity.is_zero()
5638                && !remaining.is_zero()
5639                && quantity.is_sign_negative() != remaining.is_sign_negative()
5640            {
5641                flipped_orders.insert(fill.client_order_id);
5642                Decimal::ZERO
5643            } else {
5644                remaining
5645            };
5646        }
5647
5648        Ok(quantity)
5649    }
5650
5651    fn purge_applied_fills(&mut self) {
5652        let cache = self.cache.borrow();
5653        let cash_cache = self.restricts_short_selling().then_some(&*cache);
5654        self.pending_fills.retain(|trade_id, fill| {
5655            let previous_position_id = cash_cache.and(fill.position_id);
5656            fill.position_id = cash_cache
5657                .and_then(|cache| cache.position_id(&fill.client_order_id).copied())
5658                .or_else(|| {
5659                    fill.position_id
5660                        .filter(|id| cash_cache.is_none_or(|cache| cache.position(id).is_some()))
5661                })
5662                .or_else(|| cache.position_id(&fill.client_order_id).copied())
5663                .or_else(|| {
5664                    let cache = cash_cache?;
5665                    let order = cache.order(&fill.client_order_id)?;
5666                    Self::position_id_for_order(cache, &order)
5667                })
5668                .or(fill.position_id);
5669
5670            let Some(position_id) = fill.position_id else {
5671                return cache.order_exists(&fill.client_order_id);
5672            };
5673            let Some(position) = cache.position(&position_id) else {
5674                return cache.order_exists(&fill.client_order_id);
5675            };
5676
5677            if previous_position_id
5678                .and_then(|id| cache.position(&id))
5679                .iter()
5680                .chain(std::iter::once(&position))
5681                .any(|position| position.trade_ids.contains(trade_id))
5682            {
5683                return false;
5684            }
5685            let opening_trade_id = position.events.first().map(|event| event.trade_id);
5686            if opening_trade_id != fill.opening_trade_id {
5687                // NETTING reuses position IDs; acknowledged fills can belong to archived cycles
5688                if position.replay_events.iter().any(|event| {
5689                    matches!(event, PositionReplayEvent::Filled(event) if event.trade_id == *trade_id)
5690                }) || cache.position_snapshots(Some(&position_id), None).iter()
5691                    .any(|snapshot| snapshot.trade_ids.contains(trade_id))
5692                    || cash_cache
5693                        .and_then(|cache| cache.order(&fill.client_order_id))
5694                        .filter(|order| position.opening_order_id == order.client_order_id())
5695                        .is_some_and(|order| {
5696                            order
5697                                .trade_ids()
5698                                .into_iter()
5699                                .take_while(|id| Some(**id) != opening_trade_id)
5700                                .any(|id| id == trade_id)
5701                        })
5702                {
5703                    return false;
5704                }
5705                fill.opening_trade_id = opening_trade_id;
5706            }
5707            true
5708        });
5709    }
5710
5711    fn fill_order(
5712        &mut self,
5713        order: &OrderAny,
5714        last_px: Price,
5715        last_qty: Quantity,
5716        liquidity_side: LiquiditySide,
5717        venue_position_id: Option<PositionId>,
5718        position: Option<&Position>,
5719    ) -> anyhow::Result<()> {
5720        self.check_size_precision(last_qty.precision, "fill quantity")?;
5721
5722        let (last_qty, new_filled_qty) =
5723            if let Some(filled_qty) = self.cached_filled_qty.get(&order.client_order_id()) {
5724                let leaves_qty = order.quantity().saturating_sub(*filled_qty);
5725                let last_qty = min(last_qty, leaves_qty);
5726                (last_qty, *filled_qty + last_qty)
5727            } else {
5728                let last_qty = min(last_qty, order.quantity());
5729                (last_qty, last_qty)
5730            };
5731
5732        if last_qty.is_zero() {
5733            return Ok(());
5734        }
5735
5736        let fee_order;
5737        let commission_order = {
5738            // `order` is a stale pre-fill clone: give fee models the current
5739            // pre-fill `filled_qty` (e.g. `FixedFeeModel` charges once per order).
5740            let mut cloned = order.clone();
5741            write_filled_qty(&mut cloned, new_filled_qty.saturating_sub(last_qty));
5742            if order.liquidity_side() != Some(liquidity_side) {
5743                cloned.set_liquidity_side(liquidity_side);
5744            }
5745            fee_order = cloned;
5746            &fee_order
5747        };
5748
5749        let underlying_px = self.fee_underlying_price()?;
5750        let commission = self.fee_model.get_commission_with_context(
5751            commission_order,
5752            last_qty,
5753            last_px,
5754            &self.instrument,
5755            underlying_px,
5756        )?;
5757
5758        // Resolve implicit membership before dispatch can close the cached position
5759        let mut reduce_only_order_ids = position
5760            .map(|position| self.reduce_only_order_ids(position.id))
5761            .unwrap_or_default();
5762
5763        self.cached_filled_qty
5764            .insert(order.client_order_id(), new_filled_qty);
5765
5766        let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
5767        self.generate_order_filled(
5768            order,
5769            venue_order_id,
5770            venue_position_id,
5771            last_qty,
5772            last_px,
5773            self.instrument.quote_currency(),
5774            commission,
5775            liquidity_side,
5776        );
5777
5778        let post_fill_filled_qty = self
5779            .cached_filled_qty
5780            .get(&order.client_order_id())
5781            .copied()
5782            .unwrap_or(order.filled_qty());
5783
5784        let post_fill_quantity = self
5785            .pending_updates
5786            .borrow()
5787            .get(&order.client_order_id())
5788            .and_then(|updates| updates.last())
5789            .map_or_else(
5790                || {
5791                    self.cache
5792                        .borrow()
5793                        .order(&order.client_order_id())
5794                        .map_or(order.quantity(), |order| order.quantity())
5795                },
5796                |update| update.quantity,
5797            );
5798
5799        let post_fill_leaves_qty = post_fill_quantity.saturating_sub(post_fill_filled_qty);
5800        let fully_filled = post_fill_leaves_qty.is_zero();
5801
5802        if order.is_closed() || fully_filled {
5803            self.delete_core_order(order.client_order_id());
5804            self.remove_queue_position(order.client_order_id());
5805
5806            // MarketToLimit reads `cached_filled_qty` in its caller to compute leaves;
5807            // its own cleanup happens there after the read.
5808            if order.order_type() != OrderType::MarketToLimit {
5809                self.purge_cached_filled_qty_if_closed(order.client_order_id());
5810            }
5811        }
5812
5813        if self.config.support_contingent_orders
5814            && let Some(contingency_type) = order.contingency_type()
5815        {
5816            match contingency_type {
5817                ContingencyType::Oto => {
5818                    if let Some(linked_orders_ids) = order.linked_order_ids() {
5819                        for client_order_id in linked_orders_ids {
5820                            let mut child_order = match self.order_snapshot(*client_order_id) {
5821                                Some(child_order) => child_order,
5822                                None => anyhow::bail!("Order {client_order_id} not found in cache"),
5823                            };
5824
5825                            if child_order.is_closed()
5826                                || child_order.leaves_qty().is_zero()
5827                                || child_order.is_active_local()
5828                                || self.canceled_oto_order_ids.contains(client_order_id)
5829                            {
5830                                continue;
5831                            }
5832
5833                            if self.inflight_orders.contains(*client_order_id) {
5834                                continue;
5835                            }
5836
5837                            // Check if we need to index position id
5838                            if let (None, Some(position_id)) = (
5839                                child_order.position_id(),
5840                                order.position_id().or(venue_position_id),
5841                            ) {
5842                                self.cache
5843                                    .borrow_mut()
5844                                    .add_position_id(
5845                                        &position_id,
5846                                        &self.venue,
5847                                        client_order_id,
5848                                        &child_order.strategy_id(),
5849                                    )
5850                                    .unwrap();
5851                                log::debug!(
5852                                    "Added position id {position_id} to cache for order {client_order_id}"
5853                                );
5854                            }
5855
5856                            if (!child_order.is_open())
5857                                || (matches!(child_order.status(), OrderStatus::PendingUpdate)
5858                                    && child_order
5859                                        .previous_status()
5860                                        .is_some_and(|s| matches!(s, OrderStatus::Submitted)))
5861                            {
5862                                let account_id = order
5863                                    .account_id()
5864                                    .or_else(|| self.account_ids.get(&order.trader_id()).copied())
5865                                    .ok_or_else(|| {
5866                                        anyhow::anyhow!(
5867                                            "Account ID not found for trader {}",
5868                                            order.trader_id()
5869                                        )
5870                                    })?;
5871                                self.process_order(&mut child_order, account_id);
5872                            }
5873                        }
5874                    } else {
5875                        log::error!(
5876                            "OTO order {} does not have linked orders",
5877                            order.client_order_id()
5878                        );
5879                    }
5880                }
5881                ContingencyType::Oco => {
5882                    if let Some(linked_orders_ids) = order.linked_order_ids() {
5883                        for client_order_id in linked_orders_ids {
5884                            let child_order = match self.order_snapshot(*client_order_id) {
5885                                Some(child_order) => child_order,
5886                                None => anyhow::bail!("Order {client_order_id} not found in cache"),
5887                            };
5888
5889                            if child_order.is_closed() || child_order.is_active_local() {
5890                                continue;
5891                            }
5892
5893                            self.cancel_order(&child_order, Some(false));
5894                        }
5895                    } else {
5896                        log::error!(
5897                            "OCO order {} does not have linked orders",
5898                            order.client_order_id()
5899                        );
5900                    }
5901                }
5902                ContingencyType::Ouo => {
5903                    if let Some(linked_orders_ids) = order.linked_order_ids() {
5904                        let mut excluded = linked_orders_ids.to_vec();
5905                        excluded.push(order.client_order_id());
5906
5907                        for client_order_id in linked_orders_ids {
5908                            let child_order = match self.order_snapshot(*client_order_id) {
5909                                Some(child_order) => child_order,
5910                                None => anyhow::bail!("Order {client_order_id} not found in cache"),
5911                            };
5912
5913                            if child_order.is_active_local() {
5914                                continue;
5915                            }
5916
5917                            let child_filled_qty = self
5918                                .cached_filled_qty
5919                                .get(&child_order.client_order_id())
5920                                .copied()
5921                                .unwrap_or(child_order.filled_qty());
5922                            if post_fill_leaves_qty.is_zero() && !child_order.is_closed() {
5923                                self.cancel_order_excluding(&child_order, None, &excluded);
5924                            } else if child_order.is_open()
5925                                && child_filled_qty >= post_fill_leaves_qty
5926                            {
5927                                self.cancel_order(&child_order, Some(false));
5928                            } else if post_fill_leaves_qty.non_zero()
5929                                && post_fill_leaves_qty != child_order.leaves_qty()
5930                            {
5931                                let price = child_order.price();
5932                                let trigger_price = child_order.trigger_price();
5933                                self.update_order(
5934                                    &child_order,
5935                                    Some(post_fill_leaves_qty),
5936                                    price,
5937                                    trigger_price,
5938                                    Some(false),
5939                                );
5940                            }
5941                        }
5942                    } else {
5943                        log::error!(
5944                            "OUO order {} does not have linked orders",
5945                            order.client_order_id()
5946                        );
5947                    }
5948                }
5949            }
5950        }
5951
5952        if let Some(position) = position {
5953            reduce_only_order_ids.extend(self.reduce_only_order_ids(position.id));
5954        }
5955
5956        reduce_only_order_ids.extend(self.reduce_only_linked_order_ids(
5957            order,
5958            venue_position_id.or(position.map(|position| position.id)),
5959        ));
5960        reduce_only_order_ids.sort_unstable();
5961        reduce_only_order_ids.dedup();
5962        self.sync_reduce_only_orders(order, position, &reduce_only_order_ids)?;
5963
5964        Ok(())
5965    }
5966
5967    fn reduce_only_linked_order_ids(
5968        &self,
5969        order: &OrderAny,
5970        position_id: Option<PositionId>,
5971    ) -> Vec<ClientOrderId> {
5972        if !self.config.support_contingent_orders || !self.config.use_reduce_only {
5973            return Vec::new();
5974        }
5975
5976        let Some(linked_order_ids) = order.linked_order_ids() else {
5977            return Vec::new();
5978        };
5979
5980        let cache = self.cache.borrow();
5981        let position_id = position_id
5982            .or_else(|| Self::position_id_for_order(&cache, order))
5983            .or_else(|| {
5984                self.position_for_order_in_cache(&cache, order)
5985                    .map(|position| position.id)
5986            });
5987
5988        linked_order_ids
5989            .iter()
5990            .filter(|id| {
5991                cache.order(id).is_some_and(|linked| {
5992                    linked.strategy_id() == order.strategy_id()
5993                        && Self::position_id_for_order(&cache, &linked).or_else(|| {
5994                            self.position_for_order_in_cache(&cache, &linked)
5995                                .map(|position| position.id)
5996                        }) == position_id
5997                })
5998            })
5999            .copied()
6000            .collect()
6001    }
6002
6003    fn reduce_only_order_ids(&self, position_id: PositionId) -> Vec<ClientOrderId> {
6004        if !self.config.use_reduce_only {
6005            return Vec::new();
6006        }
6007
6008        let cache = self.cache.borrow();
6009        let mut order_ids = Vec::new();
6010
6011        for resting in self.core.iter_orders() {
6012            let Some(order) = cache.order(&resting.client_order_id) else {
6013                continue;
6014            };
6015
6016            if !order.is_reduce_only() || !order.is_open() || !order.is_passive() {
6017                continue;
6018            }
6019
6020            let matches_position = match cache.position_id(&resting.client_order_id) {
6021                Some(id) => *id == position_id,
6022                None => self
6023                    .position_for_order_in_cache(&cache, &order)
6024                    .is_some_and(|position| position.id == position_id),
6025            };
6026
6027            if matches_position {
6028                order_ids.push(resting.client_order_id);
6029            }
6030        }
6031        order_ids.sort_unstable();
6032        order_ids
6033    }
6034
6035    fn sync_reduce_only_orders(
6036        &mut self,
6037        filled_order: &OrderAny,
6038        position: Option<&Position>,
6039        order_ids: &[ClientOrderId],
6040    ) -> anyhow::Result<()> {
6041        for &client_order_id in order_ids {
6042            // Core membership also excludes cancellations awaiting cache acknowledgement
6043            if client_order_id == filled_order.client_order_id()
6044                || !self.core.order_exists(client_order_id)
6045            {
6046                continue;
6047            }
6048
6049            let Some(mut order) = self.order_snapshot(client_order_id) else {
6050                continue;
6051            };
6052
6053            if !order.is_reduce_only() || order.is_closed() || !order.is_passive() {
6054                continue;
6055            }
6056
6057            // Re-read after dispatch: synchronous handlers can apply this fill immediately,
6058            // while pending fills account for a cache that has not acknowledged it yet.
6059            let position = {
6060                let cache = self.cache.borrow();
6061                position.map_or_else(
6062                    || self.position_for_order_in_cache(&cache, &order),
6063                    |position| {
6064                        Some(cache.position(&position.id).map_or_else(
6065                            || position.clone_without_events(),
6066                            |position| position.clone_without_events(),
6067                        ))
6068                    },
6069                )
6070            };
6071
6072            let remaining = self.position_quantity_remaining(&order, position.as_ref())?;
6073            if remaining.is_zero() {
6074                self.cancel_reduce_only_order(&order, filled_order.client_order_id())?;
6075                continue;
6076            }
6077
6078            let leaves = self.parent_capped_leaves(&order, remaining);
6079            let target = order.filled_qty().checked_add(leaves).ok_or_else(|| {
6080                anyhow::anyhow!("Reduce-only quantity overflow for order {client_order_id}")
6081            })?;
6082
6083            if self.resize_order(&mut order, target) {
6084                if target == order.filled_qty() {
6085                    self.cancel_reduce_only_order(&order, filled_order.client_order_id())?;
6086                } else if self.config.support_contingent_orders
6087                    && order.contingency_type() == Some(ContingencyType::Ouo)
6088                {
6089                    self.sync_ouo_leaves(&order, leaves, filled_order.client_order_id())?;
6090                }
6091            }
6092        }
6093
6094        Ok(())
6095    }
6096
6097    fn cancel_reduce_only_order(
6098        &mut self,
6099        order: &OrderAny,
6100        filled_order_id: ClientOrderId,
6101    ) -> anyhow::Result<()> {
6102        let propagate = self.config.support_contingent_orders
6103            && order.contingency_type() == Some(ContingencyType::Ouo);
6104        self.cancel_order(order, Some(!propagate));
6105
6106        if propagate {
6107            self.sync_ouo_leaves(
6108                order,
6109                Quantity::zero(order.quantity().precision),
6110                filled_order_id,
6111            )?;
6112        }
6113        Ok(())
6114    }
6115
6116    fn parent_capped_leaves(&self, order: &OrderAny, leaves: Quantity) -> Quantity {
6117        let parent = if self.config.support_contingent_orders {
6118            order
6119                .parent_order_id()
6120                .and_then(|id| self.order_snapshot(id))
6121        } else {
6122            None
6123        };
6124
6125        parent.map_or(leaves, |parent| {
6126            min(
6127                leaves,
6128                parent.filled_qty().saturating_sub(order.filled_qty()),
6129            )
6130        })
6131    }
6132
6133    fn sync_ouo_leaves(
6134        &mut self,
6135        order: &OrderAny,
6136        leaves: Quantity,
6137        filled_order_id: ClientOrderId,
6138    ) -> anyhow::Result<()> {
6139        for &client_order_id in order.linked_order_ids().into_iter().flatten() {
6140            if client_order_id == filled_order_id || !self.core.order_exists(client_order_id) {
6141                continue;
6142            }
6143
6144            let Some(mut sibling) = self.order_snapshot(client_order_id) else {
6145                continue;
6146            };
6147
6148            if sibling.is_closed() || sibling.is_active_local() || !sibling.is_passive() {
6149                continue;
6150            }
6151
6152            // Cancellation also covers core orders whose acceptance is not yet acknowledged
6153            if leaves.is_zero() {
6154                self.cancel_order(&sibling, Some(false));
6155                continue;
6156            }
6157
6158            if !sibling.is_open() {
6159                continue;
6160            }
6161
6162            let leaves = self.parent_capped_leaves(&sibling, leaves);
6163            let target = sibling.filled_qty().checked_add(leaves).ok_or_else(|| {
6164                anyhow::anyhow!("OUO quantity overflow for order {client_order_id}")
6165            })?;
6166
6167            self.resize_order(&mut sibling, target);
6168
6169            if leaves.is_zero() {
6170                self.cancel_order(&sibling, Some(false));
6171            }
6172        }
6173        Ok(())
6174    }
6175
6176    // Updates rather than modifies since a fill loop may be active, and clears the quote flag,
6177    // so a trigger does not convert the base target again.
6178    fn resize_order(&self, order: &mut OrderAny, target: Quantity) -> bool {
6179        let quote = order.is_quote_quantity() && !self.instrument.is_inverse();
6180
6181        if order.quantity() == target && !quote {
6182            return false;
6183        }
6184
6185        if quote {
6186            order.set_is_quote_quantity(false);
6187        }
6188
6189        self.generate_order_updated(order, target, order.price(), order.trigger_price(), None);
6190        true
6191    }
6192
6193    fn fee_underlying_price(&self) -> CorrectnessResult<Option<Price>> {
6194        if !matches!(
6195            self.instrument,
6196            InstrumentAny::CryptoOption(_) | InstrumentAny::OptionContract(_)
6197        ) {
6198            return Ok(None);
6199        }
6200
6201        let Some(underlying) = self.instrument.underlying() else {
6202            return Ok(None);
6203        };
6204
6205        let underlying_id = InstrumentId::from(format!("{underlying}.{}", self.venue).as_str());
6206        let instrument_id = self.instrument.id();
6207
6208        let cache = self.cache.borrow();
6209        if let Some(price) = cache
6210            .price(&underlying_id, PriceType::Last)
6211            .or_else(|| cache.price(&underlying_id, PriceType::Mark))
6212            .or_else(|| cache.price(&underlying_id, PriceType::Mid))
6213        {
6214            return Ok(Some(price));
6215        }
6216
6217        cache
6218            .option_greeks(&instrument_id)
6219            .and_then(|greeks| greeks.underlying_price)
6220            .map(|price| Price::new_checked(price, FIXED_PRECISION))
6221            .transpose()
6222    }
6223
6224    fn cached_order_is_closed(&self, client_order_id: ClientOrderId) -> bool {
6225        self.cache
6226            .borrow()
6227            .order(&client_order_id)
6228            .is_none_or(|order| order.is_closed())
6229    }
6230
6231    fn purge_cached_filled_qty_if_closed(&mut self, client_order_id: ClientOrderId) {
6232        if self.cached_order_is_closed(client_order_id) {
6233            self.cached_filled_qty.swap_remove(&client_order_id);
6234        }
6235    }
6236
6237    fn purge_closed_cached_filled_qty(&mut self) {
6238        let client_order_ids: Vec<ClientOrderId> = self.cached_filled_qty.keys().copied().collect();
6239
6240        for client_order_id in client_order_ids {
6241            self.purge_cached_filled_qty_if_closed(client_order_id);
6242        }
6243    }
6244
6245    fn update_limit_order(
6246        &mut self,
6247        order: &OrderAny,
6248        quantity: Quantity,
6249        price: Price,
6250    ) -> ModifyOutcome {
6251        if self.core.is_limit_matched(order.order_side(), price) {
6252            if order.is_post_only() {
6253                self.generate_order_modify_rejected(
6254                    order.trader_id(),
6255                    order.strategy_id(),
6256                    order.instrument_id(),
6257                    order.client_order_id(),
6258                    Ustr::from(format!(
6259                        "POST_ONLY {} {} order with new limit px of {} would have been a TAKER: bid={}, ask={}",
6260                        order.order_type(),
6261                        order.order_side(),
6262                        price,
6263                        self.core.bid.map_or_else(|| "None".to_string(), |p| p.to_string()),
6264                        self.core.ask.map_or_else(|| "None".to_string(), |p| p.to_string())
6265                    ).as_str()),
6266                    order.venue_order_id(),
6267                    order.account_id(),
6268                );
6269                return ModifyOutcome::Rejected;
6270            }
6271
6272            self.generate_order_updated(order, quantity, Some(price), None, None);
6273
6274            // Re-read from cache to get the order with events applied
6275            let client_order_id = order.client_order_id();
6276            if let Some(mut order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6277                order.set_liquidity_side(LiquiditySide::Taker);
6278            }
6279            self.fill_limit_order(client_order_id);
6280            return ModifyOutcome::Applied;
6281        }
6282        self.generate_order_updated(order, quantity, Some(price), None, None);
6283        ModifyOutcome::Applied
6284    }
6285
6286    fn update_stop_market_order(
6287        &self,
6288        order: &OrderAny,
6289        quantity: Quantity,
6290        trigger_price: Price,
6291    ) -> ModifyOutcome {
6292        if self.core.is_stop_matched_with_trigger_type(
6293            order.order_side(),
6294            trigger_price,
6295            order.trigger_type().unwrap_or(TriggerType::Default),
6296        ) {
6297            self.generate_order_modify_rejected(
6298                order.trader_id(),
6299                order.strategy_id(),
6300                order.instrument_id(),
6301                order.client_order_id(),
6302                Ustr::from(
6303                    format!(
6304                        "{} {} order new stop px of {} was in the market: bid={}, ask={}",
6305                        order.order_type(),
6306                        order.order_side(),
6307                        trigger_price,
6308                        self.core
6309                            .bid
6310                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
6311                        self.core
6312                            .ask
6313                            .map_or_else(|| "None".to_string(), |p| p.to_string())
6314                    )
6315                    .as_str(),
6316                ),
6317                order.venue_order_id(),
6318                order.account_id(),
6319            );
6320            return ModifyOutcome::Rejected;
6321        }
6322
6323        self.generate_order_updated(order, quantity, None, Some(trigger_price), None);
6324        ModifyOutcome::Applied
6325    }
6326
6327    fn update_stop_limit_order(
6328        &mut self,
6329        order: &OrderAny,
6330        quantity: Quantity,
6331        price: Price,
6332        trigger_price: Price,
6333    ) -> ModifyOutcome {
6334        if order.is_triggered().is_some_and(|t| t) {
6335            if self.core.is_limit_matched(order.order_side(), price) {
6336                return self.update_limit_order(order, quantity, price);
6337            }
6338        } else {
6339            // Update stop price
6340            if self.core.is_stop_matched_with_trigger_type(
6341                order.order_side(),
6342                trigger_price,
6343                order.trigger_type().unwrap_or(TriggerType::Default),
6344            ) {
6345                self.generate_order_modify_rejected(
6346                    order.trader_id(),
6347                    order.strategy_id(),
6348                    order.instrument_id(),
6349                    order.client_order_id(),
6350                    Ustr::from(
6351                        format!(
6352                            "{} {} order new stop px of {} was in the market: bid={}, ask={}",
6353                            order.order_type(),
6354                            order.order_side(),
6355                            trigger_price,
6356                            self.core
6357                                .bid
6358                                .map_or_else(|| "None".to_string(), |p| p.to_string()),
6359                            self.core
6360                                .ask
6361                                .map_or_else(|| "None".to_string(), |p| p.to_string())
6362                        )
6363                        .as_str(),
6364                    ),
6365                    order.venue_order_id(),
6366                    order.account_id(),
6367                );
6368                return ModifyOutcome::Rejected;
6369            }
6370        }
6371
6372        self.generate_order_updated(order, quantity, Some(price), Some(trigger_price), None);
6373        ModifyOutcome::Applied
6374    }
6375
6376    fn update_market_if_touched_order(
6377        &self,
6378        order: &OrderAny,
6379        quantity: Quantity,
6380        trigger_price: Price,
6381    ) -> ModifyOutcome {
6382        if self.core.is_touch_triggered_with_trigger_type(
6383            order.order_side(),
6384            trigger_price,
6385            order.trigger_type().unwrap_or(TriggerType::Default),
6386        ) {
6387            self.generate_order_modify_rejected(
6388                order.trader_id(),
6389                order.strategy_id(),
6390                order.instrument_id(),
6391                order.client_order_id(),
6392                Ustr::from(
6393                    format!(
6394                        "{} {} order new trigger px of {} was in the market: bid={}, ask={}",
6395                        order.order_type(),
6396                        order.order_side(),
6397                        trigger_price,
6398                        self.core
6399                            .bid
6400                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
6401                        self.core
6402                            .ask
6403                            .map_or_else(|| "None".to_string(), |p| p.to_string())
6404                    )
6405                    .as_str(),
6406                ),
6407                order.venue_order_id(),
6408                order.account_id(),
6409            );
6410
6411            // Cannot update order
6412            return ModifyOutcome::Rejected;
6413        }
6414
6415        self.generate_order_updated(order, quantity, None, Some(trigger_price), None);
6416        ModifyOutcome::Applied
6417    }
6418
6419    fn update_limit_if_touched_order(
6420        &mut self,
6421        order: &OrderAny,
6422        quantity: Quantity,
6423        price: Price,
6424        trigger_price: Price,
6425    ) -> ModifyOutcome {
6426        if order.is_triggered().is_some_and(|t| t) {
6427            if self.core.is_limit_matched(order.order_side(), price) {
6428                return self.update_limit_order(order, quantity, price);
6429            }
6430        } else {
6431            // Update trigger price
6432            if self.core.is_touch_triggered_with_trigger_type(
6433                order.order_side(),
6434                trigger_price,
6435                order.trigger_type().unwrap_or(TriggerType::Default),
6436            ) {
6437                self.generate_order_modify_rejected(
6438                    order.trader_id(),
6439                    order.strategy_id(),
6440                    order.instrument_id(),
6441                    order.client_order_id(),
6442                    Ustr::from(
6443                        format!(
6444                            "{} {} order new trigger px of {} was in the market: bid={}, ask={}",
6445                            order.order_type(),
6446                            order.order_side(),
6447                            trigger_price,
6448                            self.core
6449                                .bid
6450                                .map_or_else(|| "None".to_string(), |p| p.to_string()),
6451                            self.core
6452                                .ask
6453                                .map_or_else(|| "None".to_string(), |p| p.to_string())
6454                        )
6455                        .as_str(),
6456                    ),
6457                    order.venue_order_id(),
6458                    order.account_id(),
6459                );
6460                return ModifyOutcome::Rejected;
6461            }
6462        }
6463
6464        self.generate_order_updated(order, quantity, Some(price), Some(trigger_price), None);
6465        ModifyOutcome::Applied
6466    }
6467
6468    fn update_trailing_stop_order(&self, order: &OrderAny) {
6469        let (new_trigger_price, new_price) = match trailing_stop_calculate(
6470            self.instrument.price_increment(),
6471            order.trigger_price(),
6472            order,
6473            self.core.bid,
6474            self.core.ask,
6475            self.core.last,
6476        ) {
6477            Ok(prices) => prices,
6478            Err(e) => {
6479                // Missing market data yet: await the next update to compute the trigger.
6480                log::debug!("Cannot calculate trailing-stop update: {e}");
6481                return;
6482            }
6483        };
6484
6485        if new_trigger_price.is_none() && new_price.is_none() {
6486            return;
6487        }
6488
6489        self.generate_order_updated(order, order.quantity(), new_price, new_trigger_price, None);
6490    }
6491
6492    fn accept_order(&mut self, order: &mut OrderAny) {
6493        if order.is_closed() {
6494            // Temporary guard to prevent invalid processing
6495            return;
6496        }
6497
6498        if order.status() != OrderStatus::Accepted {
6499            let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
6500            let event = self.create_order_accepted(order, venue_order_id);
6501
6502            // Apply locally so `cancel_order` sees `Accepted`,
6503            // dispatch on apply failure so `Released` still registers with the core.
6504            if let Err(e) = order.apply(event.clone()) {
6505                log::warn!(
6506                    "Skipping local apply of accepted event for {}: {e}",
6507                    order.client_order_id(),
6508                );
6509            }
6510            self.dispatch_order_event(event);
6511
6512            // Activate before emitting `OrderUpdated` so `match_info` below
6513            // carries the activation flag.
6514            if matches!(
6515                order.order_type(),
6516                OrderType::TrailingStopLimit | OrderType::TrailingStopMarket
6517            ) && order.trigger_price().is_none()
6518                && self.maybe_activate_trailing_stop(
6519                    order,
6520                    self.core.bid,
6521                    self.core.ask,
6522                    self.core.last,
6523                )
6524            {
6525                self.update_trailing_stop_order(order);
6526            }
6527        }
6528
6529        let match_info = Self::matching_core_entry(order);
6530        self.track_post_match_order(order);
6531        self.core.add_order(match_info);
6532    }
6533
6534    fn track_post_match_order(&mut self, order: &OrderAny) {
6535        self.post_match_order_ids.insert(order.client_order_id());
6536    }
6537
6538    fn delete_core_order(&mut self, client_order_id: ClientOrderId) {
6539        self.post_match_order_ids.swap_remove(&client_order_id);
6540        let _ = self.core.delete_order(client_order_id);
6541    }
6542
6543    fn requires_post_match_maintenance(order: &OrderAny) -> bool {
6544        order.expire_time().is_some()
6545            || matches!(
6546                order.order_type(),
6547                OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
6548            )
6549    }
6550
6551    fn matching_core_entry(order: &OrderAny) -> RestingOrder {
6552        let is_limit_style_stop = matches!(
6553            order.order_type(),
6554            OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit
6555        );
6556        let is_triggered = order.is_triggered().is_some_and(|triggered| triggered);
6557        let triggered_limit_style = is_limit_style_stop && is_triggered;
6558
6559        // A trailing stop limit awaiting its first trigger price must not rest as a plain limit
6560        let limit_price = if is_limit_style_stop && !is_triggered && order.trigger_price().is_none()
6561        {
6562            None
6563        } else {
6564            order.price()
6565        };
6566
6567        RestingOrder::new_with_trigger_type(
6568            order.client_order_id(),
6569            order.order_side(),
6570            order.order_type(),
6571            Some(order.trigger_type().unwrap_or(TriggerType::Default)),
6572            if triggered_limit_style {
6573                None
6574            } else {
6575                order.trigger_price()
6576            },
6577            limit_price,
6578            match order {
6579                OrderAny::TrailingStopMarket(o) => o.is_activated,
6580                OrderAny::TrailingStopLimit(o) => o.is_activated,
6581                _ => true,
6582            },
6583        )
6584    }
6585
6586    fn expire_order(&mut self, order: &OrderAny) {
6587        self.remove_queue_position(order.client_order_id());
6588
6589        if self.config.support_contingent_orders && order.contingency_type().is_some() {
6590            self.cancel_contingent_orders(order, &[]);
6591        }
6592
6593        self.generate_order_expired(order);
6594    }
6595
6596    fn cancel_order(&mut self, order: &OrderAny, cancel_contingencies: Option<bool>) {
6597        self.cancel_order_excluding(order, cancel_contingencies, &[]);
6598    }
6599
6600    /// Cancels `order`, leaving `excluded` untouched should the cancellation cascade into its
6601    /// contingent orders.
6602    fn cancel_order_excluding(
6603        &mut self,
6604        order: &OrderAny,
6605        cancel_contingencies: Option<bool>,
6606        excluded: &[ClientOrderId],
6607    ) {
6608        if self.inflight_orders.contains(order.client_order_id()) {
6609            return;
6610        }
6611
6612        let cancel_contingencies = cancel_contingencies.unwrap_or(true);
6613
6614        if order.is_active_local()
6615            && !matches!(
6616                (order.status(), order.order_type()),
6617                (
6618                    OrderStatus::Initialized | OrderStatus::Released,
6619                    OrderType::Market
6620                )
6621            )
6622        {
6623            log::error!(
6624                "Cannot cancel an order with {} from the matching engine",
6625                order.status()
6626            );
6627            return;
6628        }
6629
6630        // Check if order exists in OrderMatching core, and delete it if it does
6631        if self.core.order_exists(order.client_order_id()) {
6632            self.delete_core_order(order.client_order_id());
6633        }
6634
6635        self.pending_oto_order_ids
6636            .swap_remove(&order.client_order_id());
6637        self.remove_queue_position(order.client_order_id());
6638
6639        let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
6640        self.generate_order_canceled(order, venue_order_id);
6641
6642        // A canceled child still looks held until the cache applies the cancel
6643        if self.config.support_contingent_orders
6644            && order.parent_order_id().is_some()
6645            && !self.cached_order_is_closed(order.client_order_id())
6646        {
6647            self.canceled_oto_order_ids.insert(order.client_order_id());
6648        }
6649
6650        if self.config.support_contingent_orders
6651            && order.contingency_type().is_some()
6652            && cancel_contingencies
6653        {
6654            self.cancel_contingent_orders(order, excluded);
6655        }
6656
6657        // OTO children are sized from this order's fills, and OTO siblings count them, so keep
6658        // them until the cache shows the order closed
6659        self.purge_cached_filled_qty_if_closed(order.client_order_id());
6660    }
6661
6662    fn update_order(
6663        &mut self,
6664        order: &OrderAny,
6665        quantity: Option<Quantity>,
6666        price: Option<Price>,
6667        trigger_price: Option<Price>,
6668        update_contingencies: Option<bool>,
6669    ) -> bool {
6670        if self.inflight_orders.contains(order.client_order_id()) {
6671            return false;
6672        }
6673
6674        let update_contingencies = update_contingencies.unwrap_or(true);
6675        let quantity = quantity.unwrap_or(order.quantity());
6676
6677        let size_prec = self.instrument.size_precision();
6678        let instrument_id = self.instrument.id();
6679
6680        if !order_precision_valid(quantity.precision, size_prec) {
6681            self.generate_order_modify_rejected(
6682                order.trader_id(),
6683                order.strategy_id(),
6684                order.instrument_id(),
6685                order.client_order_id(),
6686                Ustr::from(&format!(
6687                    "Invalid update quantity precision {}, expected {size_prec} for {instrument_id}",
6688                    quantity.precision
6689                )),
6690                order.venue_order_id(),
6691                order.account_id(),
6692            );
6693            return false;
6694        }
6695
6696        // Validate only the prices this update changes: internal quantity syncs pass back current
6697        // prices, which a restored order may hold off the price increment.
6698        let price_rejection = price
6699            .filter(|px| Some(*px) != order.price())
6700            .and_then(|px| self.update_price_rejection("price", px))
6701            .or_else(|| {
6702                trigger_price
6703                    .filter(|tp| Some(*tp) != order.trigger_price())
6704                    .and_then(|tp| self.update_price_rejection("trigger_price", tp))
6705            });
6706
6707        if let Some(reason) = price_rejection {
6708            self.generate_order_modify_rejected(
6709                order.trader_id(),
6710                order.strategy_id(),
6711                order.instrument_id(),
6712                order.client_order_id(),
6713                Ustr::from(&reason),
6714                order.venue_order_id(),
6715                order.account_id(),
6716            );
6717            return false;
6718        }
6719
6720        // Use cached_filled_qty since PassiveOrderAny in core is not updated with fills
6721        let filled_qty = self
6722            .cached_filled_qty
6723            .get(&order.client_order_id())
6724            .copied()
6725            .unwrap_or(order.filled_qty());
6726        if quantity < filled_qty {
6727            self.generate_order_modify_rejected(
6728                order.trader_id(),
6729                order.strategy_id(),
6730                order.instrument_id(),
6731                order.client_order_id(),
6732                Ustr::from(&format!(
6733                    "Cannot reduce order quantity {quantity} below filled quantity {filled_qty}",
6734                )),
6735                order.venue_order_id(),
6736                order.account_id(),
6737            );
6738            return false;
6739        }
6740
6741        let outcome = match order {
6742            OrderAny::Limit(_) | OrderAny::MarketToLimit(_) => {
6743                let price = price.unwrap_or(order.price().unwrap());
6744                self.update_limit_order(order, quantity, price)
6745            }
6746            OrderAny::StopMarket(_) => {
6747                let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
6748                self.update_stop_market_order(order, quantity, trigger_price)
6749            }
6750            OrderAny::StopLimit(_) => {
6751                let price = price.unwrap_or(order.price().unwrap());
6752                let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
6753                self.update_stop_limit_order(order, quantity, price, trigger_price)
6754            }
6755            OrderAny::MarketIfTouched(_) => {
6756                let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
6757                self.update_market_if_touched_order(order, quantity, trigger_price)
6758            }
6759            OrderAny::LimitIfTouched(_) => {
6760                let price = price.unwrap_or(order.price().unwrap());
6761                let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
6762                self.update_limit_if_touched_order(order, quantity, price, trigger_price)
6763            }
6764            // A trailing stop that is activated, or activates at the current market, rests as a
6765            // stop, so its new trigger is validated like one (as on submit). Otherwise it cannot
6766            // trigger yet, so the trigger is not checked
6767            OrderAny::TrailingStopMarket(inner) => match trigger_price.or(order.trigger_price()) {
6768                Some(trigger_price)
6769                    if inner.is_activated || self.trailing_stop_activates_now(order) =>
6770                {
6771                    self.update_stop_market_order(order, quantity, trigger_price)
6772                }
6773                trigger_price => {
6774                    self.generate_order_updated(order, quantity, None, trigger_price, None);
6775                    ModifyOutcome::Applied
6776                }
6777            },
6778            OrderAny::TrailingStopLimit(inner) => match (
6779                price.or(order.price()),
6780                trigger_price.or(order.trigger_price()),
6781            ) {
6782                (Some(price), Some(trigger_price))
6783                    if inner.is_activated || self.trailing_stop_activates_now(order) =>
6784                {
6785                    self.update_stop_limit_order(order, quantity, price, trigger_price)
6786                }
6787                (price, trigger_price) => {
6788                    self.generate_order_updated(order, quantity, price, trigger_price, None);
6789                    ModifyOutcome::Applied
6790                }
6791            },
6792            _ => {
6793                panic!(
6794                    "Unsupported order type {} for update_order",
6795                    order.order_type()
6796                );
6797            }
6798        };
6799
6800        if outcome == ModifyOutcome::Rejected {
6801            return false;
6802        }
6803
6804        // If order now has zero leaves after update, cancel it
6805        let new_leaves_qty = quantity.saturating_sub(filled_qty);
6806        if new_leaves_qty.is_zero() {
6807            if self.config.support_contingent_orders
6808                && order.contingency_type().is_some()
6809                && update_contingencies
6810            {
6811                self.update_contingent_order(order, quantity);
6812            }
6813
6814            // Pass false since we already handled contingents above
6815            self.cancel_order(order, Some(false));
6816            return true;
6817        }
6818
6819        if self.config.support_contingent_orders
6820            && order.contingency_type().is_some()
6821            && update_contingencies
6822        {
6823            self.update_contingent_order(order, quantity);
6824        }
6825
6826        true
6827    }
6828
6829    fn update_price_rejection(&self, field: &str, price: Price) -> Option<String> {
6830        let price_precision = self.instrument.price_precision();
6831        let instrument_id = self.instrument.id();
6832
6833        if !order_precision_valid(price.precision, price_precision) {
6834            return Some(format!(
6835                "Invalid update {field} precision {}, expected {price_precision} for {instrument_id}",
6836                price.precision
6837            ));
6838        }
6839
6840        let price_increment = self.instrument.price_increment();
6841        if !Self::price_matches_tick(price, price_increment) {
6842            return Some(format!(
6843                "Invalid update {field} {price}, not a multiple of {instrument_id} price increment {price_increment}"
6844            ));
6845        }
6846
6847        None
6848    }
6849
6850    /// Triggers a stop order, converting it to an active market or limit order.
6851    pub fn trigger_stop_order(&mut self, client_order_id: ClientOrderId) {
6852        let order = match self.order_snapshot(client_order_id) {
6853            Some(order) => order,
6854            None => {
6855                log::error!(
6856                    "Cannot trigger stop order: order {client_order_id} not found in cache"
6857                );
6858                return;
6859            }
6860        };
6861
6862        if order.is_closed() {
6863            log::debug!("Cannot trigger stop order: {client_order_id} already closed");
6864            return;
6865        }
6866
6867        match order.order_type() {
6868            OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit => {
6869                self.trigger_limit_style_stop_order(client_order_id, order);
6870            }
6871            OrderType::StopMarket | OrderType::MarketIfTouched | OrderType::TrailingStopMarket => {
6872                self.fill_market_order(client_order_id);
6873            }
6874            _ => {
6875                log::error!(
6876                    "Cannot trigger stop order: invalid order type {}",
6877                    order.order_type()
6878                );
6879            }
6880        }
6881    }
6882
6883    fn trigger_limit_style_stop_order(&mut self, client_order_id: ClientOrderId, order: OrderAny) {
6884        if order.is_triggered().is_some_and(|triggered| triggered) {
6885            let liquidity_side = match (order.price(), order.trigger_price()) {
6886                (Some(price), Some(trigger_price)) => Self::determine_triggered_limit_liquidity(
6887                    order.order_side(),
6888                    price,
6889                    trigger_price,
6890                ),
6891                _ => LiquiditySide::Maker,
6892            };
6893
6894            if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id)
6895                && !matches!(
6896                    cached_order.liquidity_side(),
6897                    Some(LiquiditySide::Maker | LiquiditySide::Taker)
6898                )
6899            {
6900                cached_order.set_liquidity_side(liquidity_side);
6901            }
6902            self.fill_limit_order(client_order_id);
6903            return;
6904        }
6905
6906        let event = self.create_order_triggered(&order);
6907        let order = match self.cache.borrow_mut().update_order(&event) {
6908            Ok(order) => order,
6909            Err(e) => {
6910                log::debug!(
6911                    "Failed to apply triggered event for {} before fill: {e}",
6912                    order.client_order_id(),
6913                );
6914                order
6915            }
6916        };
6917        let order = self.order_snapshot(client_order_id).unwrap_or(order);
6918        self.dispatch_order_event(event);
6919
6920        let trigger_price = order
6921            .trigger_price()
6922            .expect("Limit-style stop order must have a trigger price");
6923        let price = order
6924            .price()
6925            .expect("Limit-style stop order must have a price");
6926
6927        let maker_inside = match order.order_side() {
6928            OrderSide::Buy => self
6929                .core
6930                .ask
6931                .is_some_and(|ask| trigger_price > price && price > ask),
6932            OrderSide::Sell => self
6933                .core
6934                .bid
6935                .is_some_and(|bid| trigger_price < price && price < bid),
6936        };
6937
6938        if maker_inside {
6939            if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6940                cached_order.set_liquidity_side(LiquiditySide::Maker);
6941            }
6942            self.resync_core_entry(client_order_id);
6943            self.fill_limit_order(client_order_id);
6944            return;
6945        }
6946
6947        if self.core.is_limit_matched(order.order_side(), price) {
6948            if order.is_post_only() {
6949                self.delete_core_order(client_order_id);
6950                self.cached_filled_qty.swap_remove(&client_order_id);
6951                let event = self.create_order_rejected(
6952                    &order,
6953                    format!(
6954                        "POST_ONLY {} {} order limit px of {} would have been a TAKER: bid={}, ask={}",
6955                        order.order_type(),
6956                        order.order_side(),
6957                        price,
6958                        self.core
6959                            .bid
6960                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
6961                        self.core
6962                            .ask
6963                            .map_or_else(|| "None".to_string(), |p| p.to_string())
6964                    )
6965                    .into(),
6966                );
6967
6968                if let Err(e) = self.cache.borrow_mut().update_order(&event) {
6969                    log::debug!(
6970                        "Failed to apply rejected event for {} after post-only trigger: {e}",
6971                        order.client_order_id(),
6972                    );
6973                }
6974                self.dispatch_order_event(event);
6975                return;
6976            }
6977
6978            if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6979                cached_order.set_liquidity_side(LiquiditySide::Taker);
6980            }
6981            self.resync_core_entry(client_order_id);
6982            self.fill_limit_order(client_order_id);
6983            return;
6984        }
6985
6986        if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6987            cached_order.set_liquidity_side(Self::determine_triggered_limit_liquidity(
6988                order.order_side(),
6989                price,
6990                trigger_price,
6991            ));
6992        }
6993        self.resync_core_entry(client_order_id);
6994    }
6995
6996    fn determine_triggered_limit_liquidity(
6997        side: OrderSide,
6998        price: Price,
6999        trigger_price: Price,
7000    ) -> LiquiditySide {
7001        if (side == OrderSide::Buy && trigger_price > price)
7002            || (side == OrderSide::Sell && trigger_price < price)
7003        {
7004            LiquiditySide::Maker
7005        } else {
7006            LiquiditySide::Taker
7007        }
7008    }
7009
7010    fn update_contingent_order(&mut self, order: &OrderAny, parent_quantity: Quantity) {
7011        log::debug!(
7012            "Updating contingent orders from {}",
7013            order.client_order_id()
7014        );
7015
7016        if order.contingency_type() == Some(ContingencyType::Oto) {
7017            self.update_oto_children(order, Some(parent_quantity), &[]);
7018            return;
7019        }
7020
7021        if let Some(linked_order_ids) = order.linked_order_ids() {
7022            let parent_filled_qty = self
7023                .cached_filled_qty
7024                .get(&order.client_order_id())
7025                .copied()
7026                .unwrap_or(order.filled_qty());
7027            let parent_leaves_qty = parent_quantity.saturating_sub(parent_filled_qty);
7028
7029            for client_order_id in linked_order_ids {
7030                let child_order = match self.order_snapshot(*client_order_id) {
7031                    Some(order) => order,
7032                    None => panic!("Order {client_order_id} not found in cache."),
7033                };
7034
7035                if child_order.is_active_local() {
7036                    continue;
7037                }
7038
7039                let child_filled_qty = self
7040                    .cached_filled_qty
7041                    .get(&child_order.client_order_id())
7042                    .copied()
7043                    .unwrap_or(child_order.filled_qty());
7044
7045                if parent_leaves_qty.is_zero() {
7046                    self.cancel_order(&child_order, Some(false));
7047                } else if child_filled_qty >= parent_leaves_qty {
7048                    // Child already filled beyond parent's remaining qty, cancel it
7049                    self.cancel_order(&child_order, Some(false));
7050                } else {
7051                    let child_leaves_qty = child_order.quantity().saturating_sub(child_filled_qty);
7052                    if child_leaves_qty != parent_leaves_qty {
7053                        let price = child_order.price();
7054                        let trigger_price = child_order.trigger_price();
7055                        self.update_order(
7056                            &child_order,
7057                            Some(parent_leaves_qty),
7058                            price,
7059                            trigger_price,
7060                            Some(false),
7061                        );
7062                    }
7063                }
7064            }
7065        }
7066    }
7067
7068    // The core holds the orders this engine accepted and `pending_oto_order_ids` the OTO children
7069    // it parked, so a locally active cached status for either means their submit, and for an
7070    // accepted order its acceptance, went through a deferring event handler and is not applied
7071    // yet. Applies them to the snapshot, as `accept_order` applies its acceptance, so a
7072    // cancellation dispatched now follows them. `accepted_elsewhere` says the same of an order
7073    // another instrument's engine accepted.
7074    fn apply_deferred_submission(
7075        &mut self,
7076        order: &mut OrderAny,
7077        accepted_elsewhere: bool,
7078    ) -> anyhow::Result<()> {
7079        let client_order_id = order.client_order_id();
7080        let accepted = self.core.order_exists(client_order_id) || accepted_elsewhere;
7081
7082        if !order.is_active_local()
7083            || !(accepted || self.pending_oto_order_ids.contains(&client_order_id))
7084        {
7085            return Ok(());
7086        }
7087
7088        let account_id = match order.account_id() {
7089            Some(account_id) => account_id,
7090            None => *self
7091                .account_ids
7092                .get(&order.trader_id())
7093                .ok_or_else(|| anyhow::anyhow!("No account ID for {}", order.trader_id()))?,
7094        };
7095        let event = self.create_order_submitted(order, account_id);
7096        order.apply(event)?;
7097
7098        if accepted {
7099            let venue_order_id = self.ids_generator.get_venue_order_id(order)?;
7100            let event = self.create_order_accepted(order, venue_order_id);
7101            order.apply(event)?;
7102        }
7103        Ok(())
7104    }
7105
7106    /// Returns whether `order` works in another instrument's engine, as far as the cache shows.
7107    ///
7108    /// The venue routes an order to its instrument's engine, which claims its venue order ID in the
7109    /// cache as it accepts it, before any event is dispatched. A cancel or fill there shows only
7110    /// once its event is applied. This engine itself matches the children of another instrument
7111    /// that it releases.
7112    fn works_in_another_engine(&self, order: &OrderAny) -> bool {
7113        let client_order_id = order.client_order_id();
7114
7115        order.instrument_id() != self.instrument.id()
7116            && !self
7117                .other_instrument_oto_order_ids
7118                .contains(&client_order_id)
7119            && self
7120                .cache
7121                .borrow()
7122                .venue_order_id(&client_order_id)
7123                .is_some()
7124    }
7125
7126    /// Sizes the children of an OTO `parent` after it is modified to `parent_quantity`, or closes
7127    /// when `parent_quantity` is `None`.
7128    ///
7129    /// Each child's remaining quantity covers what the parent can still hold, less what the
7130    /// children have filled between them: the parent's quantity while it works, and its filled
7131    /// quantity once it closes. Children only grow when the parent's quantity increases. A closing
7132    /// parent cancels any child left with nothing to cover or still waiting for release, except
7133    /// that under the full trigger reducing it to its filled quantity releases those children, as
7134    /// that completes it. Without the full trigger, once the parent has fills a child out of the
7135    /// book is left alone, as it is already closing, unless it works in another instrument's
7136    /// engine.
7137    ///
7138    /// Quantities compare in the parent's units. A child still holding a quote quantity under a
7139    /// parent in base is compared as a base quantity, which it holds once it is resized or the
7140    /// parent closes. A child working in another instrument's engine covers what that
7141    /// instrument's size precision can hold.
7142    fn update_oto_children(
7143        &mut self,
7144        parent: &OrderAny,
7145        parent_quantity: Option<Quantity>,
7146        excluded: &[ClientOrderId],
7147    ) {
7148        let Some(linked_order_ids) = parent.linked_order_ids() else {
7149            return;
7150        };
7151        let parent_snapshot = self
7152            .order_snapshot(parent.client_order_id())
7153            .unwrap_or_else(|| parent.clone());
7154        let parent_filled_qty = self.engine_filled_qty(&parent_snapshot);
7155        let parent_quote = parent_snapshot.is_quote_quantity() && !self.instrument.is_inverse();
7156        let reduced_to_filled = parent_quantity.is_some_and(|q| q <= parent_filled_qty);
7157        let parent_quantity = parent_quantity.unwrap_or(parent_filled_qty);
7158        let parent_closed = parent_quantity <= parent_filled_qty;
7159        // `parent` precedes the modify, and reduce-only children track fills between modifies
7160        let parent_grew = parent_quantity > parent.quantity();
7161        let mut released_children = false;
7162
7163        for client_order_id in linked_order_ids {
7164            if excluded.contains(client_order_id) {
7165                // The venue has not received this order's submit yet
7166                continue;
7167            }
7168
7169            let mut child_order = match self.order_snapshot(*client_order_id) {
7170                Some(order) => order,
7171                None => panic!("Cannot find contingent order for {client_order_id}"),
7172            };
7173
7174            let elsewhere = self.works_in_another_engine(&child_order);
7175
7176            if let Err(e) = self.apply_deferred_submission(&mut child_order, elsewhere) {
7177                log::error!("Cannot update contingent order {client_order_id}: {e}");
7178                continue;
7179            }
7180
7181            if child_order.is_active_local()
7182                || child_order.is_closed()
7183                || self.canceled_oto_order_ids.contains(client_order_id)
7184                || self.inflight_orders.contains(*client_order_id)
7185            {
7186                continue;
7187            }
7188
7189            // Recounted per child, as releasing a marketable child fills it at once
7190            let children_filled = linked_order_ids
7191                .iter()
7192                .filter_map(|id| self.order_snapshot(*id))
7193                .fold(Quantity::zero(parent_quantity.precision), |acc, child| {
7194                    acc + self.engine_filled_qty(&child)
7195                });
7196            let remaining = parent_quantity.saturating_sub(children_filled);
7197
7198            let remaining = if elsewhere {
7199                // The parent's quantity can carry more decimals than the size precision of the
7200                // instrument whose engine matches this child
7201                let cache = self.cache.borrow();
7202                cache
7203                    .instrument(&child_order.instrument_id())
7204                    .map_or(remaining, |instrument| {
7205                        instrument
7206                            .try_make_qty_from_decimal(remaining.as_decimal(), Some(true))
7207                            .unwrap_or_else(|_| Quantity::zero(instrument.size_precision()))
7208                    })
7209            } else {
7210                remaining
7211            };
7212
7213            // A released child is in the book even before its deferred acceptance applies, or
7214            // works in another instrument's engine
7215            let released = self.core.order_exists(*client_order_id) || elsewhere;
7216
7217            // Without the full trigger the parent's first fill released every child it held, so
7218            // one out of the book is already closing, though the cache may not show it yet
7219            if !released && !self.config.oto_full_trigger && !parent_filled_qty.is_zero() {
7220                continue;
7221            }
7222            let release = !released && reduced_to_filled && self.config.oto_full_trigger;
7223            if parent_closed && ((!released && !release) || remaining.is_zero()) {
7224                self.cancel_order(&child_order, Some(false));
7225                continue;
7226            }
7227
7228            if remaining.is_zero() {
7229                // The parent has nothing left for this child to cover
7230                continue;
7231            }
7232
7233            let quote = child_order.is_quote_quantity();
7234
7235            if self.convert_to_parent_units(&mut child_order, parent_quote, elsewhere) {
7236                let converted = quote != child_order.is_quote_quantity();
7237                let mut quantity = self.engine_filled_qty(&child_order) + remaining;
7238                if parent_closed || !parent_grew {
7239                    quantity = quantity.min(child_order.quantity());
7240                }
7241
7242                // A closing parent leaves a converted child in base, so that the child's own
7243                // conversion cannot take it past the parent's fills
7244                if quantity != child_order.quantity() || (parent_closed && converted) {
7245                    // Maintenance only: a modify would re-validate prices and match the child
7246                    self.generate_order_updated(
7247                        &child_order,
7248                        quantity,
7249                        child_order.price(),
7250                        child_order.trigger_price(),
7251                        None,
7252                    );
7253                }
7254            }
7255
7256            if release {
7257                let account_id = parent
7258                    .account_id()
7259                    .or_else(|| self.account_ids.get(&parent.trader_id()).copied());
7260
7261                match (self.order_snapshot(*client_order_id), account_id) {
7262                    (Some(mut child_order), Some(account_id)) => {
7263                        self.process_order(&mut child_order, account_id);
7264                        released_children = true;
7265                    }
7266                    _ => log::error!("Cannot release OTO order {client_order_id}"),
7267                }
7268            }
7269        }
7270
7271        // A child released above can fill at once, leaving less for those sized before it, so
7272        // size the children resting in the book again
7273        if released_children {
7274            let not_resting: Vec<ClientOrderId> = linked_order_ids
7275                .iter()
7276                .filter(|id| excluded.contains(*id) || !self.core.order_exists(**id))
7277                .copied()
7278                .collect();
7279            self.update_oto_children(parent, Some(parent_quantity), &not_resting);
7280        }
7281    }
7282
7283    /// Puts the snapshot of an OTO `child` in its parent's units, returning whether the parent
7284    /// can size it.
7285    ///
7286    /// A child still holding a quote quantity under a parent in base converts at its price, else
7287    /// its trigger price, else the opposing best. Its submission converts it at its price or the
7288    /// opposing best, and its trigger at the opposing best of that moment, which the trigger
7289    /// price stands in for. Without such a price it cannot be sized, nor can a child in base
7290    /// under a parent still holding a quote quantity. An inverse instrument converts nothing. A
7291    /// child working `elsewhere` converts by its own instrument, and not at this engine's book.
7292    fn convert_to_parent_units(
7293        &self,
7294        child: &mut OrderAny,
7295        parent_quote: bool,
7296        elsewhere: bool,
7297    ) -> bool {
7298        let cache = self.cache.borrow();
7299        let instrument = cache
7300            .instrument(&child.instrument_id())
7301            .filter(|_| elsewhere)
7302            .unwrap_or(&self.instrument);
7303        let child_quote = child.is_quote_quantity() && !instrument.is_inverse();
7304
7305        if child_quote == parent_quote {
7306            return true;
7307        }
7308
7309        if parent_quote {
7310            return false;
7311        }
7312
7313        let opposing_best = match child.order_side() {
7314            OrderSide::Buy => self.core.ask,
7315            OrderSide::Sell => self.core.bid,
7316        };
7317        let reference_price = child
7318            .price()
7319            .or(child.trigger_price())
7320            .or(opposing_best.filter(|_| !elsewhere));
7321        let base_quantity = reference_price.and_then(|price| {
7322            instrument
7323                .try_calculate_base_quantity(child.quantity(), price)
7324                .ok()
7325        });
7326
7327        let Some(base_quantity) = base_quantity else {
7328            return false;
7329        };
7330        child.set_quantity(base_quantity);
7331        child.set_is_quote_quantity(false);
7332        true
7333    }
7334
7335    fn engine_filled_qty(&self, order: &OrderAny) -> Quantity {
7336        self.cached_filled_qty
7337            .get(&order.client_order_id())
7338            .copied()
7339            .unwrap_or(order.filled_qty())
7340    }
7341
7342    fn cancel_contingent_orders(&mut self, order: &OrderAny, excluded: &[ClientOrderId]) {
7343        if order.contingency_type() == Some(ContingencyType::Oto) {
7344            self.update_oto_children(order, None, excluded);
7345            return;
7346        }
7347
7348        if let Some(linked_order_ids) = order.linked_order_ids() {
7349            for client_order_id in linked_order_ids {
7350                if excluded.contains(client_order_id) {
7351                    continue;
7352                }
7353
7354                let mut contingent_order = match self.order_snapshot(*client_order_id) {
7355                    Some(order) => order,
7356                    None => panic!("Cannot find contingent order for {client_order_id}"),
7357                };
7358
7359                if let Err(e) = self.apply_deferred_submission(&mut contingent_order, false) {
7360                    log::error!("Cannot cancel contingent order {client_order_id}: {e}");
7361                    continue;
7362                }
7363
7364                if contingent_order.is_active_local() {
7365                    // order is not on the exchange yet
7366                    continue;
7367                }
7368
7369                if !contingent_order.is_closed() {
7370                    self.cancel_order(&contingent_order, Some(false));
7371                }
7372            }
7373        }
7374    }
7375
7376    fn create_order_submitted(&self, order: &OrderAny, account_id: AccountId) -> OrderEventAny {
7377        let ts_now = self.clock.borrow().timestamp_ns();
7378        OrderEventAny::Submitted(OrderSubmitted::new(
7379            order.trader_id(),
7380            order.strategy_id(),
7381            order.instrument_id(),
7382            order.client_order_id(),
7383            account_id,
7384            UUID4::new(),
7385            ts_now,
7386            ts_now,
7387        ))
7388    }
7389
7390    fn generate_order_submitted(&self, order: &OrderAny, account_id: AccountId) {
7391        let event = self.create_order_submitted(order, account_id);
7392        self.dispatch_order_event(event);
7393    }
7394
7395    fn create_order_rejected(&self, order: &OrderAny, reason: Ustr) -> OrderEventAny {
7396        let ts_now = self.clock.borrow().timestamp_ns();
7397        let account_id = order
7398            .account_id()
7399            .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
7400
7401        let due_post_only = reason.starts_with("POST_ONLY");
7402
7403        OrderEventAny::Rejected(OrderRejected::new(
7404            order.trader_id(),
7405            order.strategy_id(),
7406            order.instrument_id(),
7407            order.client_order_id(),
7408            account_id,
7409            reason,
7410            UUID4::new(),
7411            ts_now,
7412            ts_now,
7413            false,
7414            due_post_only,
7415        ))
7416    }
7417
7418    fn generate_order_rejected(&self, order: &OrderAny, reason: Ustr) {
7419        if order.contingency_type() == Some(ContingencyType::Oto) {
7420            self.rejected_oto_parent_ids
7421                .borrow_mut()
7422                .insert(order.client_order_id());
7423        }
7424
7425        let event = self.create_order_rejected(order, reason);
7426        self.dispatch_order_event(event);
7427    }
7428
7429    fn publish_order_initialized(&self, order: &OrderAny) {
7430        let event = OrderEventAny::Initialized(order.init_event().clone());
7431        let topic = msgbus::switchboard::get_event_order_topic(order.strategy_id());
7432        msgbus::publish_order_event(topic, &event);
7433    }
7434
7435    fn create_order_accepted(
7436        &self,
7437        order: &OrderAny,
7438        venue_order_id: VenueOrderId,
7439    ) -> OrderEventAny {
7440        let ts_now = self.clock.borrow().timestamp_ns();
7441        let account_id = order
7442            .account_id()
7443            .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
7444        OrderEventAny::Accepted(OrderAccepted::new(
7445            order.trader_id(),
7446            order.strategy_id(),
7447            order.instrument_id(),
7448            order.client_order_id(),
7449            venue_order_id,
7450            account_id,
7451            UUID4::new(),
7452            ts_now,
7453            ts_now,
7454            false,
7455        ))
7456    }
7457
7458    fn generate_order_accepted(&self, order: &OrderAny, venue_order_id: VenueOrderId) {
7459        let event = self.create_order_accepted(order, venue_order_id);
7460        self.dispatch_order_event(event);
7461    }
7462
7463    #[expect(clippy::too_many_arguments)]
7464    fn generate_order_modify_rejected(
7465        &self,
7466        trader_id: TraderId,
7467        strategy_id: StrategyId,
7468        instrument_id: InstrumentId,
7469        client_order_id: ClientOrderId,
7470        reason: Ustr,
7471        venue_order_id: Option<VenueOrderId>,
7472        account_id: Option<AccountId>,
7473    ) {
7474        let ts_now = self.clock.borrow().timestamp_ns();
7475        let event = OrderEventAny::ModifyRejected(OrderModifyRejected::new(
7476            trader_id,
7477            strategy_id,
7478            instrument_id,
7479            client_order_id,
7480            reason,
7481            UUID4::new(),
7482            ts_now,
7483            ts_now,
7484            false,
7485            venue_order_id,
7486            account_id,
7487        ));
7488        self.dispatch_order_event(event);
7489    }
7490
7491    #[expect(clippy::too_many_arguments)]
7492    fn generate_order_cancel_rejected(
7493        &self,
7494        trader_id: TraderId,
7495        strategy_id: StrategyId,
7496        account_id: AccountId,
7497        instrument_id: InstrumentId,
7498        client_order_id: ClientOrderId,
7499        venue_order_id: Option<VenueOrderId>,
7500        reason: Ustr,
7501    ) {
7502        let ts_now = self.clock.borrow().timestamp_ns();
7503        let event = OrderEventAny::CancelRejected(OrderCancelRejected::new(
7504            trader_id,
7505            strategy_id,
7506            instrument_id,
7507            client_order_id,
7508            reason,
7509            UUID4::new(),
7510            ts_now,
7511            ts_now,
7512            false,
7513            venue_order_id,
7514            Some(account_id),
7515        ));
7516        self.dispatch_order_event(event);
7517    }
7518
7519    fn generate_order_updated(
7520        &self,
7521        order: &OrderAny,
7522        quantity: Quantity,
7523        price: Option<Price>,
7524        trigger_price: Option<Price>,
7525        protection_price: Option<Price>,
7526    ) {
7527        let ts_now = self.clock.borrow().timestamp_ns();
7528        let event = OrderUpdated::new(
7529            order.trader_id(),
7530            order.strategy_id(),
7531            order.instrument_id(),
7532            order.client_order_id(),
7533            quantity,
7534            UUID4::new(),
7535            ts_now,
7536            ts_now,
7537            false,
7538            order.venue_order_id(),
7539            order.account_id(),
7540            price,
7541            trigger_price,
7542            protection_price,
7543            order.is_quote_quantity(),
7544        );
7545
7546        self.pending_updates
7547            .borrow_mut()
7548            .entry(order.client_order_id())
7549            .or_default()
7550            .push(event);
7551        self.dispatch_order_event(OrderEventAny::Updated(event));
7552    }
7553
7554    fn generate_order_canceled(&self, order: &OrderAny, venue_order_id: VenueOrderId) {
7555        let ts_now = self.clock.borrow().timestamp_ns();
7556        let event = OrderEventAny::Canceled(OrderCanceled::new(
7557            order.trader_id(),
7558            order.strategy_id(),
7559            order.instrument_id(),
7560            order.client_order_id(),
7561            UUID4::new(),
7562            ts_now,
7563            ts_now,
7564            false,
7565            Some(venue_order_id),
7566            order.account_id(),
7567            None,
7568        ));
7569        self.dispatch_order_event(event);
7570    }
7571
7572    fn create_order_triggered(&self, order: &OrderAny) -> OrderEventAny {
7573        let ts_now = self.clock.borrow().timestamp_ns();
7574        OrderEventAny::Triggered(OrderTriggered::new(
7575            order.trader_id(),
7576            order.strategy_id(),
7577            order.instrument_id(),
7578            order.client_order_id(),
7579            UUID4::new(),
7580            ts_now,
7581            ts_now,
7582            false,
7583            order.venue_order_id(),
7584            order.account_id(),
7585        ))
7586    }
7587
7588    fn generate_order_expired(&self, order: &OrderAny) {
7589        let ts_now = self.clock.borrow().timestamp_ns();
7590        let event = OrderEventAny::Expired(OrderExpired::new(
7591            order.trader_id(),
7592            order.strategy_id(),
7593            order.instrument_id(),
7594            order.client_order_id(),
7595            UUID4::new(),
7596            ts_now,
7597            ts_now,
7598            false,
7599            order.venue_order_id(),
7600            order.account_id(),
7601        ));
7602        self.dispatch_order_event(event);
7603    }
7604
7605    #[expect(clippy::too_many_arguments)]
7606    fn generate_order_filled(
7607        &mut self,
7608        order: &OrderAny,
7609        venue_order_id: VenueOrderId,
7610        venue_position_id: Option<PositionId>,
7611        last_qty: Quantity,
7612        last_px: Price,
7613        quote_currency: Currency,
7614        commission: Money,
7615        liquidity_side: LiquiditySide,
7616    ) {
7617        debug_assert!(
7618            last_qty <= order.quantity(),
7619            "Fill quantity {last_qty} exceeds order quantity {order_qty} for {client_order_id}",
7620            order_qty = order.quantity(),
7621            client_order_id = order.client_order_id()
7622        );
7623
7624        let ts_now = self.clock.borrow().timestamp_ns();
7625        let account_id = order
7626            .account_id()
7627            .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
7628        let fill = OrderFilled::new(
7629            order.trader_id(),
7630            order.strategy_id(),
7631            order.instrument_id(),
7632            order.client_order_id(),
7633            venue_order_id,
7634            account_id,
7635            self.ids_generator.generate_trade_id(ts_now),
7636            order.order_side(),
7637            order.order_type(),
7638            last_qty,
7639            last_px,
7640            quote_currency,
7641            liquidity_side,
7642            UUID4::new(),
7643            ts_now,
7644            ts_now,
7645            false,
7646            venue_position_id,
7647            Some(commission),
7648            None,
7649        );
7650
7651        self.record_pending_fill(&fill);
7652        self.dispatch_order_event(OrderEventAny::Filled(fill));
7653    }
7654
7655    fn record_pending_fill(&mut self, fill: &OrderFilled) {
7656        if (!self.config.use_reduce_only && !self.restricts_short_selling())
7657            || self.instrument.is_spread()
7658        {
7659            return;
7660        }
7661        self.purge_applied_fills();
7662        let cache = self.cache.borrow();
7663        let position_id = cache
7664            .position_id(&fill.client_order_id)
7665            .copied()
7666            .or(fill.position_id)
7667            .or_else(|| {
7668                (self.oms_type == OmsType::Netting).then(|| {
7669                    PositionId::new(format!("{}-{}", fill.instrument_id, fill.strategy_id))
7670                })
7671            });
7672        let opening_trade_id = position_id.and_then(|id| {
7673            cache
7674                .position(&id)
7675                .and_then(|position| position.events.first().map(|event| event.trade_id))
7676        });
7677        let mut quantity_change = if fill.order_side == OrderSide::Buy {
7678            fill.last_qty.as_decimal()
7679        } else {
7680            -fill.last_qty.as_decimal()
7681        };
7682
7683        if matches!(self.instrument, InstrumentAny::CurrencyPair(_))
7684            && let Some(commission) = fill.commission
7685            && Some(commission.currency) == self.instrument.base_currency()
7686        {
7687            quantity_change -= commission.as_decimal();
7688        }
7689        self.pending_fills.insert(
7690            fill.trade_id,
7691            PendingFill {
7692                client_order_id: fill.client_order_id,
7693                position_id,
7694                opening_trade_id,
7695                quantity_change,
7696            },
7697        );
7698    }
7699}
7700
7701#[derive(Debug)]
7702struct PendingFill {
7703    client_order_id: ClientOrderId,
7704    position_id: Option<PositionId>,
7705    opening_trade_id: Option<TradeId>,
7706    quantity_change: Decimal,
7707}
7708
7709/// Liquidity consumption changed by a FOK fill attempt, recorded so it can be reverted.
7710#[derive(Debug)]
7711struct ConsumptionJournal {
7712    order_side: OrderSide,
7713    trade_consumption: QuantityRaw,
7714    /// Level price and its prior `(original_size, consumed)`, or `None` if absent.
7715    levels: Vec<(PriceRaw, Option<(QuantityRaw, QuantityRaw)>)>,
7716}
7717
7718#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7719enum ModifyOutcome {
7720    Applied,
7721    Rejected,
7722}
7723
7724#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7725enum OrderMatchMode {
7726    All,
7727    LastPriceStopTriggers,
7728}
7729
7730#[derive(Debug)]
7731enum PostMatchOrderAction {
7732    RemoveClosed,
7733    Expire(OrderAny),
7734    UpdateTrailing(OrderAny),
7735    NoMaintenance,
7736}
7737
7738fn order_precision_valid(actual: u8, expected: u8) -> bool {
7739    actual <= expected && raw_scales_match(actual, expected)
7740}
7741
7742fn post_match_order_action<F>(
7743    order: &OrderAny,
7744    support_gtd_orders: bool,
7745    timestamp_ns: UnixNanos,
7746    clone_order: F,
7747) -> PostMatchOrderAction
7748where
7749    F: FnOnce(&OrderAny) -> OrderAny,
7750{
7751    if order.is_closed() {
7752        PostMatchOrderAction::RemoveClosed
7753    } else if support_gtd_orders
7754        && order
7755            .expire_time()
7756            .is_some_and(|expire_ns| timestamp_ns >= expire_ns)
7757    {
7758        PostMatchOrderAction::Expire(clone_order(order))
7759    } else if matches!(
7760        order.order_type(),
7761        OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
7762    ) {
7763        PostMatchOrderAction::UpdateTrailing(clone_order(order))
7764    } else {
7765        PostMatchOrderAction::NoMaintenance
7766    }
7767}
7768
7769/// Writes `filled_qty` directly onto an order clone's core state.
7770///
7771/// Used to present fee models with the current pre-fill quantity when the
7772/// order passed to the fill path is a stale clone (see `fill_order`).
7773fn write_filled_qty(order: &mut OrderAny, filled_qty: Quantity) {
7774    match order {
7775        OrderAny::Limit(o) => o.filled_qty = filled_qty,
7776        OrderAny::LimitIfTouched(o) => o.filled_qty = filled_qty,
7777        OrderAny::Market(o) => o.filled_qty = filled_qty,
7778        OrderAny::MarketIfTouched(o) => o.filled_qty = filled_qty,
7779        OrderAny::MarketToLimit(o) => o.filled_qty = filled_qty,
7780        OrderAny::StopLimit(o) => o.filled_qty = filled_qty,
7781        OrderAny::StopMarket(o) => o.filled_qty = filled_qty,
7782        OrderAny::TrailingStopLimit(o) => o.filled_qty = filled_qty,
7783        OrderAny::TrailingStopMarket(o) => o.filled_qty = filled_qty,
7784    }
7785}
7786
7787#[derive(Debug, Clone, Copy)]
7788struct BarTickSizes {
7789    open: Quantity,
7790    high: Quantity,
7791    low: Quantity,
7792    close: Quantity,
7793}
7794
7795impl BarTickSizes {
7796    fn from_volume(volume: Quantity, size_increment: Quantity) -> Self {
7797        let precision_diff = FIXED_PRECISION.saturating_sub(volume.precision);
7798        let scale = QuantityRaw::pow(10, u32::from(precision_diff));
7799        let units = volume.raw() / scale;
7800        let increment_units = (size_increment.raw() / scale).max(1);
7801        let rounded_units = (units / increment_units) * increment_units;
7802        let increments = rounded_units / increment_units;
7803        let zero = Quantity::zero(volume.precision);
7804        let size =
7805            |increments| Quantity::from_raw(increments * increment_units * scale, volume.precision);
7806
7807        match increments {
7808            0 => Self {
7809                open: zero,
7810                high: zero,
7811                low: zero,
7812                close: zero,
7813            },
7814
7815            // One increment cannot cover both high and low without exceeding the bar volume.
7816            1 => Self {
7817                open: zero,
7818                high: zero,
7819                low: zero,
7820                close: size(1),
7821            },
7822            2 => Self {
7823                open: zero,
7824                high: size(1),
7825                low: size(1),
7826                close: zero,
7827            },
7828            3 => {
7829                let path_size = size(1);
7830
7831                Self {
7832                    open: path_size,
7833                    high: path_size,
7834                    low: path_size,
7835                    close: zero,
7836                }
7837            }
7838            _ => {
7839                let path_increments = increments / 4;
7840                let close_increments = increments - (path_increments * 3);
7841                let path_size = size(path_increments);
7842
7843                Self {
7844                    open: path_size,
7845                    high: path_size,
7846                    low: path_size,
7847                    close: size(close_increments),
7848                }
7849            }
7850        }
7851    }
7852}
7853
7854#[cfg(test)]
7855mod tests {
7856    use std::{
7857        cell::{Cell, RefCell},
7858        collections::{HashMap, HashSet},
7859        rc::Rc,
7860    };
7861
7862    use nautilus_common::{
7863        cache::Cache,
7864        clock::VirtualClock,
7865        messages::execution::{CancelAllOrders, ModifyOrder},
7866    };
7867    use nautilus_core::{UUID4, UnixNanos, correctness::CorrectnessError};
7868    #[cfg(feature = "high-precision")]
7869    use nautilus_model::orderbook::BookLevel;
7870    use nautilus_model::{
7871        data::{
7872            Bar, BarType, DEPTH10_LEN, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick,
7873            TradeTick,
7874            option_chain::OptionGreeks,
7875            order::{BookOrder, OrderId},
7876        },
7877        enums::{
7878            AccountType, AggressorSide, BookAction, BookType, ContingencyType, LiquiditySide,
7879            OmsType, OrderSide, OrderStatus, OrderType, PositionSide, RecordFlag, TimeInForce,
7880            TrailingOffsetType, TriggerType,
7881        },
7882        events::{OrderEventAny, OrderReleased},
7883        identifiers::{AccountId, ClientOrderId, StrategyId, TradeId, TraderId, VenueOrderId},
7884        instruments::{
7885            Instrument, InstrumentAny,
7886            stubs::{crypto_option_btc_deribit, crypto_perpetual_ethusdt, futures_contract_es},
7887        },
7888        orderbook::OrderBook,
7889        orders::{Order, OrderAny, OrderTestBuilder, stubs::TestOrderEventStubs},
7890        types::{Money, Price, Quantity, fixed::FIXED_PRECISION, quantity::QuantityRaw},
7891    };
7892    use proptest::prelude::*;
7893    use rstest::rstest;
7894    use rust_decimal::Decimal;
7895    use rust_decimal_macros::dec;
7896
7897    use super::{
7898        BarTickSizes, OrderFilled, OrderMatchingEngine, Position, PositionId, PostMatchOrderAction,
7899        order_precision_valid, post_match_order_action,
7900    };
7901    use crate::{
7902        matching_engine::config::OrderMatchingEngineConfig,
7903        models::{
7904            fee::{FeeModel, FeeModelAny, FeeModelHandle, MakerTakerFeeModel},
7905            fill::{FillModel, FillModelHandle},
7906        },
7907    };
7908
7909    // Caches the order before processing, as the execution engine does
7910    fn process_cached_order(
7911        engine: &mut OrderMatchingEngine,
7912        order: &mut OrderAny,
7913        account_id: AccountId,
7914    ) {
7915        if !engine.cache.borrow().order_exists(&order.client_order_id()) {
7916            engine
7917                .cache
7918                .borrow_mut()
7919                .add_order(order.clone(), None, None, false)
7920                .unwrap();
7921        }
7922
7923        engine.process_order(order, account_id);
7924    }
7925
7926    fn assert_valid_bar_tick_sizes(volume: Quantity, size_increment: Quantity) {
7927        let sizes = BarTickSizes::from_volume(volume, size_increment);
7928        let total_raw = sizes.open.raw() + sizes.high.raw() + sizes.low.raw() + sizes.close.raw();
7929        assert!(total_raw <= volume.raw());
7930
7931        for quantity in [sizes.open, sizes.high, sizes.low, sizes.close] {
7932            assert_eq!(quantity.precision, volume.precision);
7933            assert!(
7934                OrderMatchingEngine::quantity_matches_precision(quantity, volume.precision),
7935                "bar tick quantity {quantity} not aligned to precision {}",
7936                volume.precision,
7937            );
7938            assert!(
7939                size_increment.is_zero() || quantity.raw().is_multiple_of(size_increment.raw()),
7940                "bar tick quantity {quantity} not aligned to increment {size_increment}",
7941            );
7942        }
7943
7944        if size_increment.is_positive() {
7945            assert!(
7946                volume.raw() - total_raw < size_increment.raw(),
7947                "bar tick split left {} raw units from volume {volume} and increment {size_increment}",
7948                volume.raw() - total_raw,
7949            );
7950        }
7951    }
7952
7953    #[rstest]
7954    #[case::lower(0, FIXED_PRECISION, true)]
7955    #[case::equal(FIXED_PRECISION, FIXED_PRECISION, true)]
7956    #[case::excess(3, 2, false)]
7957    #[case::native_equal(18, 18, true)]
7958    #[case::native_lower(17, 18, false)]
7959    #[case::native_shared_lower(FIXED_PRECISION, 18, false)]
7960    fn test_order_precision_valid(
7961        #[case] actual: u8,
7962        #[case] expected: u8,
7963        #[case] accepted: bool,
7964    ) {
7965        assert_eq!(order_precision_valid(actual, expected), accepted);
7966    }
7967
7968    #[rstest]
7969    #[case("100.009", "100.011", "100.000", true)]
7970    #[case("100.009", "100.020", "100.008", false)]
7971    #[case("100.010", "100.020", "100.000", false)]
7972    fn test_bar_high_first_preserves_stored_distances(
7973        #[case] open: &str,
7974        #[case] high: &str,
7975        #[case] low: &str,
7976        #[case] expected: bool,
7977    ) {
7978        let (mut engine, _, _) = collision_engine();
7979        engine.config.bar_adaptive_high_low_ordering = true;
7980        let mut prices = [Price::from(open), Price::from(high), Price::from(low)];
7981        for price in &mut prices {
7982            price.precision = 2;
7983        }
7984
7985        let bar = Bar::new(
7986            BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
7987            prices[0],
7988            prices[1],
7989            prices[2],
7990            prices[0],
7991            Quantity::from("1.000"),
7992            1.into(),
7993            1.into(),
7994        );
7995        assert_eq!(engine.bar_high_first(&bar), expected);
7996    }
7997
7998    #[cfg(feature = "high-precision")]
7999    #[rstest]
8000    fn test_consume_trade_level_preserves_native_raw_units() {
8001        let precision = if Quantity::from_raw_checked(0, 18).is_ok() {
8002            18
8003        } else {
8004            FIXED_PRECISION
8005        };
8006
8007        let size = Quantity::from_raw(2_000_000_000_000_000_000, precision);
8008        let level = BookLevel::from_order(BookOrder::new(
8009            OrderSide::Sell,
8010            Price::from("1.00"),
8011            size,
8012            1,
8013        ));
8014        let mut consumption = indexmap::IndexMap::default();
8015        let mut remaining = size.raw();
8016        OrderMatchingEngine::consume_trade_level(&mut consumption, &mut remaining, &level);
8017        assert_eq!(remaining, 0);
8018        assert_eq!(
8019            consumption[&level.price.value.raw()],
8020            (size.raw(), size.raw())
8021        );
8022    }
8023
8024    #[rstest]
8025    fn test_post_match_order_action_does_not_clone_no_maintenance_order() {
8026        let order = post_match_limit_order();
8027        let clone_count = Cell::new(0);
8028
8029        let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
8030            clone_count.set(clone_count.get() + 1);
8031            order.clone()
8032        });
8033
8034        assert!(matches!(action, PostMatchOrderAction::NoMaintenance));
8035        assert_eq!(clone_count.get(), 0);
8036    }
8037
8038    #[rstest]
8039    #[case::spread(
8040        InstrumentAny::FuturesSpread(nautilus_model::instruments::stubs::futures_spread_es()),
8041        0
8042    )]
8043    #[case::outright(InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt()), 1)]
8044    fn test_pending_fills_exclude_instruments_without_positions(
8045        #[case] instrument: InstrumentAny,
8046        #[case] expected_pending: usize,
8047    ) {
8048        let cache = Rc::new(RefCell::new(Cache::default()));
8049        let mut engine = OrderMatchingEngine::new(
8050            instrument.clone(),
8051            1,
8052            FillModelHandle::default(),
8053            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
8054            BookType::L1_MBP,
8055            OmsType::Netting,
8056            AccountType::Margin,
8057            Rc::new(RefCell::new(VirtualClock::new())),
8058            cache.clone(),
8059            Default::default(),
8060        );
8061        let (order, fill) = pending_position_fill(
8062            &instrument,
8063            PositionId::from("POSITION-001"),
8064            "OPEN",
8065            OrderSide::Buy,
8066            "1",
8067        );
8068        cache
8069            .borrow_mut()
8070            .add_order(order, None, None, false)
8071            .unwrap();
8072        engine.record_pending_fill(&fill);
8073        cache
8074            .borrow_mut()
8075            .update_order(&OrderEventAny::Filled(fill))
8076            .unwrap();
8077        engine.purge_applied_fills();
8078        assert_eq!(engine.pending_fills.len(), expected_pending);
8079    }
8080
8081    #[rstest]
8082    fn test_pending_fills_wait_for_position_acknowledgement(
8083        #[values(OmsType::Netting, OmsType::Hedging)] oms_type: OmsType,
8084        #[values(OrderSide::Buy, OrderSide::Sell)] closing_side: OrderSide,
8085    ) {
8086        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8087        let cache = Rc::new(RefCell::new(Cache::default()));
8088        let mut engine = OrderMatchingEngine::new(
8089            instrument.clone(),
8090            1,
8091            FillModelHandle::default(),
8092            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
8093            BookType::L1_MBP,
8094            oms_type,
8095            AccountType::Margin,
8096            Rc::new(RefCell::new(VirtualClock::new())),
8097            cache.clone(),
8098            Default::default(),
8099        );
8100        let position_id = PositionId::from("POSITION-001");
8101        let opening_side = if closing_side == OrderSide::Buy {
8102            OrderSide::Sell
8103        } else {
8104            OrderSide::Buy
8105        };
8106        let (opening, opening_fill) =
8107            pending_position_fill(&instrument, position_id, "OPEN", opening_side, "0.500");
8108        let (closing, first_fill) = pending_position_fill(
8109            &instrument,
8110            position_id,
8111            "CLOSE-FIRST",
8112            closing_side,
8113            "0.400",
8114        );
8115        let (_, second_fill) = pending_position_fill(
8116            &instrument,
8117            position_id,
8118            "CLOSE-SECOND",
8119            closing_side,
8120            "0.100",
8121        );
8122        let (unrelated, unrelated_fill) = pending_position_fill(
8123            &instrument,
8124            PositionId::from("POSITION-002"),
8125            "UNRELATED",
8126            closing_side,
8127            "0.200",
8128        );
8129        let position = Position::new(&instrument, opening_fill);
8130        cache
8131            .borrow_mut()
8132            .add_order(opening, None, None, false)
8133            .unwrap();
8134        cache
8135            .borrow_mut()
8136            .add_position(&position, oms_type)
8137            .unwrap();
8138        cache
8139            .borrow_mut()
8140            .add_order(closing.clone(), Some(position_id), None, false)
8141            .unwrap();
8142        engine.record_pending_fill(&first_fill);
8143        cache
8144            .borrow_mut()
8145            .add_order(unrelated, None, None, false)
8146            .unwrap();
8147        engine.record_pending_fill(&unrelated_fill);
8148        assert_eq!(
8149            engine
8150                .position_quantity_remaining(&closing, Some(&position))
8151                .unwrap(),
8152            Quantity::from("0.100")
8153        );
8154
8155        cache
8156            .borrow_mut()
8157            .update_order(&OrderEventAny::Filled(first_fill.clone()))
8158            .unwrap();
8159        assert_eq!(
8160            engine
8161                .position_quantity_remaining(&closing, Some(&position))
8162                .unwrap(),
8163            Quantity::from("0.100")
8164        );
8165        let position = cache
8166            .borrow_mut()
8167            .update_position_from_fill(position_id, &first_fill)
8168            .unwrap();
8169        assert_eq!(
8170            engine
8171                .position_quantity_remaining(&closing, Some(&position))
8172                .unwrap(),
8173            Quantity::from("0.100")
8174        );
8175        assert!(!engine.pending_fills.contains_key(&first_fill.trade_id));
8176
8177        engine.record_pending_fill(&second_fill);
8178        assert_eq!(
8179            engine
8180                .position_quantity_remaining(&closing, Some(&position))
8181                .unwrap(),
8182            Quantity::from("0.000")
8183        );
8184        engine.reset();
8185        assert!(engine.pending_fills.is_empty());
8186        assert_eq!(
8187            engine
8188                .position_quantity_remaining(&closing, Some(&position))
8189                .unwrap(),
8190            Quantity::from("0.100")
8191        );
8192    }
8193
8194    #[rstest]
8195    #[case::base_fee("0.010 ETH", "0.89000")]
8196    #[case::quote_fee("0.010 USDT", "0.90000")]
8197    fn test_pending_spot_fills_include_base_currency_commission(
8198        #[case] commission: &str,
8199        #[case] expected: &str,
8200    ) {
8201        let instrument = InstrumentAny::CurrencyPair(
8202            nautilus_model::instruments::stubs::currency_pair_ethusdt(),
8203        );
8204        let cache = Rc::new(RefCell::new(Cache::default()));
8205        let mut engine = OrderMatchingEngine::new(
8206            instrument.clone(),
8207            1,
8208            FillModelHandle::default(),
8209            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
8210            BookType::L1_MBP,
8211            OmsType::Netting,
8212            AccountType::Cash,
8213            Rc::new(RefCell::new(VirtualClock::new())),
8214            cache.clone(),
8215            Default::default(),
8216        );
8217        let position_id = PositionId::from("POSITION-001");
8218        let (opening, opening_fill) =
8219            pending_position_fill(&instrument, position_id, "OPEN", OrderSide::Buy, "0.50000");
8220        let (_, mut increase_fill) = pending_position_fill(
8221            &instrument,
8222            position_id,
8223            "INCREASE",
8224            OrderSide::Buy,
8225            "0.40000",
8226        );
8227        let (closing, _) = pending_position_fill(
8228            &instrument,
8229            position_id,
8230            "CLOSE",
8231            OrderSide::Sell,
8232            "1.00000",
8233        );
8234        increase_fill.commission = Some(Money::from(commission));
8235        let position = Position::new(&instrument, opening_fill);
8236        cache
8237            .borrow_mut()
8238            .add_order(opening, None, None, false)
8239            .unwrap();
8240        cache
8241            .borrow_mut()
8242            .add_position(&position, OmsType::Netting)
8243            .unwrap();
8244        engine.record_pending_fill(&increase_fill);
8245        assert_eq!(
8246            engine
8247                .position_quantity_remaining(&closing, Some(&position))
8248                .unwrap(),
8249            Quantity::from(expected)
8250        );
8251        let position = cache
8252            .borrow_mut()
8253            .update_position_from_fill(position_id, &increase_fill)
8254            .unwrap();
8255        assert_eq!(
8256            engine
8257                .position_quantity_remaining(&closing, Some(&position))
8258                .unwrap(),
8259            Quantity::from(expected)
8260        );
8261        assert!(engine.pending_fills.is_empty());
8262    }
8263
8264    #[rstest]
8265    fn test_pending_fills_survive_position_flip_and_archive_acknowledgement() {
8266        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8267        let cache = Rc::new(RefCell::new(Cache::default()));
8268        let mut engine = OrderMatchingEngine::new(
8269            instrument.clone(),
8270            1,
8271            FillModelHandle::default(),
8272            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
8273            BookType::L1_MBP,
8274            OmsType::Netting,
8275            AccountType::Margin,
8276            Rc::new(RefCell::new(VirtualClock::new())),
8277            cache.clone(),
8278            Default::default(),
8279        );
8280        let position_id = PositionId::from("POSITION-001");
8281        let (opening, opening_fill) =
8282            pending_position_fill(&instrument, position_id, "OPEN", OrderSide::Buy, "10.000");
8283        let (flipping, flip_fill) =
8284            pending_position_fill(&instrument, position_id, "FLIP", OrderSide::Sell, "15.000");
8285        let (closing, close_fill) =
8286            pending_position_fill(&instrument, position_id, "CLOSE", OrderSide::Buy, "4.000");
8287
8288        for order in [opening, flipping, closing.clone()] {
8289            cache
8290                .borrow_mut()
8291                .add_order(order, None, None, false)
8292                .unwrap();
8293        }
8294        let mut position = Position::new(&instrument, opening_fill);
8295        cache
8296            .borrow_mut()
8297            .add_position(&position, OmsType::Netting)
8298            .unwrap();
8299        engine.record_pending_fill(&flip_fill);
8300        engine.record_pending_fill(&close_fill);
8301        assert_eq!(
8302            engine
8303                .position_quantity_remaining(&closing, Some(&position))
8304                .unwrap(),
8305            Quantity::from("1.000")
8306        );
8307
8308        let (closing_flip, opening_flip) = flip_fill
8309            .split_for_position_flip(Quantity::from("10.000"), Some(position_id), UUID4::new())
8310            .unwrap();
8311        position.apply(&closing_flip);
8312        cache.borrow_mut().snapshot_position(&position).unwrap();
8313        let position = Position::new(&instrument, opening_flip);
8314        cache
8315            .borrow_mut()
8316            .add_position(&position, OmsType::Netting)
8317            .unwrap();
8318        assert_eq!(
8319            engine
8320                .position_quantity_remaining(&closing, Some(&position))
8321                .unwrap(),
8322            Quantity::from("1.000")
8323        );
8324        assert!(!engine.pending_fills.contains_key(&flip_fill.trade_id));
8325        assert!(engine.pending_fills.contains_key(&close_fill.trade_id));
8326
8327        let position = cache
8328            .borrow_mut()
8329            .update_position_from_fill(position_id, &close_fill)
8330            .unwrap();
8331        assert_eq!(
8332            engine
8333                .position_quantity_remaining(&closing, Some(&position))
8334                .unwrap(),
8335            Quantity::from("1.000")
8336        );
8337        assert!(engine.pending_fills.is_empty());
8338
8339        let (_, flatten_fill) =
8340            pending_position_fill(&instrument, position_id, "FLATTEN", OrderSide::Buy, "1.000");
8341        let (_, reopen_fill) =
8342            pending_position_fill(&instrument, position_id, "REOPEN", OrderSide::Sell, "3.000");
8343        engine.record_pending_fill(&flatten_fill);
8344        engine.record_pending_fill(&reopen_fill);
8345        cache
8346            .borrow_mut()
8347            .update_position_from_fill(position_id, &flatten_fill)
8348            .unwrap();
8349        let closed = cache.borrow().position(&position_id).unwrap().clone();
8350        cache.borrow_mut().snapshot_position(&closed).unwrap();
8351        let position = Position::new(&instrument, reopen_fill);
8352        cache
8353            .borrow_mut()
8354            .add_position_without_order(&position, OmsType::Netting)
8355            .unwrap();
8356        assert_eq!(
8357            engine
8358                .position_quantity_remaining(&closing, Some(&position))
8359                .unwrap(),
8360            Quantity::from("3.000")
8361        );
8362        assert!(engine.pending_fills.is_empty());
8363    }
8364
8365    #[rstest]
8366    fn test_position_fills_sync_reduce_only_orders(
8367        #[values(OrderSide::Buy, OrderSide::Sell)] opening_side: OrderSide,
8368        #[values(OmsType::Netting, OmsType::Hedging)] oms_type: OmsType,
8369        #[values(false, true)] deferred: bool,
8370        #[values(OrderType::Limit, OrderType::StopMarket, OrderType::StopLimit)]
8371        resting_type: OrderType,
8372        #[values(false, true)] support_contingent_orders: bool,
8373        #[values(false, true)] indexed: bool,
8374    ) {
8375        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8376        let cache = Rc::new(RefCell::new(Cache::default()));
8377        let mut engine = OrderMatchingEngine::new(
8378            instrument.clone(),
8379            1,
8380            FillModelHandle::default(),
8381            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
8382            BookType::L2_MBP,
8383            oms_type,
8384            AccountType::Margin,
8385            Rc::new(RefCell::new(VirtualClock::new())),
8386            cache.clone(),
8387            OrderMatchingEngineConfig {
8388                support_contingent_orders,
8389                ..Default::default()
8390            },
8391        );
8392        let position_id = PositionId::from("SYNC-POSITION");
8393        let closing_side = if opening_side == OrderSide::Buy {
8394            OrderSide::Sell
8395        } else {
8396            OrderSide::Buy
8397        };
8398        let (opening, mut opening_fill) = pending_position_fill(
8399            &instrument,
8400            position_id,
8401            "SYNC-OPEN",
8402            opening_side,
8403            if support_contingent_orders {
8404                "3.000"
8405            } else {
8406                "10.000"
8407            },
8408        );
8409        let position_id = if oms_type == OmsType::Netting {
8410            PositionId::new(format!("{}-{}", instrument.id(), opening.strategy_id()))
8411        } else {
8412            position_id
8413        };
8414        opening_fill.position_id = Some(position_id);
8415        let mut position = Position::new(&instrument, opening_fill.clone());
8416        cache
8417            .borrow_mut()
8418            .add_order(opening, Some(position_id), None, false)
8419            .unwrap();
8420        cache
8421            .borrow_mut()
8422            .update_order(&OrderEventAny::Filled(opening_fill))
8423            .unwrap();
8424
8425        for (id, qty) in [("SYNC-PARENT-A", "2.000"), ("SYNC-PARENT-B", "5.000")] {
8426            let (parent, mut fill) =
8427                pending_position_fill(&instrument, position_id, id, opening_side, qty);
8428            fill.venue_order_id = VenueOrderId::from(id);
8429            if support_contingent_orders {
8430                position.apply(&fill);
8431            }
8432            cache
8433                .borrow_mut()
8434                .add_order(parent, Some(position_id), None, false)
8435                .unwrap();
8436
8437            if support_contingent_orders {
8438                cache
8439                    .borrow_mut()
8440                    .update_order(&OrderEventAny::Filled(fill))
8441                    .unwrap();
8442            }
8443        }
8444        cache
8445            .borrow_mut()
8446            .add_position(&position, oms_type)
8447            .unwrap();
8448        engine
8449            .account_ids
8450            .insert(position.trader_id, position.account_id);
8451        let events = Rc::new(RefCell::new(Vec::new()));
8452        let events_handler = events.clone();
8453        let handler_cache = cache.clone();
8454        engine.set_event_handler(Rc::new(move |event| {
8455            if !deferred || matches!(event, OrderEventAny::Accepted(_)) {
8456                handler_cache.borrow_mut().update_order(&event).unwrap();
8457                if let OrderEventAny::Filled(fill) = &event {
8458                    handler_cache
8459                        .borrow_mut()
8460                        .update_position_from_fill(position_id, fill)
8461                        .unwrap();
8462                }
8463            }
8464            events_handler.borrow_mut().push(event);
8465        }));
8466
8467        for (id, parent, reduce_only, assigned_position) in [
8468            ("SYNC-A", Some("SYNC-PARENT-A"), true, position_id),
8469            ("SYNC-B", Some("SYNC-PARENT-B"), true, position_id),
8470            ("SYNC-STANDALONE", None, true, position_id),
8471            ("SYNC-NON-REDUCE", None, false, position_id),
8472            (
8473                "SYNC-UNRELATED",
8474                None,
8475                true,
8476                PositionId::from("OTHER-POSITION"),
8477            ),
8478        ] {
8479            let mut builder = OrderTestBuilder::new(resting_type);
8480            builder
8481                .instrument_id(instrument.id())
8482                .client_order_id(ClientOrderId::from(id))
8483                .side(closing_side)
8484                .quantity(Quantity::from("10.000"))
8485                .reduce_only(reduce_only)
8486                .submit(true);
8487
8488            if resting_type != OrderType::StopMarket {
8489                builder.price(Price::from("2000.00"));
8490            }
8491
8492            if resting_type != OrderType::Limit {
8493                builder.trigger_price(Price::from("3000.00"));
8494            }
8495
8496            if let Some(parent) = parent {
8497                builder.parent_order_id(ClientOrderId::from(parent));
8498            }
8499            let mut order = builder.build();
8500            cache
8501                .borrow_mut()
8502                .add_order(
8503                    order.clone(),
8504                    if !indexed && assigned_position == position_id {
8505                        None
8506                    } else {
8507                        Some(assigned_position)
8508                    },
8509                    None,
8510                    false,
8511                )
8512                .unwrap();
8513            engine.accept_order(&mut order);
8514        }
8515        let (closing, _) = pending_position_fill(
8516            &instrument,
8517            position_id,
8518            "SYNC-CLOSE",
8519            closing_side,
8520            "10.000",
8521        );
8522        cache
8523            .borrow_mut()
8524            .add_order(closing.clone(), Some(position_id), None, false)
8525            .unwrap();
8526        events.borrow_mut().clear();
8527
8528        for (quantity, expected_updates, expected_cancels) in [
8529            (
8530                "4.000",
8531                vec![
8532                    (
8533                        "SYNC-A",
8534                        if support_contingent_orders {
8535                            "2.000"
8536                        } else {
8537                            "6.000"
8538                        },
8539                    ),
8540                    (
8541                        "SYNC-B",
8542                        if support_contingent_orders {
8543                            "5.000"
8544                        } else {
8545                            "6.000"
8546                        },
8547                    ),
8548                    ("SYNC-STANDALONE", "6.000"),
8549                ],
8550                Vec::new(),
8551            ),
8552            (
8553                "2.000",
8554                if support_contingent_orders {
8555                    vec![("SYNC-B", "4.000"), ("SYNC-STANDALONE", "4.000")]
8556                } else {
8557                    vec![
8558                        ("SYNC-A", "4.000"),
8559                        ("SYNC-B", "4.000"),
8560                        ("SYNC-STANDALONE", "4.000"),
8561                    ]
8562                },
8563                Vec::new(),
8564            ),
8565            (
8566                "4.000",
8567                Vec::new(),
8568                vec!["SYNC-A", "SYNC-B", "SYNC-STANDALONE"],
8569            ),
8570        ] {
8571            let start = events.borrow().len();
8572            engine
8573                .apply_fills(
8574                    &closing,
8575                    &[(Price::from("1000.00"), Quantity::from(quantity))],
8576                    LiquiditySide::Taker,
8577                    Some(position_id),
8578                    Some(&position),
8579                    None,
8580                    false,
8581                )
8582                .unwrap();
8583            let events = events.borrow();
8584            let emitted = &events[start..];
8585            assert_eq!(
8586                emitted.len(),
8587                1 + expected_updates.len() + expected_cancels.len()
8588            );
8589            let OrderEventAny::Filled(fill) = &emitted[0] else {
8590                panic!("Expected closing fill first")
8591            };
8592            assert_eq!(fill.client_order_id, closing.client_order_id());
8593            assert_eq!(fill.last_qty, Quantity::from(quantity));
8594            assert_eq!(fill.last_px, Price::from("1000.00"));
8595            let mut updates = Vec::new();
8596            let mut cancels = Vec::new();
8597
8598            for event in &emitted[1..] {
8599                match event {
8600                    OrderEventAny::Updated(update) => {
8601                        assert_eq!(
8602                            update.price,
8603                            (resting_type != OrderType::StopMarket).then(|| Price::from("2000.00"))
8604                        );
8605                        assert_eq!(
8606                            update.trigger_price,
8607                            (resting_type != OrderType::Limit).then(|| Price::from("3000.00"))
8608                        );
8609                        updates.push((update.client_order_id.to_string(), update.quantity));
8610                    }
8611                    OrderEventAny::Canceled(cancel) => {
8612                        cancels.push(cancel.client_order_id.to_string());
8613                    }
8614                    other => panic!("Unexpected event {other:?}"),
8615                }
8616            }
8617            updates.sort_by(|a, b| a.0.cmp(&b.0));
8618            cancels.sort();
8619            assert_eq!(
8620                updates,
8621                expected_updates
8622                    .into_iter()
8623                    .map(|(id, qty)| (id.to_string(), Quantity::from(qty)))
8624                    .collect::<Vec<_>>()
8625            );
8626            assert_eq!(cancels, expected_cancels);
8627        }
8628
8629        if deferred {
8630            for event in events.borrow().iter() {
8631                cache.borrow_mut().update_order(event).unwrap();
8632                if let OrderEventAny::Filled(fill) = event {
8633                    cache
8634                        .borrow_mut()
8635                        .update_position_from_fill(position_id, fill)
8636                        .unwrap();
8637                }
8638            }
8639        }
8640        let cache = cache.borrow();
8641        assert_eq!(
8642            cache.position(&position_id).unwrap().quantity,
8643            Quantity::from("0.000")
8644        );
8645
8646        for (id, quantity) in [
8647            (
8648                "SYNC-A",
8649                if support_contingent_orders {
8650                    "2.000"
8651                } else {
8652                    "4.000"
8653                },
8654            ),
8655            ("SYNC-B", "4.000"),
8656            ("SYNC-STANDALONE", "4.000"),
8657        ] {
8658            let id = ClientOrderId::from(id);
8659            let order = cache.order(&id).unwrap();
8660            assert_eq!(order.status(), OrderStatus::Canceled);
8661            assert_eq!(order.quantity(), Quantity::from(quantity));
8662            assert!(!engine.order_exists(id));
8663        }
8664
8665        for id in ["SYNC-NON-REDUCE", "SYNC-UNRELATED"] {
8666            let id = ClientOrderId::from(id);
8667            let order = cache.order(&id).unwrap();
8668            assert_eq!(order.status(), OrderStatus::Accepted);
8669            assert_eq!(order.quantity(), Quantity::from("10.000"));
8670            assert!(engine.order_exists(id));
8671        }
8672    }
8673
8674    #[rstest]
8675    #[case(None, "7.000", "11.000", OrderStatus::PartiallyFilled)]
8676    #[case(None, "9.000", "9.000", OrderStatus::PartiallyFilled)]
8677    #[case(None, "10.000", "10.000", OrderStatus::Canceled)]
8678    #[case(Some("10.000"), "7.000", "10.000", OrderStatus::PartiallyFilled)]
8679    #[case(Some("9.000"), "7.000", "9.000", OrderStatus::PartiallyFilled)]
8680    #[case(Some("8.000"), "7.000", "8.000", OrderStatus::Canceled)]
8681    fn test_position_sync_accounts_for_prior_fills(
8682        #[case] parent_filled: Option<&str>,
8683        #[case] closing_quantity: &str,
8684        #[case] expected_quantity: &str,
8685        #[case] expected_status: OrderStatus,
8686        #[values(false, true)] deferred: bool,
8687        #[values(false, true)] use_reduce_only: bool,
8688    ) {
8689        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8690        let position_id = PositionId::from("FLOOR-POSITION");
8691        let cache = Rc::new(RefCell::new(Cache::default()));
8692        let mut engine = OrderMatchingEngine::new(
8693            instrument.clone(),
8694            1,
8695            FillModelHandle::default(),
8696            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
8697            BookType::L2_MBP,
8698            OmsType::Hedging,
8699            AccountType::Margin,
8700            Rc::new(RefCell::new(VirtualClock::new())),
8701            cache.clone(),
8702            OrderMatchingEngineConfig {
8703                use_reduce_only,
8704                ..Default::default()
8705            },
8706        );
8707        let opening_quantity = parent_filled.map_or(Quantity::from("18.000"), |quantity| {
8708            Quantity::from("18.000") - Quantity::from(quantity)
8709        });
8710        let (opening, opening_fill) = pending_position_fill(
8711            &instrument,
8712            position_id,
8713            "FLOOR-OPEN",
8714            OrderSide::Buy,
8715            &opening_quantity.to_string(),
8716        );
8717        let mut position = Position::new(&instrument, opening_fill.clone());
8718        engine
8719            .account_ids
8720            .insert(position.trader_id, position.account_id);
8721        cache
8722            .borrow_mut()
8723            .add_order(opening, Some(position_id), None, false)
8724            .unwrap();
8725        cache
8726            .borrow_mut()
8727            .update_order(&OrderEventAny::Filled(opening_fill))
8728            .unwrap();
8729        let parent_id = parent_filled.map(|quantity| {
8730            let (parent, mut fill) = pending_position_fill(
8731                &instrument,
8732                position_id,
8733                "FLOOR-PARENT",
8734                OrderSide::Buy,
8735                quantity,
8736            );
8737            fill.venue_order_id = VenueOrderId::from("FLOOR-PARENT");
8738            position.apply(&fill);
8739            let parent_id = parent.client_order_id();
8740            cache
8741                .borrow_mut()
8742                .add_order(parent, Some(position_id), None, false)
8743                .unwrap();
8744            cache
8745                .borrow_mut()
8746                .update_order(&OrderEventAny::Filled(fill))
8747                .unwrap();
8748            parent_id
8749        });
8750        let mut builder = OrderTestBuilder::new(OrderType::Limit);
8751        if let Some(parent_id) = parent_id {
8752            builder.parent_order_id(parent_id);
8753        }
8754        let mut resting = builder
8755            .instrument_id(instrument.id())
8756            .client_order_id(ClientOrderId::from("FLOOR-RESTING"))
8757            .side(OrderSide::Sell)
8758            .quantity(Quantity::from("10.000"))
8759            .price(Price::from("2000.00"))
8760            .reduce_only(true)
8761            .submit(true)
8762            .build();
8763        cache
8764            .borrow_mut()
8765            .add_order(resting.clone(), Some(position_id), None, false)
8766            .unwrap();
8767        let handler_cache = cache.clone();
8768        engine.set_event_handler(Rc::new(move |event| {
8769            handler_cache.borrow_mut().update_order(&event).unwrap();
8770        }));
8771        engine.accept_order(&mut resting);
8772        let (_, mut prior_fill) = pending_position_fill(
8773            &instrument,
8774            position_id,
8775            "FLOOR-RESTING",
8776            OrderSide::Sell,
8777            "8.000",
8778        );
8779        prior_fill.venue_order_id = resting.venue_order_id().unwrap();
8780        prior_fill.order_type = OrderType::Limit;
8781        position.apply(&prior_fill);
8782        cache
8783            .borrow_mut()
8784            .update_order(&OrderEventAny::Filled(prior_fill))
8785            .unwrap();
8786        cache
8787            .borrow_mut()
8788            .add_position(&position, OmsType::Hedging)
8789            .unwrap();
8790        let (closing, _) = pending_position_fill(
8791            &instrument,
8792            position_id,
8793            "FLOOR-CLOSE",
8794            OrderSide::Sell,
8795            closing_quantity,
8796        );
8797        cache
8798            .borrow_mut()
8799            .add_order(closing.clone(), Some(position_id), None, false)
8800            .unwrap();
8801        let events = Rc::new(RefCell::new(Vec::new()));
8802        let events_handler = events.clone();
8803        let handler_cache = cache.clone();
8804        engine.set_event_handler(Rc::new(move |event| {
8805            if !deferred {
8806                handler_cache.borrow_mut().update_order(&event).unwrap();
8807                if let OrderEventAny::Filled(fill) = &event {
8808                    handler_cache
8809                        .borrow_mut()
8810                        .update_position_from_fill(position_id, fill)
8811                        .unwrap();
8812                }
8813            }
8814            events_handler.borrow_mut().push(event);
8815        }));
8816
8817        engine
8818            .apply_fills(
8819                &closing,
8820                &[(Price::from("1000.00"), Quantity::from(closing_quantity))],
8821                LiquiditySide::Taker,
8822                Some(position_id),
8823                Some(&position),
8824                None,
8825                false,
8826            )
8827            .unwrap();
8828
8829        let expected_quantity = Quantity::from(if use_reduce_only {
8830            expected_quantity
8831        } else {
8832            "10.000"
8833        });
8834        let expected_status = if use_reduce_only {
8835            expected_status
8836        } else {
8837            OrderStatus::PartiallyFilled
8838        };
8839        let updated = expected_quantity != Quantity::from("10.000");
8840        let canceled = expected_status == OrderStatus::Canceled;
8841        let events = events.borrow();
8842        assert_eq!(
8843            events.len(),
8844            1 + usize::from(updated) + usize::from(canceled)
8845        );
8846        assert!(
8847            matches!(&events[0], OrderEventAny::Filled(fill) if fill.last_qty == Quantity::from(closing_quantity))
8848        );
8849
8850        if updated {
8851            let OrderEventAny::Updated(update) = &events[1] else {
8852                panic!("Expected remaining quantity update")
8853            };
8854            assert_eq!(update.client_order_id, resting.client_order_id());
8855            assert_eq!(update.quantity, expected_quantity);
8856            assert_eq!(update.price, Some(Price::from("2000.00")));
8857            assert_eq!(update.trigger_price, None);
8858        }
8859
8860        if canceled {
8861            let OrderEventAny::Canceled(cancel) = events.last().unwrap() else {
8862                panic!("Expected cancellation with no remaining capacity")
8863            };
8864            assert_eq!(cancel.client_order_id, resting.client_order_id());
8865        }
8866
8867        if deferred {
8868            for event in events.iter() {
8869                cache.borrow_mut().update_order(event).unwrap();
8870                if let OrderEventAny::Filled(fill) = event {
8871                    cache
8872                        .borrow_mut()
8873                        .update_position_from_fill(position_id, fill)
8874                        .unwrap();
8875                }
8876            }
8877        }
8878        let cache = cache.borrow();
8879        let resting = cache.order(&resting.client_order_id()).unwrap();
8880        assert_eq!(resting.filled_qty(), Quantity::from("8.000"));
8881        assert_eq!(resting.quantity(), expected_quantity);
8882        assert_eq!(
8883            resting.leaves_qty(),
8884            expected_quantity - Quantity::from("8.000")
8885        );
8886        assert_eq!(resting.status(), expected_status);
8887        assert_eq!(engine.order_exists(resting.client_order_id()), !canceled);
8888        assert_eq!(
8889            cache.position(&position_id).unwrap().quantity,
8890            Quantity::from("10.000") - Quantity::from(closing_quantity)
8891        );
8892    }
8893
8894    #[rstest]
8895    #[case(("0.000", "0.000"), (None, None), "open", (["6.000", "4.000"], [Some("6.000"), Some("4.000")]), false)]
8896    #[case(("2.000", "3.000"), (None, None), "open", (["8.000", "6.000"], [Some("9.000"), Some("7.000")]), false)]
8897    #[case(("2.000", "3.000"), (Some("7.000"), Some("6.000")), "open", (["7.000", "6.000"], [Some("6.000"), None]), false)]
8898    #[case(("2.000", "3.000"), (Some("2.000"), None), "open", (["2.000", "2.000"], [None, None]), true)]
8899    #[case(("2.000", "3.000"), (None, Some("3.000")), "open", (["8.000", "6.000"], [Some("3.000"), None]), true)]
8900    #[case(("0.000", "0.000"), (None, None), "closed", (["6.000", "4.000"], [None, None]), false)]
8901    #[case(("0.000", "0.000"), (None, None), "local", (["6.000", "4.000"], [None, None]), false)]
8902    #[case(("0.000", "0.000"), (None, None), "cancellation_unacknowledged", (["6.000", "4.000"], [None, None]), false)]
8903    fn test_position_sync_resizes_mixed_ouo_sibling(
8904        #[case] filled: (&str, &str),
8905        #[case] parents: (Option<&str>, Option<&str>),
8906        #[case] sibling_state: &str,
8907        #[case] expected: ([&str; 2], [Option<&str>; 2]),
8908        #[case] first_cancel: bool,
8909        #[values(0, 1, 2)] delivery: usize,
8910        #[values(false, true)] support_contingent_orders: bool,
8911    ) {
8912        let (source_filled, sibling_filled) = filled;
8913        let (source_parent, sibling_parent) = parents;
8914        let (source_quantities, sibling_updates) = expected;
8915        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8916        let position_id = PositionId::from("MIXED-POSITION");
8917        let cache = Rc::new(RefCell::new(Cache::default()));
8918        let mut engine = OrderMatchingEngine::new(
8919            instrument.clone(),
8920            1,
8921            FillModelHandle::default(),
8922            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
8923            BookType::L2_MBP,
8924            OmsType::Hedging,
8925            AccountType::Margin,
8926            Rc::new(RefCell::new(VirtualClock::new())),
8927            cache.clone(),
8928            OrderMatchingEngineConfig {
8929                support_contingent_orders,
8930                ..Default::default()
8931            },
8932        );
8933        let opening_quantity = Quantity::from("10.000")
8934            + Quantity::from(source_filled)
8935            + Quantity::from(sibling_filled);
8936        let (opening, opening_fill) = pending_position_fill(
8937            &instrument,
8938            position_id,
8939            "MIXED-OPEN",
8940            OrderSide::Buy,
8941            &opening_quantity.to_string(),
8942        );
8943        let mut position = Position::new(&instrument, opening_fill.clone());
8944        engine
8945            .account_ids
8946            .insert(position.trader_id, position.account_id);
8947        cache
8948            .borrow_mut()
8949            .add_order(opening, Some(position_id), None, false)
8950            .unwrap();
8951        cache
8952            .borrow_mut()
8953            .update_order(&OrderEventAny::Filled(opening_fill))
8954            .unwrap();
8955        let handler_cache = cache.clone();
8956        engine.set_event_handler(Rc::new(move |event| {
8957            handler_cache.borrow_mut().update_order(&event).unwrap();
8958        }));
8959
8960        for (id, sibling, reduce_only, filled, parent_quantity) in [
8961            ("MIXED-A", "MIXED-B", true, source_filled, source_parent),
8962            ("MIXED-B", "MIXED-A", false, sibling_filled, sibling_parent),
8963        ] {
8964            let mut builder = OrderTestBuilder::new(OrderType::Limit);
8965
8966            if let Some(quantity) = parent_quantity {
8967                let parent_id = format!("{id}-PARENT");
8968                let (parent, mut fill) = pending_position_fill(
8969                    &instrument,
8970                    position_id,
8971                    &parent_id,
8972                    OrderSide::Buy,
8973                    quantity,
8974                );
8975                fill.venue_order_id = VenueOrderId::from(parent_id.as_str());
8976                cache
8977                    .borrow_mut()
8978                    .add_order(parent, Some(position_id), None, false)
8979                    .unwrap();
8980                cache
8981                    .borrow_mut()
8982                    .update_order(&OrderEventAny::Filled(fill))
8983                    .unwrap();
8984                builder.parent_order_id(ClientOrderId::from(parent_id));
8985            }
8986            let mut order = builder
8987                .instrument_id(instrument.id())
8988                .client_order_id(ClientOrderId::from(id))
8989                .side(OrderSide::Sell)
8990                .quantity(Quantity::from("10.000"))
8991                .price(Price::from("2000.00"))
8992                .reduce_only(reduce_only)
8993                .contingency_type(ContingencyType::Ouo)
8994                .linked_order_ids(vec![ClientOrderId::from(sibling)])
8995                .submit(sibling_state != "local" || reduce_only)
8996                .build();
8997            cache
8998                .borrow_mut()
8999                .add_order(order.clone(), Some(position_id), None, false)
9000                .unwrap();
9001
9002            if sibling_state != "local" || reduce_only {
9003                engine.accept_order(&mut order);
9004            }
9005
9006            if Quantity::from(filled).non_zero() {
9007                let (_, mut fill) =
9008                    pending_position_fill(&instrument, position_id, id, OrderSide::Sell, filled);
9009                fill.venue_order_id = order.venue_order_id().unwrap();
9010                fill.order_type = OrderType::Limit;
9011                position.apply(&fill);
9012                cache
9013                    .borrow_mut()
9014                    .update_order(&OrderEventAny::Filled(fill))
9015                    .unwrap();
9016            }
9017
9018            if !reduce_only && sibling_state == "closed" {
9019                engine.cancel_order(&order, Some(false));
9020            }
9021        }
9022        cache
9023            .borrow_mut()
9024            .add_position(&position, OmsType::Hedging)
9025            .unwrap();
9026        let events = Rc::new(RefCell::new(Vec::new()));
9027        let events_handler = events.clone();
9028        let handler_cache = cache.clone();
9029        engine.set_event_handler(Rc::new(move |event| {
9030            if delivery == 0 {
9031                handler_cache.borrow_mut().update_order(&event).unwrap();
9032                if let OrderEventAny::Filled(fill) = &event {
9033                    handler_cache
9034                        .borrow_mut()
9035                        .update_position_from_fill(position_id, fill)
9036                        .unwrap();
9037                }
9038            }
9039            events_handler.borrow_mut().push(event);
9040        }));
9041
9042        if sibling_state == "cancellation_unacknowledged" {
9043            let sibling = engine
9044                .order_snapshot(ClientOrderId::from("MIXED-B"))
9045                .unwrap();
9046            engine.cancel_order(&sibling, Some(false));
9047        }
9048        let (closing, _) = pending_position_fill(
9049            &instrument,
9050            position_id,
9051            "MIXED-CLOSE",
9052            OrderSide::Sell,
9053            "10.000",
9054        );
9055        cache
9056            .borrow_mut()
9057            .add_order(closing.clone(), Some(position_id), None, false)
9058            .unwrap();
9059        let mut acknowledged = 0;
9060        let mut source_quantity = Quantity::from("10.000");
9061        let mut sibling_quantity = Quantity::from("10.000");
9062        let mut source_canceled = false;
9063        let mut sibling_canceled =
9064            matches!(sibling_state, "closed" | "cancellation_unacknowledged");
9065
9066        for (step, (quantity, remaining)) in
9067            [("4.000", "6.000"), ("2.000", "4.000"), ("4.000", "0.000")]
9068                .into_iter()
9069                .enumerate()
9070        {
9071            let start = events.borrow().len();
9072            engine
9073                .apply_fills(
9074                    &closing,
9075                    &[(Price::from("1000.00"), Quantity::from(quantity))],
9076                    LiquiditySide::Taker,
9077                    Some(position_id),
9078                    Some(&position),
9079                    None,
9080                    false,
9081                )
9082                .unwrap();
9083            let mut expected = vec![("fill", "MIXED-CLOSE", Quantity::from(quantity))];
9084
9085            if !source_canceled {
9086                if step < 2 {
9087                    let target = if support_contingent_orders {
9088                        Quantity::from(source_quantities[step])
9089                    } else {
9090                        Quantity::from(source_filled) + Quantity::from(remaining)
9091                    };
9092
9093                    if target != source_quantity {
9094                        expected.push(("update", "MIXED-A", target));
9095                        source_quantity = target;
9096
9097                        if support_contingent_orders && source_parent == Some(source_filled) {
9098                            expected.push(("cancel", "MIXED-A", Quantity::zero(3)));
9099                            source_canceled = true;
9100
9101                            if !sibling_canceled && sibling_state != "local" {
9102                                expected.push(("cancel", "MIXED-B", Quantity::zero(3)));
9103                                sibling_canceled = true;
9104                            }
9105                        } else if support_contingent_orders {
9106                            if let Some(target) = sibling_updates[step] {
9107                                sibling_quantity = Quantity::from(target);
9108                                expected.push(("update", "MIXED-B", sibling_quantity));
9109                            }
9110
9111                            if step == 0 && first_cancel {
9112                                expected.push(("cancel", "MIXED-B", Quantity::zero(3)));
9113                                sibling_canceled = true;
9114                            }
9115                        }
9116                    }
9117                } else {
9118                    expected.push(("cancel", "MIXED-A", Quantity::zero(3)));
9119                    source_canceled = true;
9120
9121                    if support_contingent_orders && !sibling_canceled && sibling_state != "local" {
9122                        expected.push(("cancel", "MIXED-B", Quantity::zero(3)));
9123                        sibling_canceled = true;
9124                    }
9125                }
9126            }
9127            let recorded = events.borrow();
9128            let actual: Vec<_> = recorded[start..]
9129                .iter()
9130                .map(|event| match event {
9131                    OrderEventAny::Filled(fill) => {
9132                        assert_eq!(fill.last_px, Price::from("1000.00"));
9133                        ("fill", fill.client_order_id.as_str(), fill.last_qty)
9134                    }
9135                    OrderEventAny::Updated(update) => {
9136                        assert_eq!(update.price, Some(Price::from("2000.00")));
9137                        assert_eq!(update.trigger_price, None);
9138                        ("update", update.client_order_id.as_str(), update.quantity)
9139                    }
9140                    OrderEventAny::Canceled(cancel) => {
9141                        ("cancel", cancel.client_order_id.as_str(), Quantity::zero(3))
9142                    }
9143                    other => panic!("Unexpected event {other:?}"),
9144                })
9145                .collect();
9146            assert_eq!(actual, expected);
9147            drop(recorded);
9148
9149            if delivery == 2 {
9150                let end = events.borrow().len() - 1;
9151                for event in &events.borrow()[acknowledged..end] {
9152                    cache.borrow_mut().update_order(event).unwrap();
9153                    if let OrderEventAny::Filled(fill) = event {
9154                        cache
9155                            .borrow_mut()
9156                            .update_position_from_fill(position_id, fill)
9157                            .unwrap();
9158                    }
9159                }
9160                acknowledged = end;
9161            }
9162            let before = events.borrow().len();
9163            let ids = engine.reduce_only_order_ids(position_id);
9164            engine
9165                .sync_reduce_only_orders(&closing, Some(&position), &ids)
9166                .unwrap();
9167            assert_eq!(events.borrow().len(), before);
9168        }
9169
9170        if delivery != 0 {
9171            for event in &events.borrow()[acknowledged..] {
9172                cache.borrow_mut().update_order(event).unwrap();
9173                if let OrderEventAny::Filled(fill) = event {
9174                    cache
9175                        .borrow_mut()
9176                        .update_position_from_fill(position_id, fill)
9177                        .unwrap();
9178                }
9179            }
9180        }
9181        let cache = cache.borrow();
9182
9183        for (id, filled, quantity, canceled) in [
9184            ("MIXED-A", source_filled, source_quantity, source_canceled),
9185            (
9186                "MIXED-B",
9187                sibling_filled,
9188                sibling_quantity,
9189                sibling_canceled,
9190            ),
9191        ] {
9192            let order = cache.order(&ClientOrderId::from(id)).unwrap();
9193            assert_eq!(order.quantity(), quantity);
9194            assert_eq!(order.filled_qty(), Quantity::from(filled));
9195            assert_eq!(order.leaves_qty(), quantity - Quantity::from(filled));
9196            assert_eq!(
9197                order.status(),
9198                if canceled {
9199                    OrderStatus::Canceled
9200                } else if sibling_state == "local" {
9201                    OrderStatus::Initialized
9202                } else if Quantity::from(filled).is_zero() {
9203                    OrderStatus::Accepted
9204                } else {
9205                    OrderStatus::PartiallyFilled
9206                }
9207            );
9208            assert_eq!(
9209                engine.order_exists(order.client_order_id()),
9210                !canceled && sibling_state != "local"
9211            );
9212        }
9213        assert_eq!(
9214            cache.position(&position_id).unwrap().quantity,
9215            Quantity::from("0.000")
9216        );
9217        assert_eq!(
9218            cache.position(&position_id).unwrap().side,
9219            PositionSide::Flat
9220        );
9221    }
9222
9223    #[rstest]
9224    fn test_position_sync_does_not_resize_order_being_filled(
9225        #[values(false, true)] deferred: bool,
9226    ) {
9227        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9228        let position_id = PositionId::from("REENTRANT-POSITION");
9229        let cache = Rc::new(RefCell::new(Cache::default()));
9230        let mut engine = OrderMatchingEngine::new(
9231            instrument.clone(),
9232            1,
9233            FillModelHandle::default(),
9234            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
9235            BookType::L2_MBP,
9236            OmsType::Hedging,
9237            AccountType::Margin,
9238            Rc::new(RefCell::new(VirtualClock::new())),
9239            cache.clone(),
9240            Default::default(),
9241        );
9242        let (opening, opening_fill) = pending_position_fill(
9243            &instrument,
9244            position_id,
9245            "REENTRANT-OPEN",
9246            OrderSide::Buy,
9247            "6.000",
9248        );
9249        let position = Position::new(&instrument, opening_fill.clone());
9250        engine
9251            .account_ids
9252            .insert(position.trader_id, position.account_id);
9253        cache
9254            .borrow_mut()
9255            .add_order(opening, Some(position_id), None, false)
9256            .unwrap();
9257        cache
9258            .borrow_mut()
9259            .update_order(&OrderEventAny::Filled(opening_fill))
9260            .unwrap();
9261        cache
9262            .borrow_mut()
9263            .add_position(&position, OmsType::Hedging)
9264            .unwrap();
9265
9266        for (id, price, size) in [(1, "1000.00", "4.000"), (2, "999.00", "5.000")] {
9267            engine
9268                .process_order_book_delta(&OrderBookDelta::new(
9269                    instrument.id(),
9270                    BookAction::Add,
9271                    BookOrder::new(OrderSide::Buy, Price::from(price), Quantity::from(size), id),
9272                    0,
9273                    id,
9274                    UnixNanos::from(id),
9275                    UnixNanos::from(id),
9276                ))
9277                .unwrap();
9278        }
9279        let events = Rc::new(RefCell::new(Vec::new()));
9280        let events_handler = events.clone();
9281        let handler_cache = cache.clone();
9282        engine.set_event_handler(Rc::new(move |event| {
9283            if !deferred || matches!(event, OrderEventAny::Accepted(_)) {
9284                handler_cache.borrow_mut().update_order(&event).unwrap();
9285                if let OrderEventAny::Filled(fill) = &event {
9286                    handler_cache
9287                        .borrow_mut()
9288                        .update_position_from_fill(position_id, fill)
9289                        .unwrap();
9290                }
9291            }
9292            events_handler.borrow_mut().push(event);
9293        }));
9294
9295        for (id, sibling, reduce_only) in [
9296            ("REENTRANT-A", "REENTRANT-B", true),
9297            ("REENTRANT-B", "REENTRANT-A", false),
9298        ] {
9299            let mut builder = OrderTestBuilder::new(OrderType::Limit);
9300            builder
9301                .instrument_id(instrument.id())
9302                .client_order_id(ClientOrderId::from(id))
9303                .side(OrderSide::Sell)
9304                .quantity(Quantity::from("10.000"))
9305                .price(Price::from(if reduce_only { "2000.00" } else { "999.00" }))
9306                .reduce_only(reduce_only)
9307                .contingency_type(ContingencyType::Ouo)
9308                .linked_order_ids(vec![ClientOrderId::from(sibling)])
9309                .submit(true);
9310            let mut order = builder.build();
9311            order.set_liquidity_side(LiquiditySide::Taker);
9312            cache
9313                .borrow_mut()
9314                .add_order(order.clone(), Some(position_id), None, false)
9315                .unwrap();
9316            engine.accept_order(&mut order);
9317        }
9318        events.borrow_mut().clear();
9319
9320        engine.iterate(UnixNanos::from(3), AggressorSide::NoAggressor);
9321
9322        if deferred {
9323            for event in events.borrow().iter() {
9324                cache.borrow_mut().update_order(event).unwrap();
9325                if let OrderEventAny::Filled(fill) = event {
9326                    cache
9327                        .borrow_mut()
9328                        .update_position_from_fill(position_id, fill)
9329                        .unwrap();
9330                }
9331            }
9332        }
9333        let cache = cache.borrow();
9334        let filled = cache.order(&ClientOrderId::from("REENTRANT-B")).unwrap();
9335        assert_eq!(filled.quantity(), Quantity::from("10.000"));
9336        assert_eq!(filled.filled_qty(), Quantity::from("9.000"));
9337        assert_eq!(filled.leaves_qty(), Quantity::from("1.000"));
9338        assert_eq!(filled.overfill_qty(), Quantity::from("0.000"));
9339        assert_eq!(filled.status(), OrderStatus::PartiallyFilled);
9340        assert_eq!(
9341            cache.position(&position_id).unwrap().quantity,
9342            Quantity::from("3.000")
9343        );
9344        assert_eq!(
9345            cache.position(&position_id).unwrap().side,
9346            PositionSide::Short
9347        );
9348        let recorded = events.borrow();
9349        let actual: Vec<_> = recorded
9350            .iter()
9351            .map(|event| match event {
9352                OrderEventAny::Filled(fill) => {
9353                    ("fill", fill.client_order_id.as_str(), fill.last_qty)
9354                }
9355                OrderEventAny::Updated(update) => {
9356                    ("update", update.client_order_id.as_str(), update.quantity)
9357                }
9358                OrderEventAny::Canceled(cancel) => {
9359                    ("cancel", cancel.client_order_id.as_str(), Quantity::zero(3))
9360                }
9361                other => panic!("Unexpected event {other:?}"),
9362            })
9363            .collect();
9364        assert_eq!(
9365            actual,
9366            vec![
9367                ("fill", "REENTRANT-B", Quantity::from("4.000")),
9368                ("update", "REENTRANT-A", Quantity::from("6.000")),
9369                ("update", "REENTRANT-A", Quantity::from("2.000")),
9370                ("fill", "REENTRANT-B", Quantity::from("5.000")),
9371                ("update", "REENTRANT-A", Quantity::from("1.000")),
9372                ("cancel", "REENTRANT-A", Quantity::zero(3)),
9373            ]
9374        );
9375    }
9376
9377    #[rstest]
9378    #[case("5.000", "5.000", false)]
9379    #[case("10.000", "0.000", true)]
9380    fn test_position_sync_handles_unacknowledged_sibling_acceptance(
9381        #[case] closing_quantity: &str,
9382        #[case] remaining_quantity: &str,
9383        #[case] canceled: bool,
9384        #[values(false, true)] deferred: bool,
9385    ) {
9386        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9387        let position_id = PositionId::from("REENTRANT-POSITION");
9388        let cache = Rc::new(RefCell::new(Cache::default()));
9389        let mut engine = OrderMatchingEngine::new(
9390            instrument.clone(),
9391            1,
9392            FillModelHandle::default(),
9393            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
9394            BookType::L2_MBP,
9395            OmsType::Hedging,
9396            AccountType::Margin,
9397            Rc::new(RefCell::new(VirtualClock::new())),
9398            cache.clone(),
9399            Default::default(),
9400        );
9401        let (opening, opening_fill) = pending_position_fill(
9402            &instrument,
9403            position_id,
9404            "REENTRANT-OPEN",
9405            OrderSide::Buy,
9406            "10.000",
9407        );
9408        let position = Position::new(&instrument, opening_fill.clone());
9409        engine
9410            .account_ids
9411            .insert(position.trader_id, position.account_id);
9412        cache
9413            .borrow_mut()
9414            .add_order(opening, Some(position_id), None, false)
9415            .unwrap();
9416        cache
9417            .borrow_mut()
9418            .update_order(&OrderEventAny::Filled(opening_fill))
9419            .unwrap();
9420        cache
9421            .borrow_mut()
9422            .add_position(&position, OmsType::Hedging)
9423            .unwrap();
9424        let events = Rc::new(RefCell::new(Vec::new()));
9425        let events_handler = events.clone();
9426        let handler_cache = cache.clone();
9427        let sibling_id = ClientOrderId::from("ACCEPT-B");
9428        engine.set_event_handler(Rc::new(move |event| {
9429            let id = match &event {
9430                OrderEventAny::Accepted(event) => event.client_order_id,
9431                OrderEventAny::Filled(event) => event.client_order_id,
9432                OrderEventAny::Canceled(event) => event.client_order_id,
9433                OrderEventAny::Updated(event) => event.client_order_id,
9434                other => panic!("Unexpected event {other:?}"),
9435            };
9436            let applied =
9437                id != sibling_id && (!deferred || matches!(event, OrderEventAny::Accepted(_)));
9438            if applied {
9439                handler_cache.borrow_mut().update_order(&event).unwrap();
9440                if let OrderEventAny::Filled(fill) = &event {
9441                    handler_cache
9442                        .borrow_mut()
9443                        .update_position_from_fill(position_id, fill)
9444                        .unwrap();
9445                }
9446            }
9447            events_handler.borrow_mut().push((event, applied));
9448        }));
9449
9450        for (id, sibling, reduce_only) in [
9451            ("ACCEPT-A", "ACCEPT-B", true),
9452            ("ACCEPT-B", "ACCEPT-A", false),
9453        ] {
9454            let mut order = OrderTestBuilder::new(OrderType::Limit)
9455                .instrument_id(instrument.id())
9456                .client_order_id(ClientOrderId::from(id))
9457                .side(OrderSide::Sell)
9458                .quantity(Quantity::from("10.000"))
9459                .price(Price::from("2000.00"))
9460                .reduce_only(reduce_only)
9461                .contingency_type(ContingencyType::Ouo)
9462                .linked_order_ids(vec![ClientOrderId::from(sibling)])
9463                .submit(true)
9464                .build();
9465            cache
9466                .borrow_mut()
9467                .add_order(order.clone(), Some(position_id), None, false)
9468                .unwrap();
9469            engine.accept_order(&mut order);
9470        }
9471        assert_eq!(
9472            cache.borrow().order(&sibling_id).unwrap().status(),
9473            OrderStatus::Submitted
9474        );
9475        assert!(engine.order_exists(sibling_id));
9476        let (closing, _) = pending_position_fill(
9477            &instrument,
9478            position_id,
9479            "ACCEPT-CLOSE",
9480            OrderSide::Sell,
9481            closing_quantity,
9482        );
9483        cache
9484            .borrow_mut()
9485            .add_order(closing.clone(), Some(position_id), None, false)
9486            .unwrap();
9487        engine
9488            .apply_fills(
9489                &closing,
9490                &[(Price::from("1000.00"), Quantity::from(closing_quantity))],
9491                LiquiditySide::Taker,
9492                Some(position_id),
9493                Some(&position),
9494                None,
9495                false,
9496            )
9497            .unwrap();
9498        let ids = engine.reduce_only_order_ids(position_id);
9499        engine
9500            .sync_reduce_only_orders(&closing, Some(&position), &ids)
9501            .unwrap();
9502        let events = events.borrow();
9503        let actual: Vec<_> = events
9504            .iter()
9505            .map(|(event, _)| match event {
9506                OrderEventAny::Accepted(event) => ("accepted", event.client_order_id.as_str()),
9507                OrderEventAny::Filled(fill) => {
9508                    assert_eq!(fill.last_qty, Quantity::from(closing_quantity));
9509                    assert_eq!(fill.last_px, Price::from("1000.00"));
9510                    ("filled", fill.client_order_id.as_str())
9511                }
9512                OrderEventAny::Updated(event) => {
9513                    assert_eq!(event.quantity, Quantity::from("5.000"));
9514                    assert_eq!(event.price, Some(Price::from("2000.00")));
9515                    assert_eq!(event.trigger_price, None);
9516                    ("updated", event.client_order_id.as_str())
9517                }
9518                OrderEventAny::Canceled(event) => ("canceled", event.client_order_id.as_str()),
9519                other => panic!("Unexpected event {other:?}"),
9520            })
9521            .collect();
9522        let mut expected = vec![
9523            ("accepted", "ACCEPT-A"),
9524            ("accepted", "ACCEPT-B"),
9525            ("filled", "ACCEPT-CLOSE"),
9526        ];
9527
9528        if canceled {
9529            expected.extend([("canceled", "ACCEPT-A"), ("canceled", "ACCEPT-B")]);
9530        } else {
9531            expected.push(("updated", "ACCEPT-A"));
9532        }
9533        assert_eq!(actual, expected);
9534        assert_eq!(engine.order_exists(sibling_id), !canceled);
9535
9536        for (event, applied) in events.iter() {
9537            if !applied {
9538                cache.borrow_mut().update_order(event).unwrap();
9539                if let OrderEventAny::Filled(fill) = event {
9540                    cache
9541                        .borrow_mut()
9542                        .update_position_from_fill(position_id, fill)
9543                        .unwrap();
9544                }
9545            }
9546        }
9547        let cache = cache.borrow();
9548        for id in ["ACCEPT-A", "ACCEPT-B"] {
9549            let order = cache.order(&ClientOrderId::from(id)).unwrap();
9550            let quantity = Quantity::from(if !canceled && id == "ACCEPT-A" {
9551                "5.000"
9552            } else {
9553                "10.000"
9554            });
9555            assert_eq!(
9556                order.status(),
9557                if canceled {
9558                    OrderStatus::Canceled
9559                } else {
9560                    OrderStatus::Accepted
9561                }
9562            );
9563            assert_eq!(order.quantity(), quantity);
9564            assert_eq!(order.filled_qty(), Quantity::from("0.000"));
9565            assert_eq!(order.leaves_qty(), quantity);
9566        }
9567        assert_eq!(
9568            cache.position(&position_id).unwrap().quantity,
9569            Quantity::from(remaining_quantity)
9570        );
9571        assert_eq!(
9572            cache.position(&position_id).unwrap().side,
9573            if canceled {
9574                PositionSide::Flat
9575            } else {
9576                PositionSide::Long
9577            }
9578        );
9579    }
9580
9581    #[rstest]
9582    fn test_position_sync_mixed_ouo_does_not_match_recursively(#[values(0, 1, 2)] delivery: usize) {
9583        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9584        let position_id = PositionId::from("REENTRANT-POSITION");
9585        let cache = Rc::new(RefCell::new(Cache::default()));
9586        let mut engine = OrderMatchingEngine::new(
9587            instrument.clone(),
9588            1,
9589            FillModelHandle::default(),
9590            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
9591            BookType::L2_MBP,
9592            OmsType::Hedging,
9593            AccountType::Margin,
9594            Rc::new(RefCell::new(VirtualClock::new())),
9595            cache.clone(),
9596            Default::default(),
9597        );
9598        let (opening, opening_fill) = pending_position_fill(
9599            &instrument,
9600            position_id,
9601            "REENTRANT-OPEN",
9602            OrderSide::Buy,
9603            "10.000",
9604        );
9605        let position = Position::new(&instrument, opening_fill.clone());
9606        engine
9607            .account_ids
9608            .insert(position.trader_id, position.account_id);
9609        cache
9610            .borrow_mut()
9611            .add_order(opening, Some(position_id), None, false)
9612            .unwrap();
9613        cache
9614            .borrow_mut()
9615            .update_order(&OrderEventAny::Filled(opening_fill))
9616            .unwrap();
9617        cache
9618            .borrow_mut()
9619            .add_position(&position, OmsType::Hedging)
9620            .unwrap();
9621
9622        for (id, price, size) in [(1, "1000.00", "1.000"), (2, "999.00", "9.000")] {
9623            engine
9624                .process_order_book_delta(&OrderBookDelta::new(
9625                    instrument.id(),
9626                    BookAction::Add,
9627                    BookOrder::new(OrderSide::Buy, Price::from(price), Quantity::from(size), id),
9628                    0,
9629                    id,
9630                    UnixNanos::from(id),
9631                    UnixNanos::from(id),
9632                ))
9633                .unwrap();
9634        }
9635        let events = Rc::new(RefCell::new(Vec::new()));
9636        let events_handler = events.clone();
9637        let handler_cache = cache.clone();
9638        engine.set_event_handler(Rc::new(move |event| {
9639            if delivery == 0 || matches!(event, OrderEventAny::Accepted(_)) {
9640                handler_cache.borrow_mut().update_order(&event).unwrap();
9641                if let OrderEventAny::Filled(fill) = &event {
9642                    handler_cache
9643                        .borrow_mut()
9644                        .update_position_from_fill(position_id, fill)
9645                        .unwrap();
9646                }
9647            }
9648            events_handler.borrow_mut().push(event);
9649        }));
9650
9651        for (id, sibling, reduce_only) in [
9652            ("REENTRANT-A", "REENTRANT-B", true),
9653            ("REENTRANT-B", "REENTRANT-A", false),
9654        ] {
9655            let mut builder = OrderTestBuilder::new(OrderType::Limit);
9656            builder
9657                .instrument_id(instrument.id())
9658                .client_order_id(ClientOrderId::from(id))
9659                .side(OrderSide::Sell)
9660                .quantity(Quantity::from("10.000"))
9661                .price(Price::from("999.00"))
9662                .reduce_only(reduce_only)
9663                .contingency_type(ContingencyType::Ouo)
9664                .linked_order_ids(vec![ClientOrderId::from(sibling)])
9665                .submit(true);
9666            let mut order = builder.build();
9667            order.set_liquidity_side(LiquiditySide::Taker);
9668            cache
9669                .borrow_mut()
9670                .add_order(order.clone(), Some(position_id), None, false)
9671                .unwrap();
9672            engine.accept_order(&mut order);
9673        }
9674        events.borrow_mut().clear();
9675
9676        let (mut closing, _) = pending_position_fill(
9677            &instrument,
9678            position_id,
9679            "REENTRANT-CLOSE",
9680            OrderSide::Sell,
9681            "4.000",
9682        );
9683        cache
9684            .borrow_mut()
9685            .add_order(closing.clone(), Some(position_id), None, false)
9686            .unwrap();
9687        engine.process_order(&mut closing, position.account_id);
9688        let mut acknowledged = 0;
9689
9690        if delivery == 2 {
9691            for event in &events.borrow()[..5] {
9692                cache.borrow_mut().update_order(event).unwrap();
9693                if let OrderEventAny::Filled(fill) = event {
9694                    cache
9695                        .borrow_mut()
9696                        .update_position_from_fill(position_id, fill)
9697                        .unwrap();
9698                }
9699            }
9700            acknowledged = 5;
9701        }
9702        assert_eq!(
9703            engine
9704                .position_quantity_remaining(
9705                    &closing,
9706                    Some(&cache.borrow().position(&position_id).unwrap())
9707                )
9708                .unwrap(),
9709            Quantity::from("6.000")
9710        );
9711
9712        for id in ["REENTRANT-A", "REENTRANT-B"] {
9713            let order = engine.order_snapshot(ClientOrderId::from(id)).unwrap();
9714            assert_eq!(order.quantity(), Quantity::from("6.000"));
9715            assert_eq!(order.filled_qty(), Quantity::from("0.000"));
9716            assert_eq!(order.leaves_qty(), Quantity::from("6.000"));
9717        }
9718        let (mut flattening, _) = pending_position_fill(
9719            &instrument,
9720            position_id,
9721            "REENTRANT-FLAT",
9722            OrderSide::Sell,
9723            "6.000",
9724        );
9725        cache
9726            .borrow_mut()
9727            .add_order(flattening.clone(), Some(position_id), None, false)
9728            .unwrap();
9729        engine.process_order(&mut flattening, position.account_id);
9730        let recorded = events.borrow();
9731        let actual: Vec<_> = recorded
9732            .iter()
9733            .map(|event| match event {
9734                OrderEventAny::Filled(fill) => (
9735                    "fill",
9736                    fill.client_order_id.as_str(),
9737                    fill.last_qty,
9738                    Some(fill.last_px),
9739                ),
9740                OrderEventAny::Updated(update) => {
9741                    assert_eq!(update.trigger_price, None);
9742                    (
9743                        "update",
9744                        update.client_order_id.as_str(),
9745                        update.quantity,
9746                        update.price,
9747                    )
9748                }
9749                OrderEventAny::Canceled(cancel) => (
9750                    "cancel",
9751                    cancel.client_order_id.as_str(),
9752                    Quantity::zero(3),
9753                    None,
9754                ),
9755                other => panic!("Unexpected event {other:?}"),
9756            })
9757            .collect();
9758        assert_eq!(
9759            actual,
9760            vec![
9761                (
9762                    "fill",
9763                    "REENTRANT-CLOSE",
9764                    Quantity::from("1.000"),
9765                    Some(Price::from("1000.00"))
9766                ),
9767                (
9768                    "update",
9769                    "REENTRANT-A",
9770                    Quantity::from("9.000"),
9771                    Some(Price::from("999.00"))
9772                ),
9773                (
9774                    "update",
9775                    "REENTRANT-B",
9776                    Quantity::from("9.000"),
9777                    Some(Price::from("999.00"))
9778                ),
9779                (
9780                    "fill",
9781                    "REENTRANT-CLOSE",
9782                    Quantity::from("3.000"),
9783                    Some(Price::from("999.00"))
9784                ),
9785                (
9786                    "update",
9787                    "REENTRANT-A",
9788                    Quantity::from("6.000"),
9789                    Some(Price::from("999.00"))
9790                ),
9791                (
9792                    "update",
9793                    "REENTRANT-B",
9794                    Quantity::from("6.000"),
9795                    Some(Price::from("999.00"))
9796                ),
9797                (
9798                    "fill",
9799                    "REENTRANT-FLAT",
9800                    Quantity::from("1.000"),
9801                    Some(Price::from("1000.00"))
9802                ),
9803                (
9804                    "update",
9805                    "REENTRANT-A",
9806                    Quantity::from("5.000"),
9807                    Some(Price::from("999.00"))
9808                ),
9809                (
9810                    "update",
9811                    "REENTRANT-B",
9812                    Quantity::from("5.000"),
9813                    Some(Price::from("999.00"))
9814                ),
9815                (
9816                    "fill",
9817                    "REENTRANT-FLAT",
9818                    Quantity::from("5.000"),
9819                    Some(Price::from("999.00"))
9820                ),
9821                ("cancel", "REENTRANT-A", Quantity::zero(3), None),
9822                ("cancel", "REENTRANT-B", Quantity::zero(3), None),
9823            ]
9824        );
9825
9826        if delivery != 0 {
9827            for event in &recorded[acknowledged..] {
9828                cache.borrow_mut().update_order(event).unwrap();
9829                if let OrderEventAny::Filled(fill) = event {
9830                    cache
9831                        .borrow_mut()
9832                        .update_position_from_fill(position_id, fill)
9833                        .unwrap();
9834                }
9835            }
9836        }
9837        let cache = cache.borrow();
9838        assert_eq!(
9839            cache.position(&position_id).unwrap().quantity,
9840            Quantity::from("0.000")
9841        );
9842        assert_eq!(
9843            cache.position(&position_id).unwrap().side,
9844            PositionSide::Flat
9845        );
9846
9847        for id in ["REENTRANT-A", "REENTRANT-B"] {
9848            let order = cache.order(&ClientOrderId::from(id)).unwrap();
9849            assert_eq!(order.status(), OrderStatus::Canceled);
9850            assert_eq!(order.quantity(), Quantity::from("5.000"));
9851            assert_eq!(order.filled_qty(), Quantity::from("0.000"));
9852            assert_eq!(order.leaves_qty(), Quantity::from("5.000"));
9853            assert!(!engine.order_exists(order.client_order_id()));
9854        }
9855    }
9856
9857    #[rstest]
9858    fn test_position_sync_does_not_match_recursively(#[values(false, true)] deferred: bool) {
9859        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
9860        let position_id = PositionId::from("REENTRANT-POSITION");
9861        let cache = Rc::new(RefCell::new(Cache::default()));
9862        let mut engine = OrderMatchingEngine::new(
9863            instrument.clone(),
9864            1,
9865            FillModelHandle::default(),
9866            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
9867            BookType::L2_MBP,
9868            OmsType::Hedging,
9869            AccountType::Margin,
9870            Rc::new(RefCell::new(VirtualClock::new())),
9871            cache.clone(),
9872            Default::default(),
9873        );
9874        let (opening, opening_fill) = pending_position_fill(
9875            &instrument,
9876            position_id,
9877            "REENTRANT-OPEN",
9878            OrderSide::Buy,
9879            "10.000",
9880        );
9881        let position = Position::new(&instrument, opening_fill.clone());
9882        engine
9883            .account_ids
9884            .insert(position.trader_id, position.account_id);
9885        cache
9886            .borrow_mut()
9887            .add_order(opening, Some(position_id), None, false)
9888            .unwrap();
9889        cache
9890            .borrow_mut()
9891            .update_order(&OrderEventAny::Filled(opening_fill))
9892            .unwrap();
9893        cache
9894            .borrow_mut()
9895            .add_position(&position, OmsType::Hedging)
9896            .unwrap();
9897        let (parent, mut parent_fill) = pending_position_fill(
9898            &instrument,
9899            position_id,
9900            "REENTRANT-PARENT",
9901            OrderSide::Buy,
9902            "2.000",
9903        );
9904        parent_fill.venue_order_id = VenueOrderId::from("REENTRANT-PARENT");
9905        cache
9906            .borrow_mut()
9907            .add_order(parent, Some(position_id), None, false)
9908            .unwrap();
9909        cache
9910            .borrow_mut()
9911            .update_order(&OrderEventAny::Filled(parent_fill))
9912            .unwrap();
9913
9914        for (id, price, size) in [(1, "1000.00", "1.000"), (2, "999.00", "9.000")] {
9915            engine
9916                .process_order_book_delta(&OrderBookDelta::new(
9917                    instrument.id(),
9918                    BookAction::Add,
9919                    BookOrder::new(OrderSide::Buy, Price::from(price), Quantity::from(size), id),
9920                    0,
9921                    id,
9922                    UnixNanos::from(id),
9923                    UnixNanos::from(id),
9924                ))
9925                .unwrap();
9926        }
9927        let events = Rc::new(RefCell::new(Vec::new()));
9928        let events_handler = events.clone();
9929        let handler_cache = cache.clone();
9930        engine.set_event_handler(Rc::new(move |event| {
9931            if !deferred || matches!(event, OrderEventAny::Accepted(_)) {
9932                handler_cache.borrow_mut().update_order(&event).unwrap();
9933                if let OrderEventAny::Filled(fill) = &event {
9934                    handler_cache
9935                        .borrow_mut()
9936                        .update_position_from_fill(position_id, fill)
9937                        .unwrap();
9938                }
9939            }
9940            events_handler.borrow_mut().push(event);
9941        }));
9942
9943        for (id, quantity, parent) in [
9944            ("REENTRANT-A", "2.000", Some("REENTRANT-PARENT")),
9945            ("REENTRANT-B", "10.000", None),
9946        ] {
9947            let mut builder = OrderTestBuilder::new(OrderType::Limit);
9948            builder
9949                .instrument_id(instrument.id())
9950                .client_order_id(ClientOrderId::from(id))
9951                .side(OrderSide::Sell)
9952                .quantity(Quantity::from(quantity))
9953                .price(Price::from("999.00"))
9954                .reduce_only(true)
9955                .submit(true);
9956
9957            if let Some(parent) = parent {
9958                builder.parent_order_id(ClientOrderId::from(parent));
9959            }
9960            let mut order = builder.build();
9961            order.set_liquidity_side(LiquiditySide::Taker);
9962            cache
9963                .borrow_mut()
9964                .add_order(order.clone(), Some(position_id), None, false)
9965                .unwrap();
9966            engine.accept_order(&mut order);
9967        }
9968        events.borrow_mut().clear();
9969
9970        assert_eq!(engine.core.iterate_asks().len(), 2);
9971        assert_eq!(
9972            cache.borrow().position(&position_id).unwrap().quantity,
9973            Quantity::from("10.000")
9974        );
9975        engine.iterate(UnixNanos::from(3), AggressorSide::NoAggressor);
9976
9977        let events = events.borrow();
9978        let fills: Vec<_> = events
9979            .iter()
9980            .filter_map(|event| match event {
9981                OrderEventAny::Filled(fill) => Some((
9982                    fill.client_order_id.to_string(),
9983                    fill.last_qty,
9984                    fill.last_px,
9985                )),
9986                _ => None,
9987            })
9988            .collect();
9989        assert_eq!(
9990            fills,
9991            vec![
9992                (
9993                    "REENTRANT-A".to_string(),
9994                    Quantity::from("1.000"),
9995                    Price::from("1000.00")
9996                ),
9997                (
9998                    "REENTRANT-A".to_string(),
9999                    Quantity::from("1.000"),
10000                    Price::from("999.00")
10001                ),
10002                (
10003                    "REENTRANT-B".to_string(),
10004                    Quantity::from("1.000"),
10005                    Price::from("1000.00")
10006                ),
10007                (
10008                    "REENTRANT-B".to_string(),
10009                    Quantity::from("7.000"),
10010                    Price::from("999.00")
10011                ),
10012            ]
10013        );
10014        assert!(
10015            !events
10016                .iter()
10017                .any(|event| matches!(event, OrderEventAny::Canceled(_)))
10018        );
10019
10020        if deferred {
10021            for event in events.iter() {
10022                cache.borrow_mut().update_order(event).unwrap();
10023                if let OrderEventAny::Filled(fill) = event {
10024                    cache
10025                        .borrow_mut()
10026                        .update_position_from_fill(position_id, fill)
10027                        .unwrap();
10028                }
10029            }
10030        }
10031        let cache = cache.borrow();
10032        assert_eq!(
10033            cache.position(&position_id).unwrap().quantity,
10034            Quantity::from("0.000")
10035        );
10036
10037        for (id, quantity) in [("REENTRANT-A", "2.000"), ("REENTRANT-B", "8.000")] {
10038            let order = cache.order(&ClientOrderId::from(id)).unwrap();
10039            assert_eq!(order.status(), OrderStatus::Filled);
10040            assert_eq!(order.quantity(), Quantity::from(quantity));
10041            assert_eq!(order.filled_qty(), Quantity::from(quantity));
10042        }
10043    }
10044
10045    #[rstest]
10046    fn test_position_sync_includes_newly_activated_oto_child(
10047        #[values(false, true)] deferred: bool,
10048    ) {
10049        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10050        let position_id = PositionId::from("ACTIVATION-POSITION");
10051        let cache = Rc::new(RefCell::new(Cache::default()));
10052        let mut engine = OrderMatchingEngine::new(
10053            instrument.clone(),
10054            1,
10055            FillModelHandle::default(),
10056            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
10057            BookType::L2_MBP,
10058            OmsType::Hedging,
10059            AccountType::Margin,
10060            Rc::new(RefCell::new(VirtualClock::new())),
10061            cache.clone(),
10062            Default::default(),
10063        );
10064        let (opening, opening_fill) = pending_position_fill(
10065            &instrument,
10066            position_id,
10067            "ACTIVATION-OPEN",
10068            OrderSide::Buy,
10069            "10.000",
10070        );
10071        let position = Position::new(&instrument, opening_fill.clone());
10072        engine
10073            .account_ids
10074            .insert(position.trader_id, position.account_id);
10075        cache
10076            .borrow_mut()
10077            .add_order(opening, Some(position_id), None, false)
10078            .unwrap();
10079        cache
10080            .borrow_mut()
10081            .update_order(&OrderEventAny::Filled(opening_fill))
10082            .unwrap();
10083        cache
10084            .borrow_mut()
10085            .add_position(&position, OmsType::Hedging)
10086            .unwrap();
10087        let parent_id = ClientOrderId::from("ACTIVATION-PARENT");
10088        let child_id = ClientOrderId::from("ACTIVATION-CHILD");
10089        let parent = OrderTestBuilder::new(OrderType::Market)
10090            .instrument_id(instrument.id())
10091            .client_order_id(parent_id)
10092            .side(OrderSide::Buy)
10093            .quantity(Quantity::from("10.000"))
10094            .contingency_type(ContingencyType::Oto)
10095            .linked_order_ids(vec![child_id])
10096            .submit(true)
10097            .build();
10098        let child = OrderTestBuilder::new(OrderType::Limit)
10099            .instrument_id(instrument.id())
10100            .client_order_id(child_id)
10101            .side(OrderSide::Sell)
10102            .quantity(Quantity::from("10.000"))
10103            .price(Price::from("2000.00"))
10104            .reduce_only(true)
10105            .parent_order_id(parent_id)
10106            .submit(true)
10107            .build();
10108
10109        for order in [parent.clone(), child] {
10110            cache
10111                .borrow_mut()
10112                .add_order(order, Some(position_id), None, false)
10113                .unwrap();
10114        }
10115        let events = Rc::new(RefCell::new(Vec::new()));
10116        let events_handler = events.clone();
10117        let handler_cache = cache.clone();
10118        engine.set_event_handler(Rc::new(move |event| {
10119            if !deferred || matches!(event, OrderEventAny::Accepted(_)) {
10120                handler_cache.borrow_mut().update_order(&event).unwrap();
10121                if let OrderEventAny::Filled(fill) = &event {
10122                    handler_cache
10123                        .borrow_mut()
10124                        .update_position_from_fill(position_id, fill)
10125                        .unwrap();
10126                }
10127            }
10128            events_handler.borrow_mut().push(event);
10129        }));
10130        assert!(!engine.order_exists(child_id));
10131
10132        engine
10133            .apply_fills(
10134                &parent,
10135                &[(Price::from("1000.00"), Quantity::from("2.000"))],
10136                LiquiditySide::Taker,
10137                Some(position_id),
10138                Some(&position),
10139                None,
10140                false,
10141            )
10142            .unwrap();
10143
10144        let events = events.borrow();
10145        assert_eq!(events.len(), 3);
10146        assert!(
10147            matches!(&events[0], OrderEventAny::Filled(fill) if fill.client_order_id == parent_id && fill.last_qty == Quantity::from("2.000"))
10148        );
10149        assert!(
10150            matches!(&events[1], OrderEventAny::Accepted(accepted) if accepted.client_order_id == child_id)
10151        );
10152        let OrderEventAny::Updated(update) = &events[2] else {
10153            panic!("Expected child quantity update")
10154        };
10155        assert_eq!(update.client_order_id, child_id);
10156        assert_eq!(update.quantity, Quantity::from("2.000"));
10157        assert_eq!(update.price, Some(Price::from("2000.00")));
10158        assert_eq!(update.trigger_price, None);
10159        assert!(engine.order_exists(child_id));
10160        assert_eq!(
10161            engine.order_snapshot(child_id).unwrap().quantity(),
10162            Quantity::from("2.000")
10163        );
10164
10165        if deferred {
10166            for event in events.iter() {
10167                if matches!(event, OrderEventAny::Accepted(_)) {
10168                    continue;
10169                }
10170                cache.borrow_mut().update_order(event).unwrap();
10171                if let OrderEventAny::Filled(fill) = event {
10172                    cache
10173                        .borrow_mut()
10174                        .update_position_from_fill(position_id, fill)
10175                        .unwrap();
10176                }
10177            }
10178        }
10179        let cache = cache.borrow();
10180        assert_eq!(
10181            cache.position(&position_id).unwrap().quantity,
10182            Quantity::from("12.000")
10183        );
10184        let child = cache.order(&child_id).unwrap();
10185        assert_eq!(child.status(), OrderStatus::Accepted);
10186        assert_eq!(child.quantity(), Quantity::from("2.000"));
10187        assert_eq!(child.filled_qty(), Quantity::from("0.000"));
10188        assert_eq!(child.leaves_qty(), Quantity::from("2.000"));
10189    }
10190
10191    fn pending_position_fill(
10192        instrument: &InstrumentAny,
10193        position_id: PositionId,
10194        id: &str,
10195        side: OrderSide,
10196        quantity: &str,
10197    ) -> (OrderAny, OrderFilled) {
10198        let order = OrderTestBuilder::new(OrderType::Market)
10199            .instrument_id(instrument.id())
10200            .client_order_id(ClientOrderId::from(id))
10201            .side(side)
10202            .quantity(Quantity::from(quantity))
10203            .submit(true)
10204            .build();
10205        let OrderEventAny::Filled(fill) = TestOrderEventStubs::filled(
10206            &order,
10207            instrument,
10208            Some(TradeId::from(id)),
10209            Some(position_id),
10210            Some(Price::from("1000.00")),
10211            None,
10212            None,
10213            Some(Money::zero(instrument.quote_currency())),
10214            None,
10215            None,
10216        ) else {
10217            unreachable!()
10218        };
10219        (order, fill)
10220    }
10221
10222    #[rstest]
10223    fn test_pending_modify_updates_acknowledge_individually_and_reset() {
10224        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10225        let cache = Rc::new(RefCell::new(Cache::default()));
10226        let mut engine = OrderMatchingEngine::new(
10227            instrument.clone(),
10228            1,
10229            FillModelHandle::default(),
10230            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
10231            BookType::L2_MBP,
10232            OmsType::Netting,
10233            AccountType::Margin,
10234            Rc::new(RefCell::new(VirtualClock::new())),
10235            cache.clone(),
10236            OrderMatchingEngineConfig::default(),
10237        );
10238        let mut order = OrderTestBuilder::new(OrderType::Limit)
10239            .instrument_id(instrument.id())
10240            .side(OrderSide::Buy)
10241            .quantity(Quantity::from("1.000"))
10242            .price(Price::from("99.00"))
10243            .submit(true)
10244            .build();
10245        let id = order.client_order_id();
10246        engine.set_event_handler(Rc::new(|_| {}));
10247        process_cached_order(&mut engine, &mut order, AccountId::from("ACCOUNT-001"));
10248        let pending = Rc::new(RefCell::new(Vec::new()));
10249        let events = pending.clone();
10250        engine.set_event_handler(Rc::new(move |event| events.borrow_mut().push(event)));
10251
10252        for (quantity, price) in [
10253            (Some(Quantity::from("2.000")), None),
10254            (None, Some(Price::from("100.00"))),
10255        ] {
10256            engine.process_modify(
10257                &ModifyOrder::new(
10258                    order.trader_id(),
10259                    None,
10260                    order.strategy_id(),
10261                    order.instrument_id(),
10262                    id,
10263                    None,
10264                    quantity,
10265                    price,
10266                    None,
10267                    UUID4::new(),
10268                    UnixNanos::from(1),
10269                    None,
10270                    None,
10271                ),
10272                AccountId::from("ACCOUNT-001"),
10273            );
10274        }
10275        assert_eq!(pending.borrow().len(), 2);
10276        cache
10277            .borrow_mut()
10278            .update_order(&pending.borrow()[0])
10279            .unwrap();
10280        let snapshot = engine.order_snapshot(id).unwrap();
10281        assert_eq!(snapshot.quantity(), Quantity::from("2.000"));
10282        assert_eq!(snapshot.price(), Some(Price::from("100.00")));
10283        assert_eq!(engine.pending_updates.borrow()[&id].len(), 1);
10284        cache
10285            .borrow_mut()
10286            .update_order(&pending.borrow()[1])
10287            .unwrap();
10288        engine.iterate(UnixNanos::from(2), AggressorSide::NoAggressor);
10289        assert!(engine.pending_updates.borrow().is_empty());
10290        engine.process_modify(
10291            &ModifyOrder::new(
10292                order.trader_id(),
10293                None,
10294                order.strategy_id(),
10295                order.instrument_id(),
10296                id,
10297                None,
10298                Some(Quantity::from("3.000")),
10299                None,
10300                None,
10301                UUID4::new(),
10302                UnixNanos::from(3),
10303                None,
10304                None,
10305            ),
10306            AccountId::from("ACCOUNT-001"),
10307        );
10308        assert_eq!(
10309            engine.order_snapshot(id).unwrap().quantity(),
10310            Quantity::from("3.000")
10311        );
10312        engine.reset();
10313        assert!(engine.pending_updates.borrow().is_empty());
10314        assert_eq!(
10315            engine.order_snapshot(id).unwrap().quantity(),
10316            Quantity::from("2.000")
10317        );
10318    }
10319
10320    #[rstest]
10321    fn test_process_order_rejects_reduce_only_when_support_is_disabled() {
10322        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10323        let mut engine = OrderMatchingEngine::new(
10324            instrument.clone(),
10325            1,
10326            FillModelHandle::default(),
10327            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
10328            BookType::L1_MBP,
10329            OmsType::Netting,
10330            AccountType::Margin,
10331            Rc::new(RefCell::new(VirtualClock::new())),
10332            Rc::new(RefCell::new(Cache::default())),
10333            OrderMatchingEngineConfig::builder()
10334                .use_reduce_only(false)
10335                .build(),
10336        );
10337        let events = Rc::new(RefCell::new(Vec::new()));
10338        let events_handler = Rc::clone(&events);
10339        engine.set_event_handler(Rc::new(move |event| {
10340            events_handler.borrow_mut().push(event);
10341        }));
10342        let mut order = OrderTestBuilder::new(OrderType::Market)
10343            .instrument_id(instrument.id())
10344            .side(OrderSide::Sell)
10345            .quantity(Quantity::from("1.000"))
10346            .reduce_only(true)
10347            .submit(true)
10348            .build();
10349
10350        process_cached_order(&mut engine, &mut order, AccountId::from("ACCOUNT-001"));
10351
10352        let events = events.borrow();
10353        assert_eq!(events.len(), 1);
10354        let OrderEventAny::Rejected(rejected) = &events[0] else {
10355            panic!("Expected OrderRejected, was {:?}", events[0]);
10356        };
10357        assert_eq!(
10358            rejected.reason,
10359            "Reduce-only orders are not supported by this matching engine"
10360        );
10361    }
10362
10363    #[rstest]
10364    #[case::accepted_parent(false, false)]
10365    #[case::rejected_parent(true, false)]
10366    #[case::child_listed_first(false, true)]
10367    fn test_process_order_parks_oto_child_until_parent_rejected(
10368        #[case] parent_rejected: bool,
10369        #[case] child_first: bool,
10370    ) {
10371        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10372        let cache = Rc::new(RefCell::new(Cache::default()));
10373        let mut engine = OrderMatchingEngine::new(
10374            instrument.clone(),
10375            1,
10376            FillModelHandle::default(),
10377            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
10378            BookType::L1_MBP,
10379            OmsType::Netting,
10380            AccountType::Margin,
10381            Rc::new(RefCell::new(VirtualClock::new())),
10382            cache.clone(),
10383            OrderMatchingEngineConfig::builder()
10384                .use_reduce_only(false)
10385                .build(),
10386        );
10387        let handler_cache = cache.clone();
10388        engine.set_event_handler(Rc::new(move |event| {
10389            handler_cache.borrow_mut().update_order(&event).unwrap();
10390        }));
10391        let parent_id = ClientOrderId::from("PARK-PARENT");
10392        let child_id = ClientOrderId::from("PARK-CHILD");
10393        // A reduce-only parent is rejected because this engine does not support reduce-only
10394        let mut parent = OrderTestBuilder::new(OrderType::Limit)
10395            .instrument_id(instrument.id())
10396            .client_order_id(parent_id)
10397            .side(OrderSide::Buy)
10398            .quantity(Quantity::from("1.000"))
10399            .price(Price::from("900.00"))
10400            .reduce_only(parent_rejected)
10401            .contingency_type(ContingencyType::Oto)
10402            .linked_order_ids(vec![child_id])
10403            .submit(true)
10404            .build();
10405        let mut child = OrderTestBuilder::new(OrderType::Limit)
10406            .instrument_id(instrument.id())
10407            .client_order_id(child_id)
10408            .side(OrderSide::Sell)
10409            .quantity(Quantity::from("1.000"))
10410            .price(Price::from("1100.00"))
10411            .parent_order_id(parent_id)
10412            .submit(true)
10413            .build();
10414
10415        for order in [&parent, &child] {
10416            cache
10417                .borrow_mut()
10418                .add_order(order.clone(), None, None, false)
10419                .unwrap();
10420        }
10421        let account_id = AccountId::from("ACCOUNT-001");
10422
10423        if child_first {
10424            engine.process_order(&mut child, account_id);
10425            engine.process_order(&mut parent, account_id);
10426        } else {
10427            engine.process_order(&mut parent, account_id);
10428            engine.process_order(&mut child, account_id);
10429        }
10430
10431        assert_eq!(engine.order_exists(parent_id), !parent_rejected);
10432        assert!(!engine.order_exists(child_id));
10433        assert_eq!(
10434            engine.pending_oto_order_ids.contains(&child_id),
10435            !parent_rejected
10436        );
10437        assert_eq!(
10438            cache.borrow().order(&child_id).unwrap().status(),
10439            if parent_rejected {
10440                OrderStatus::Rejected
10441            } else {
10442                OrderStatus::Submitted
10443            }
10444        );
10445    }
10446
10447    #[rstest]
10448    fn test_process_order_rejects_oto_child_of_deferred_parent_rejection_until_purged() {
10449        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10450        let cache = Rc::new(RefCell::new(Cache::default()));
10451        let mut engine = OrderMatchingEngine::new(
10452            instrument.clone(),
10453            1,
10454            FillModelHandle::default(),
10455            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
10456            BookType::L1_MBP,
10457            OmsType::Netting,
10458            AccountType::Margin,
10459            Rc::new(RefCell::new(VirtualClock::new())),
10460            cache.clone(),
10461            OrderMatchingEngineConfig::builder()
10462                .use_reduce_only(false)
10463                .build(),
10464        );
10465        // Defers events as an immediate-mode venue does until its command completes
10466        let deferred = Rc::new(RefCell::new(Vec::new()));
10467        let handler_deferred = deferred.clone();
10468        engine.set_event_handler(Rc::new(move |event| {
10469            handler_deferred.borrow_mut().push(event);
10470        }));
10471        let parent_id = ClientOrderId::from("PARK-PARENT");
10472        let child_id = ClientOrderId::from("PARK-CHILD");
10473        // A reduce-only parent is rejected because this engine does not support reduce-only
10474        let mut parent = OrderTestBuilder::new(OrderType::Limit)
10475            .instrument_id(instrument.id())
10476            .client_order_id(parent_id)
10477            .side(OrderSide::Buy)
10478            .quantity(Quantity::from("1.000"))
10479            .price(Price::from("900.00"))
10480            .reduce_only(true)
10481            .contingency_type(ContingencyType::Oto)
10482            .linked_order_ids(vec![child_id])
10483            .submit(true)
10484            .build();
10485        let mut child = OrderTestBuilder::new(OrderType::Limit)
10486            .instrument_id(instrument.id())
10487            .client_order_id(child_id)
10488            .side(OrderSide::Sell)
10489            .quantity(Quantity::from("1.000"))
10490            .price(Price::from("1100.00"))
10491            .parent_order_id(parent_id)
10492            .submit(true)
10493            .build();
10494
10495        for order in [&parent, &child] {
10496            cache
10497                .borrow_mut()
10498                .add_order(order.clone(), None, None, false)
10499                .unwrap();
10500        }
10501        let account_id = AccountId::from("ACCOUNT-001");
10502        engine.process_order(&mut parent, account_id);
10503        engine.process_order(&mut child, account_id);
10504
10505        let events = deferred.borrow().clone();
10506        assert_eq!(events.len(), 2);
10507        assert!(
10508            events
10509                .iter()
10510                .all(|event| matches!(event, OrderEventAny::Rejected(_)))
10511        );
10512        assert_eq!(events[1].client_order_id(), child_id);
10513        assert!(!engine.pending_oto_order_ids.contains(&child_id));
10514
10515        // Kept until the cache shows the rejection
10516        engine.iterate(UnixNanos::from(1), AggressorSide::NoAggressor);
10517        assert!(engine.rejected_oto_parent_ids.borrow().contains(&parent_id));
10518
10519        for event in &events {
10520            cache.borrow_mut().update_order(event).unwrap();
10521        }
10522        engine.iterate(UnixNanos::from(2), AggressorSide::NoAggressor);
10523        assert!(engine.rejected_oto_parent_ids.borrow().is_empty());
10524    }
10525
10526    #[rstest]
10527    fn test_post_match_order_action_does_not_clone_closed_order() {
10528        let order = post_match_closed_limit_order();
10529        let clone_count = Cell::new(0);
10530
10531        let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
10532            clone_count.set(clone_count.get() + 1);
10533            order.clone()
10534        });
10535
10536        assert!(matches!(action, PostMatchOrderAction::RemoveClosed));
10537        assert_eq!(clone_count.get(), 0);
10538    }
10539
10540    #[rstest]
10541    fn test_post_match_order_action_clones_expired_gtd_order_once() {
10542        let order = post_match_gtd_limit_order();
10543        let clone_count = Cell::new(0);
10544
10545        let action = post_match_order_action(&order, true, UnixNanos::from(10_u64), |order| {
10546            clone_count.set(clone_count.get() + 1);
10547            order.clone()
10548        });
10549
10550        let PostMatchOrderAction::Expire(cloned) = action else {
10551            panic!("Expected expired action, was {action:?}");
10552        };
10553        assert_eq!(cloned.client_order_id(), order.client_order_id());
10554        assert_eq!(clone_count.get(), 1);
10555    }
10556
10557    #[rstest]
10558    fn test_post_match_order_action_clones_trailing_order_once() {
10559        let order = post_match_trailing_stop_order();
10560        let clone_count = Cell::new(0);
10561
10562        let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
10563            clone_count.set(clone_count.get() + 1);
10564            order.clone()
10565        });
10566
10567        let PostMatchOrderAction::UpdateTrailing(cloned) = action else {
10568            panic!("Expected trailing update action, was {action:?}");
10569        };
10570        assert_eq!(cloned.client_order_id(), order.client_order_id());
10571        assert_eq!(clone_count.get(), 1);
10572    }
10573
10574    fn post_match_limit_order() -> OrderAny {
10575        OrderTestBuilder::new(OrderType::Limit)
10576            .instrument_id(crypto_perpetual_ethusdt().id())
10577            .side(OrderSide::Buy)
10578            .price(Price::from("1500.00"))
10579            .quantity(Quantity::from("1.000"))
10580            .client_order_id(ClientOrderId::from("POST-MATCH-LIMIT"))
10581            .submit(true)
10582            .build()
10583    }
10584
10585    fn post_match_closed_limit_order() -> OrderAny {
10586        let account_id = AccountId::from("SIM-001");
10587        let venue_order_id = VenueOrderId::from("V-001");
10588        let mut order = post_match_limit_order();
10589        order
10590            .apply(TestOrderEventStubs::accepted(
10591                &order,
10592                account_id,
10593                venue_order_id,
10594            ))
10595            .unwrap();
10596        order
10597            .apply(TestOrderEventStubs::canceled(
10598                &order,
10599                account_id,
10600                Some(venue_order_id),
10601            ))
10602            .unwrap();
10603        order
10604    }
10605
10606    fn post_match_gtd_limit_order() -> OrderAny {
10607        OrderTestBuilder::new(OrderType::Limit)
10608            .instrument_id(crypto_perpetual_ethusdt().id())
10609            .side(OrderSide::Buy)
10610            .price(Price::from("1500.00"))
10611            .quantity(Quantity::from("1.000"))
10612            .time_in_force(TimeInForce::Gtd)
10613            .expire_time(UnixNanos::from(10_u64))
10614            .client_order_id(ClientOrderId::from("POST-MATCH-GTD"))
10615            .submit(true)
10616            .build()
10617    }
10618
10619    fn post_match_trailing_stop_order() -> OrderAny {
10620        OrderTestBuilder::new(OrderType::TrailingStopMarket)
10621            .instrument_id(crypto_perpetual_ethusdt().id())
10622            .side(OrderSide::Buy)
10623            .quantity(Quantity::from("1.000"))
10624            .trigger_price(Price::from("1510.00"))
10625            .trigger_type(TriggerType::BidAsk)
10626            .trailing_offset(Decimal::new(5, 0))
10627            .trailing_offset_type(TrailingOffsetType::Price)
10628            .client_order_id(ClientOrderId::from("POST-MATCH-TRAIL"))
10629            .submit(true)
10630            .build()
10631    }
10632
10633    #[rstest]
10634    fn test_fill_order_calculates_commission_from_fill_liquidity_side() {
10635        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10636        let cache = Rc::new(RefCell::new(Cache::default()));
10637        let clock = Rc::new(RefCell::new(VirtualClock::new()));
10638        let mut engine = OrderMatchingEngine::new(
10639            instrument.clone(),
10640            1,
10641            FillModelHandle::default(),
10642            FeeModelAny::MakerTaker(MakerTakerFeeModel::new(dec!(0.0002), dec!(0.0004))).into(),
10643            BookType::L1_MBP,
10644            OmsType::Netting,
10645            AccountType::Margin,
10646            clock,
10647            cache,
10648            Default::default(),
10649        );
10650        let events = Rc::new(RefCell::new(Vec::new()));
10651        let events_handler = Rc::clone(&events);
10652        engine.set_event_handler(Rc::new(move |event| {
10653            events_handler.borrow_mut().push(event);
10654        }));
10655
10656        let mut order = OrderTestBuilder::new(OrderType::Market)
10657            .instrument_id(instrument.id())
10658            .side(OrderSide::Buy)
10659            .quantity(Quantity::from("1.000"))
10660            .submit(true)
10661            .build();
10662        order.set_liquidity_side(LiquiditySide::Maker);
10663        engine
10664            .account_ids
10665            .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
10666
10667        engine
10668            .fill_order(
10669                &order,
10670                Price::from("1500.00"),
10671                Quantity::from("1.000"),
10672                LiquiditySide::Taker,
10673                None,
10674                None,
10675            )
10676            .unwrap();
10677
10678        let events = events.borrow();
10679        assert_eq!(events.len(), 1);
10680        let fill = match &events[0] {
10681            OrderEventAny::Filled(fill) => fill,
10682            event => panic!("Expected OrderFilled, was {event:?}"),
10683        };
10684        let commission = fill.commission.expect("expected commission");
10685        let expected_commission =
10686            fill.last_qty.as_decimal() * fill.last_px.as_decimal() * dec!(0.0004);
10687
10688        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
10689        assert_eq!(commission.currency, instrument.quote_currency());
10690        assert_eq!(commission.as_decimal(), expected_commission);
10691    }
10692
10693    #[rstest]
10694    fn test_custom_fee_model_handle_is_called_by_fill_order() {
10695        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10696        let cache = Rc::new(RefCell::new(Cache::default()));
10697        let clock = Rc::new(RefCell::new(VirtualClock::new()));
10698        let calls = Rc::new(Cell::new(0));
10699        let expected_commission = Money::from("1.23 USDT");
10700        let fee_model = FeeModelHandle::new(RecordingFeeModel {
10701            calls: Rc::clone(&calls),
10702            commission: expected_commission,
10703        });
10704        let cloned_fee_model = fee_model.clone();
10705        drop(fee_model);
10706        let mut engine = OrderMatchingEngine::new(
10707            instrument.clone(),
10708            1,
10709            FillModelHandle::default(),
10710            cloned_fee_model,
10711            BookType::L1_MBP,
10712            OmsType::Netting,
10713            AccountType::Margin,
10714            clock,
10715            cache,
10716            Default::default(),
10717        );
10718        let events = Rc::new(RefCell::new(Vec::new()));
10719        let events_handler = Rc::clone(&events);
10720        engine.set_event_handler(Rc::new(move |event| {
10721            events_handler.borrow_mut().push(event);
10722        }));
10723
10724        let order = OrderTestBuilder::new(OrderType::Market)
10725            .instrument_id(instrument.id())
10726            .side(OrderSide::Buy)
10727            .quantity(Quantity::from("1.000"))
10728            .submit(true)
10729            .build();
10730        engine
10731            .account_ids
10732            .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
10733
10734        engine
10735            .fill_order(
10736                &order,
10737                Price::from("1500.00"),
10738                Quantity::from("1.000"),
10739                LiquiditySide::Taker,
10740                None,
10741                None,
10742            )
10743            .unwrap();
10744
10745        let events = events.borrow();
10746        assert_eq!(events.len(), 1);
10747        let fill = match &events[0] {
10748            OrderEventAny::Filled(fill) => fill,
10749            event => panic!("Expected OrderFilled, was {event:?}"),
10750        };
10751
10752        assert_eq!(calls.get(), 1);
10753        assert_eq!(fill.commission, Some(expected_commission));
10754    }
10755
10756    #[rstest]
10757    fn test_fill_order_does_not_cache_filled_qty_when_fee_model_fails() {
10758        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10759        let cache = Rc::new(RefCell::new(Cache::default()));
10760        let clock = Rc::new(RefCell::new(VirtualClock::new()));
10761        let mut engine = OrderMatchingEngine::new(
10762            instrument.clone(),
10763            1,
10764            FillModelHandle::default(),
10765            FeeModelHandle::new(FailingFeeModel),
10766            BookType::L1_MBP,
10767            OmsType::Netting,
10768            AccountType::Margin,
10769            clock,
10770            cache,
10771            Default::default(),
10772        );
10773        let events = Rc::new(RefCell::new(Vec::new()));
10774        let events_handler = Rc::clone(&events);
10775        engine.set_event_handler(Rc::new(move |event| {
10776            events_handler.borrow_mut().push(event);
10777        }));
10778
10779        let order = OrderTestBuilder::new(OrderType::Market)
10780            .instrument_id(instrument.id())
10781            .side(OrderSide::Buy)
10782            .quantity(Quantity::from("1.000"))
10783            .submit(true)
10784            .build();
10785        engine
10786            .account_ids
10787            .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
10788
10789        let result = engine.fill_order(
10790            &order,
10791            Price::from("1500.00"),
10792            Quantity::from("1.000"),
10793            LiquiditySide::Taker,
10794            None,
10795            None,
10796        );
10797
10798        assert!(result.is_err());
10799        assert_eq!(engine.cached_filled_qty_len(), 0);
10800        assert!(events.borrow().is_empty());
10801    }
10802
10803    #[rstest]
10804    fn test_process_cancel_all_includes_submitted_orders_for_selected_account() {
10805        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10806        let instrument_id = instrument.id();
10807        let cache = Rc::new(RefCell::new(Cache::default()));
10808        let clock = Rc::new(RefCell::new(VirtualClock::new()));
10809        let mut engine = OrderMatchingEngine::new(
10810            instrument,
10811            1,
10812            FillModelHandle::default(),
10813            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
10814            BookType::L1_MBP,
10815            OmsType::Netting,
10816            AccountType::Margin,
10817            clock,
10818            Rc::clone(&cache),
10819            Default::default(),
10820        );
10821        let selected_account = AccountId::from("ACCOUNT-001");
10822        let other_account = AccountId::from("ACCOUNT-002");
10823        let selected_strategy = StrategyId::from("STRATEGY-001");
10824        let other_strategy = StrategyId::from("STRATEGY-002");
10825        let selected_order = OrderTestBuilder::new(OrderType::Limit)
10826            .strategy_id(selected_strategy)
10827            .instrument_id(instrument_id)
10828            .client_order_id(ClientOrderId::from("O-SUBMITTED-SELECTED"))
10829            .side(OrderSide::Buy)
10830            .price(Price::from("1400.00"))
10831            .quantity(Quantity::from("1.000"))
10832            .build();
10833        let other_order = OrderTestBuilder::new(OrderType::Limit)
10834            .strategy_id(other_strategy)
10835            .instrument_id(instrument_id)
10836            .client_order_id(ClientOrderId::from("O-SUBMITTED-OTHER"))
10837            .side(OrderSide::Buy)
10838            .price(Price::from("1300.00"))
10839            .quantity(Quantity::from("1.000"))
10840            .build();
10841        {
10842            let mut cache = cache.borrow_mut();
10843            cache
10844                .add_order(selected_order.clone(), None, None, false)
10845                .unwrap();
10846            cache
10847                .add_order(other_order.clone(), None, None, false)
10848                .unwrap();
10849            cache
10850                .update_order(&TestOrderEventStubs::submitted(
10851                    &selected_order,
10852                    selected_account,
10853                ))
10854                .unwrap();
10855            cache
10856                .update_order(&TestOrderEventStubs::submitted(&other_order, other_account))
10857                .unwrap();
10858        }
10859
10860        let events = Rc::new(RefCell::new(Vec::new()));
10861        let events_handler = Rc::clone(&events);
10862        let event_cache = Rc::clone(&cache);
10863        engine.set_event_handler(Rc::new(move |event| {
10864            event_cache.borrow_mut().update_order(&event).unwrap();
10865            events_handler.borrow_mut().push(event);
10866        }));
10867        let command = CancelAllOrders::new(
10868            TraderId::from("TRADER-001"),
10869            None,
10870            StrategyId::from("CALLER-001"),
10871            instrument_id,
10872            None,
10873            UUID4::new(),
10874            UnixNanos::default(),
10875            None,
10876            None,
10877        );
10878
10879        engine.process_cancel_all(&command, selected_account);
10880
10881        let events = events.borrow();
10882        assert_eq!(events.len(), 1);
10883        let OrderEventAny::Canceled(canceled) = &events[0] else {
10884            panic!("Expected OrderCanceled, was {:?}", events[0]);
10885        };
10886        assert_eq!(canceled.client_order_id, selected_order.client_order_id());
10887        assert_eq!(canceled.strategy_id, selected_strategy);
10888        assert_eq!(canceled.account_id, Some(selected_account));
10889        let cache = cache.borrow();
10890        assert_eq!(
10891            cache
10892                .order(&selected_order.client_order_id())
10893                .unwrap()
10894                .status(),
10895            OrderStatus::Canceled
10896        );
10897        assert_eq!(
10898            cache
10899                .order(&other_order.client_order_id())
10900                .unwrap()
10901                .status(),
10902            OrderStatus::Submitted
10903        );
10904    }
10905
10906    #[rstest]
10907    fn test_process_cancel_all_excluding_leaves_excluded_orders_untouched() {
10908        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
10909        let instrument_id = instrument.id();
10910        let cache = Rc::new(RefCell::new(Cache::default()));
10911        let clock = Rc::new(RefCell::new(VirtualClock::new()));
10912        let mut engine = OrderMatchingEngine::new(
10913            instrument,
10914            1,
10915            FillModelHandle::default(),
10916            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
10917            BookType::L1_MBP,
10918            OmsType::Netting,
10919            AccountType::Margin,
10920            clock,
10921            Rc::clone(&cache),
10922            Default::default(),
10923        );
10924        let account_id = AccountId::from("ACCOUNT-001");
10925        let strategy_id = StrategyId::from("STRATEGY-001");
10926        let received = OrderTestBuilder::new(OrderType::Limit)
10927            .strategy_id(strategy_id)
10928            .instrument_id(instrument_id)
10929            .client_order_id(ClientOrderId::from("O-RECEIVED"))
10930            .side(OrderSide::Buy)
10931            .price(Price::from("1400.00"))
10932            .quantity(Quantity::from("1.000"))
10933            .build();
10934        let in_transit = OrderTestBuilder::new(OrderType::Limit)
10935            .strategy_id(strategy_id)
10936            .instrument_id(instrument_id)
10937            .client_order_id(ClientOrderId::from("O-IN-TRANSIT"))
10938            .side(OrderSide::Buy)
10939            .price(Price::from("1300.00"))
10940            .quantity(Quantity::from("1.000"))
10941            .build();
10942        {
10943            let mut cache = cache.borrow_mut();
10944            cache
10945                .add_order(received.clone(), None, None, false)
10946                .unwrap();
10947            cache
10948                .add_order(in_transit.clone(), None, None, false)
10949                .unwrap();
10950            cache
10951                .update_order(&TestOrderEventStubs::submitted(&received, account_id))
10952                .unwrap();
10953            cache
10954                .update_order(&TestOrderEventStubs::submitted(&in_transit, account_id))
10955                .unwrap();
10956        }
10957
10958        let events = Rc::new(RefCell::new(Vec::new()));
10959        let events_handler = Rc::clone(&events);
10960        let event_cache = Rc::clone(&cache);
10961        engine.set_event_handler(Rc::new(move |event| {
10962            event_cache.borrow_mut().update_order(&event).unwrap();
10963            events_handler.borrow_mut().push(event);
10964        }));
10965        let command = CancelAllOrders::new(
10966            TraderId::from("TRADER-001"),
10967            None,
10968            StrategyId::from("CALLER-001"),
10969            instrument_id,
10970            None,
10971            UUID4::new(),
10972            UnixNanos::default(),
10973            None,
10974            None,
10975        );
10976
10977        engine.process_cancel_all_excluding(&command, account_id, &[in_transit.client_order_id()]);
10978
10979        let events = events.borrow();
10980        assert_eq!(
10981            events.len(),
10982            1,
10983            "expected one OrderCanceled, was {events:?}"
10984        );
10985        let OrderEventAny::Canceled(canceled) = &events[0] else {
10986            panic!("Expected OrderCanceled, was {:?}", events[0]);
10987        };
10988        assert_eq!(canceled.client_order_id, received.client_order_id());
10989        assert_eq!(
10990            cache
10991                .borrow()
10992                .order(&in_transit.client_order_id())
10993                .unwrap()
10994                .status(),
10995            OrderStatus::Submitted,
10996            "an excluded order must be left untouched",
10997        );
10998    }
10999
11000    #[rstest]
11001    fn test_process_cancel_all_excluding_spares_an_excluded_contingent_order() {
11002        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
11003        let instrument_id = instrument.id();
11004        let cache = Rc::new(RefCell::new(Cache::default()));
11005        let clock = Rc::new(RefCell::new(VirtualClock::new()));
11006        let mut engine = OrderMatchingEngine::new(
11007            instrument,
11008            1,
11009            FillModelHandle::default(),
11010            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
11011            BookType::L1_MBP,
11012            OmsType::Netting,
11013            AccountType::Margin,
11014            clock,
11015            Rc::clone(&cache),
11016            Default::default(),
11017        );
11018        assert!(engine.config.support_contingent_orders);
11019        let account_id = AccountId::from("ACCOUNT-001");
11020        let strategy_id = StrategyId::from("STRATEGY-001");
11021        let received_id = ClientOrderId::from("O-RECEIVED");
11022        let in_transit_id = ClientOrderId::from("O-IN-TRANSIT");
11023        let received = OrderTestBuilder::new(OrderType::Limit)
11024            .strategy_id(strategy_id)
11025            .instrument_id(instrument_id)
11026            .client_order_id(received_id)
11027            .side(OrderSide::Buy)
11028            .price(Price::from("1400.00"))
11029            .quantity(Quantity::from("1.000"))
11030            .contingency_type(ContingencyType::Oco)
11031            .linked_order_ids(vec![in_transit_id])
11032            .build();
11033        let in_transit = OrderTestBuilder::new(OrderType::Limit)
11034            .strategy_id(strategy_id)
11035            .instrument_id(instrument_id)
11036            .client_order_id(in_transit_id)
11037            .side(OrderSide::Buy)
11038            .price(Price::from("1300.00"))
11039            .quantity(Quantity::from("1.000"))
11040            .contingency_type(ContingencyType::Oco)
11041            .linked_order_ids(vec![received_id])
11042            .build();
11043        {
11044            let mut cache = cache.borrow_mut();
11045            cache
11046                .add_order(received.clone(), None, None, false)
11047                .unwrap();
11048            cache
11049                .add_order(in_transit.clone(), None, None, false)
11050                .unwrap();
11051            cache
11052                .update_order(&TestOrderEventStubs::submitted(&received, account_id))
11053                .unwrap();
11054            cache
11055                .update_order(&TestOrderEventStubs::submitted(&in_transit, account_id))
11056                .unwrap();
11057        }
11058
11059        let events = Rc::new(RefCell::new(Vec::new()));
11060        let events_handler = Rc::clone(&events);
11061        let event_cache = Rc::clone(&cache);
11062        engine.set_event_handler(Rc::new(move |event| {
11063            event_cache.borrow_mut().update_order(&event).unwrap();
11064            events_handler.borrow_mut().push(event);
11065        }));
11066        let command = CancelAllOrders::new(
11067            TraderId::from("TRADER-001"),
11068            None,
11069            StrategyId::from("CALLER-001"),
11070            instrument_id,
11071            None,
11072            UUID4::new(),
11073            UnixNanos::default(),
11074            None,
11075            None,
11076        );
11077
11078        engine.process_cancel_all_excluding(&command, account_id, &[in_transit_id]);
11079
11080        let events = events.borrow();
11081        assert_eq!(
11082            events.len(),
11083            1,
11084            "expected one OrderCanceled, was {events:?}"
11085        );
11086        let OrderEventAny::Canceled(canceled) = &events[0] else {
11087            panic!("Expected OrderCanceled, was {:?}", events[0]);
11088        };
11089        assert_eq!(canceled.client_order_id, received_id);
11090        assert_eq!(
11091            cache.borrow().order(&in_transit_id).unwrap().status(),
11092            OrderStatus::Submitted,
11093            "canceling its OCO sibling must not cancel an excluded order",
11094        );
11095    }
11096
11097    fn collision_engine() -> (OrderMatchingEngine, Rc<RefCell<Cache>>, VenueOrderId) {
11098        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
11099        let cache = Rc::new(RefCell::new(Cache::default()));
11100        let venue_order_id = VenueOrderId::from(format!("{}-1-1", instrument.id().venue));
11101        cache
11102            .borrow_mut()
11103            .add_venue_order_id(&ClientOrderId::from("O-OWNER"), &venue_order_id, false)
11104            .unwrap();
11105        let engine = OrderMatchingEngine::new(
11106            instrument,
11107            1,
11108            FillModelHandle::default(),
11109            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
11110            BookType::L1_MBP,
11111            OmsType::Netting,
11112            AccountType::Margin,
11113            Rc::new(RefCell::new(VirtualClock::new())),
11114            Rc::clone(&cache),
11115            Default::default(),
11116        );
11117
11118        (engine, cache, venue_order_id)
11119    }
11120
11121    #[rstest]
11122    #[case(OrderType::Market)]
11123    #[case(OrderType::MarketToLimit)]
11124    fn test_market_collision_probes_and_fills_with_default_ack_config(
11125        #[case] order_type: OrderType,
11126    ) {
11127        let (mut engine, cache, venue_order_id) = collision_engine();
11128        assert!(!engine.config.use_market_order_acks);
11129        let quote = QuoteTick::new(
11130            engine.instrument.id(),
11131            Price::from("1499.00"),
11132            Price::from("1500.00"),
11133            Quantity::from("10.000"),
11134            Quantity::from("10.000"),
11135            UnixNanos::default(),
11136            UnixNanos::default(),
11137        );
11138        engine.process_quote_tick(&quote);
11139        let events = Rc::new(RefCell::new(Vec::new()));
11140        let events_handler = Rc::clone(&events);
11141        engine.set_event_handler(Rc::new(move |event| {
11142            events_handler.borrow_mut().push(event);
11143        }));
11144        let mut order = OrderTestBuilder::new(order_type)
11145            .instrument_id(engine.instrument.id())
11146            .client_order_id(ClientOrderId::from("O-CLAIMANT"))
11147            .side(OrderSide::Buy)
11148            .quantity(Quantity::from("1.000"))
11149            .submit(true)
11150            .build();
11151
11152        process_cached_order(&mut engine, &mut order, AccountId::from("ACCOUNT-001"));
11153
11154        assert!(
11155            !events
11156                .borrow()
11157                .iter()
11158                .any(|event| matches!(event, OrderEventAny::Rejected(_)))
11159        );
11160        assert!(
11161            events
11162                .borrow()
11163                .iter()
11164                .any(|event| matches!(event, OrderEventAny::Filled(_)))
11165        );
11166        assert!(cache.borrow().order_exists(&order.client_order_id()));
11167        assert_eq!(
11168            cache.borrow().client_order_id(&venue_order_id),
11169            Some(&ClientOrderId::from("O-OWNER"))
11170        );
11171        assert_eq!(
11172            cache.borrow().venue_order_id(&order.client_order_id()),
11173            Some(&VenueOrderId::from(format!("{}-1-2", engine.venue)))
11174        );
11175    }
11176
11177    struct RecordingFeeModel {
11178        calls: Rc<Cell<u32>>,
11179        commission: Money,
11180    }
11181
11182    impl FeeModel for RecordingFeeModel {
11183        fn get_commission(
11184            &self,
11185            _order: &OrderAny,
11186            _fill_quantity: Quantity,
11187            _fill_px: Price,
11188            _instrument: &InstrumentAny,
11189        ) -> anyhow::Result<Money> {
11190            self.calls.set(self.calls.get() + 1);
11191            Ok(self.commission)
11192        }
11193    }
11194
11195    struct FailingFeeModel;
11196
11197    impl FeeModel for FailingFeeModel {
11198        fn get_commission(
11199            &self,
11200            _order: &OrderAny,
11201            _fill_quantity: Quantity,
11202            _fill_px: Price,
11203            _instrument: &InstrumentAny,
11204        ) -> anyhow::Result<Money> {
11205            Err(anyhow::anyhow!("fee model failed"))
11206        }
11207    }
11208
11209    #[rstest]
11210    fn test_custom_fill_model_handle_is_called_by_market_fill() {
11211        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
11212        let cache = Rc::new(RefCell::new(Cache::default()));
11213        let clock = Rc::new(RefCell::new(VirtualClock::new()));
11214        let calls = Rc::new(Cell::new(0));
11215        let fill_model = FillModelHandle::new(RecordingFillModel {
11216            calls: Rc::clone(&calls),
11217        });
11218        let mut engine = OrderMatchingEngine::new(
11219            instrument.clone(),
11220            1,
11221            fill_model,
11222            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
11223            BookType::L1_MBP,
11224            OmsType::Netting,
11225            AccountType::Margin,
11226            clock,
11227            cache,
11228            Default::default(),
11229        );
11230        let quote = QuoteTick::new(
11231            instrument.id(),
11232            Price::from("1500.00"),
11233            Price::from("1501.00"),
11234            Quantity::from("10.000"),
11235            Quantity::from("10.000"),
11236            UnixNanos::default(),
11237            UnixNanos::default(),
11238        );
11239        engine.process_quote_tick(&quote);
11240
11241        let mut order = OrderTestBuilder::new(OrderType::Market)
11242            .instrument_id(instrument.id())
11243            .side(OrderSide::Buy)
11244            .quantity(Quantity::from("1.000"))
11245            .submit(true)
11246            .build();
11247        process_cached_order(&mut engine, &mut order, AccountId::from("ACCOUNT-001"));
11248
11249        assert_eq!(calls.get(), 1);
11250    }
11251
11252    #[rstest]
11253    fn test_l1_depth_skips_padding_for_last_quote_tracking() {
11254        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
11255        let cache = Rc::new(RefCell::new(Cache::default()));
11256        let clock = Rc::new(RefCell::new(VirtualClock::new()));
11257        let mut engine = OrderMatchingEngine::new(
11258            instrument.clone(),
11259            1,
11260            FillModelHandle::default(),
11261            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
11262            BookType::L1_MBP,
11263            OmsType::Netting,
11264            AccountType::Margin,
11265            clock,
11266            cache,
11267            Default::default(),
11268        );
11269        let mut bids = [BookOrder::default(); DEPTH10_LEN];
11270        let mut asks = [BookOrder::default(); DEPTH10_LEN];
11271        bids[1] = BookOrder::new(
11272            OrderSide::Buy,
11273            Price::from("1499.00"),
11274            Quantity::from("1.000"),
11275            1,
11276        );
11277        asks[0] = BookOrder::new(
11278            OrderSide::Sell,
11279            Price::from("1500.00"),
11280            Quantity::from("1.000"),
11281            2,
11282        );
11283
11284        let depth = OrderBookDepth::new(
11285            instrument.id(),
11286            bids,
11287            asks,
11288            [0; DEPTH10_LEN],
11289            [0; DEPTH10_LEN],
11290            0,
11291            0,
11292            UnixNanos::from(1_u64),
11293            UnixNanos::from(1_u64),
11294        );
11295        engine.process_order_book_depth(&depth).unwrap();
11296
11297        assert_eq!(engine.last_quote_bid, Some(Price::from("1499.00")));
11298        assert_eq!(engine.last_quote_ask, Some(Price::from("1500.00")));
11299
11300        let depth_without_bid = OrderBookDepth::new(
11301            instrument.id(),
11302            [BookOrder::default(); DEPTH10_LEN],
11303            asks,
11304            [0; DEPTH10_LEN],
11305            [0; DEPTH10_LEN],
11306            0,
11307            1,
11308            UnixNanos::from(2_u64),
11309            UnixNanos::from(2_u64),
11310        );
11311        engine.process_order_book_depth(&depth_without_bid).unwrap();
11312
11313        assert_eq!(engine.last_quote_bid, None);
11314        assert_eq!(engine.last_quote_ask, Some(Price::from("1500.00")));
11315    }
11316
11317    struct TestBookFillModel {
11318        book: OrderBook,
11319    }
11320
11321    impl FillModel for TestBookFillModel {
11322        fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
11323            Ok(true)
11324        }
11325
11326        fn is_slipped(&mut self) -> anyhow::Result<bool> {
11327            Ok(false)
11328        }
11329
11330        fn get_orderbook_for_fill_simulation(
11331            &mut self,
11332            _instrument: &InstrumentAny,
11333            _order: &OrderAny,
11334            _best_bid: Option<Price>,
11335            _best_ask: Option<Price>,
11336        ) -> anyhow::Result<Option<OrderBook>> {
11337            Ok(Some(self.book.clone()))
11338        }
11339    }
11340
11341    fn set_custom_fill_book(engine: &mut OrderMatchingEngine, size: &str, outside_limit: bool) {
11342        let mut book = OrderBook::new(engine.instrument.id(), BookType::L2_MBP);
11343        let size = Quantity::from(size);
11344        if size.is_positive() {
11345            for (side, price) in [
11346                (
11347                    OrderSide::Buy,
11348                    if outside_limit { "1498.00" } else { "1499.00" },
11349                ),
11350                (
11351                    OrderSide::Sell,
11352                    if outside_limit { "1502.00" } else { "1501.00" },
11353                ),
11354            ] {
11355                book.add(
11356                    BookOrder::new(side, Price::from(price), size, 0),
11357                    0,
11358                    1,
11359                    UnixNanos::from(1),
11360                );
11361            }
11362        }
11363
11364        engine.set_fill_model(FillModelHandle::new(TestBookFillModel { book }));
11365    }
11366
11367    fn custom_book_engine(
11368        book_type: BookType,
11369        liquidity_consumption: bool,
11370        use_market_order_acks: bool,
11371    ) -> (OrderMatchingEngine, Rc<RefCell<Vec<OrderEventAny>>>) {
11372        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
11373        let cache = Rc::new(RefCell::new(Cache::default()));
11374
11375        let mut engine = OrderMatchingEngine::new(
11376            instrument,
11377            1,
11378            FillModelHandle::default(),
11379            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
11380            book_type,
11381            OmsType::Netting,
11382            AccountType::Margin,
11383            Rc::new(RefCell::new(VirtualClock::new())),
11384            cache.clone(),
11385            OrderMatchingEngineConfig {
11386                liquidity_consumption,
11387                use_market_order_acks,
11388                ..Default::default()
11389            },
11390        );
11391
11392        for (side, price) in [(OrderSide::Buy, "1499.00"), (OrderSide::Sell, "1501.00")] {
11393            engine
11394                .process_order_book_delta(&OrderBookDelta::new(
11395                    engine.instrument.id(),
11396                    BookAction::Add,
11397                    BookOrder::new(side, Price::from(price), Quantity::from("10.000"), 0),
11398                    RecordFlag::F_LAST as u8,
11399                    1,
11400                    UnixNanos::from(1),
11401                    UnixNanos::from(1),
11402                ))
11403                .unwrap();
11404        }
11405
11406        let events = Rc::new(RefCell::new(Vec::new()));
11407        let saved_events = events.clone();
11408        engine.set_event_handler(Rc::new(move |event| {
11409            cache.borrow_mut().update_order(&event).unwrap();
11410            saved_events.borrow_mut().push(event);
11411        }));
11412
11413        (engine, events)
11414    }
11415
11416    #[rstest]
11417    fn test_canceled_fok_reverts_only_crossed_consumption_levels(
11418        #[values(OrderType::Market, OrderType::Limit)] order_type: OrderType,
11419    ) {
11420        let (mut engine, events) = custom_book_engine(BookType::L2_MBP, true, false);
11421        engine
11422            .process_order_book_delta(&OrderBookDelta::new(
11423                engine.instrument.id(),
11424                BookAction::Add,
11425                BookOrder::new(
11426                    OrderSide::Sell,
11427                    Price::from("1502.00"),
11428                    Quantity::from("5.000"),
11429                    0,
11430                ),
11431                RecordFlag::F_LAST as u8,
11432                2,
11433                UnixNanos::from(2),
11434                UnixNanos::from(2),
11435            ))
11436            .unwrap();
11437
11438        // Prior consumption leaves 6 of 10 at 1501, and 1600 is never crossed
11439        let size = |qty: &str| Quantity::from(qty).raw();
11440        engine
11441            .ask_consumption
11442            .insert(Price::from("1600.00").raw(), (size("7.000"), size("3.000")));
11443        engine.ask_consumption.insert(
11444            Price::from("1501.00").raw(),
11445            (size("10.000"), size("4.000")),
11446        );
11447        let levels_before: Vec<_> = engine.ask_consumption.clone().into_iter().collect();
11448
11449        // Crossing 1501 and 1502 gives 11 of the 15 required, so the FOK is canceled
11450        let mut builder = OrderTestBuilder::new(order_type);
11451        builder
11452            .instrument_id(engine.instrument.id())
11453            .side(OrderSide::Buy)
11454            .quantity(Quantity::from("15.000"))
11455            .time_in_force(TimeInForce::Fok)
11456            .submit(true);
11457
11458        if order_type == OrderType::Limit {
11459            builder.price(Price::from("1502.00"));
11460        }
11461        let mut order = builder.build();
11462        engine
11463            .cache
11464            .borrow_mut()
11465            .add_order(order.clone(), None, None, false)
11466            .unwrap();
11467        engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
11468
11469        assert!(
11470            !events
11471                .borrow()
11472                .iter()
11473                .any(|event| matches!(event, OrderEventAny::Filled(_)))
11474        );
11475        let levels_after: Vec<_> = engine.ask_consumption.clone().into_iter().collect();
11476        assert_eq!(levels_after, levels_before);
11477        assert!(engine.bid_consumption.is_empty());
11478        assert_eq!(engine.trade_consumption, 0);
11479    }
11480
11481    #[rstest]
11482    #[case::historical_gtc((None, TimeInForce::Gtc, "3.000", OrderStatus::Filled))]
11483    #[case::historical_ioc((None, TimeInForce::Ioc, "3.000", OrderStatus::Filled))]
11484    #[case::historical_fok((None, TimeInForce::Fok, "3.000", OrderStatus::Filled))]
11485    #[case::custom_fok((Some("3.000"), TimeInForce::Fok, "3.000", OrderStatus::Filled))]
11486    #[case::empty_gtc((Some("0.000"), TimeInForce::Gtc, "0.000", OrderStatus::Accepted))]
11487    #[case::empty_ioc((Some("0.000"), TimeInForce::Ioc, "0.000", OrderStatus::Canceled))]
11488    #[case::empty_fok((Some("0.000"), TimeInForce::Fok, "0.000", OrderStatus::Canceled))]
11489    #[case::partial_gtc((Some("1.000"), TimeInForce::Gtc, "1.000", OrderStatus::PartiallyFilled))]
11490    #[case::partial_ioc((Some("1.000"), TimeInForce::Ioc, "1.000", OrderStatus::Canceled))]
11491    #[case::partial_fok((Some("1.000"), TimeInForce::Fok, "0.000", OrderStatus::Canceled))]
11492    fn test_l2_custom_book_order_lifecycle(
11493        #[case] scenario: (Option<&str>, TimeInForce, &str, OrderStatus),
11494        #[values(OrderType::Market, OrderType::Limit)] order_type: OrderType,
11495        #[values(OrderSide::Buy, OrderSide::Sell)] side: OrderSide,
11496        #[values(false, true)] liquidity_consumption: bool,
11497        #[values(false, true)] use_market_order_acks: bool,
11498        #[values(false, true)] one_sided: bool,
11499    ) {
11500        let (custom_size, time_in_force, expected_qty, expected_status) = scenario;
11501        let (mut engine, events) = custom_book_engine(
11502            BookType::L2_MBP,
11503            liquidity_consumption,
11504            use_market_order_acks,
11505        );
11506
11507        if one_sided {
11508            match side {
11509                OrderSide::Buy => engine.book.clear_bids(2, UnixNanos::from(2)),
11510                OrderSide::Sell => engine.book.clear_asks(2, UnixNanos::from(2)),
11511            }
11512
11513            engine.iterate(UnixNanos::from(2), AggressorSide::NoAggressor);
11514        }
11515
11516        if let Some(size) = custom_size {
11517            set_custom_fill_book(&mut engine, size, false);
11518        }
11519
11520        let price = Price::from(if side == OrderSide::Buy {
11521            "1501.00"
11522        } else {
11523            "1499.00"
11524        });
11525
11526        let mut order = OrderTestBuilder::new(order_type)
11527            .instrument_id(engine.instrument.id())
11528            .side(side)
11529            .price(price)
11530            .quantity(Quantity::from("3.000"))
11531            .time_in_force(time_in_force)
11532            .submit(true)
11533            .build();
11534        engine
11535            .cache
11536            .borrow_mut()
11537            .add_order(order.clone(), None, None, false)
11538            .unwrap();
11539
11540        if matches!((order_type, custom_size), (OrderType::Market, Some(_))) {
11541            assert!(
11542                engine
11543                    .determine_market_fill_model_price_and_volume(&order)
11544                    .unwrap()
11545                    .1
11546            );
11547        }
11548
11549        engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
11550
11551        let expected_qty = Quantity::from(expected_qty);
11552
11553        let expected_status = match (order_type, expected_status, use_market_order_acks) {
11554            (OrderType::Market, OrderStatus::Accepted, false) => OrderStatus::Rejected,
11555            (OrderType::Market, OrderStatus::Accepted, true) => OrderStatus::Canceled,
11556            _ => expected_status,
11557        };
11558
11559        let fills: Vec<_> = events
11560            .borrow()
11561            .iter()
11562            .filter_map(|event| match event {
11563                OrderEventAny::Filled(fill) => Some((fill.order_side, fill.last_px, fill.last_qty)),
11564                _ => None,
11565            })
11566            .collect();
11567
11568        let expected_fills = if expected_qty.is_zero() {
11569            Vec::new()
11570        } else {
11571            vec![(side, price, expected_qty)]
11572        };
11573
11574        assert_eq!(fills, expected_fills);
11575        let cached = engine
11576            .cache
11577            .borrow()
11578            .order(&order.client_order_id())
11579            .unwrap()
11580            .clone();
11581        assert_eq!(cached.status(), expected_status);
11582        assert_eq!(cached.filled_qty(), expected_qty);
11583        assert_eq!(cached.leaves_qty(), Quantity::from("3.000") - expected_qty);
11584        let historical_size = Some(Quantity::from("10.000"));
11585
11586        let expected_sizes = match (one_sided, side) {
11587            (true, OrderSide::Buy) => (None, historical_size),
11588            (true, OrderSide::Sell) => (historical_size, None),
11589            (false, _) => (historical_size, historical_size),
11590        };
11591
11592        assert_eq!(
11593            (engine.book.best_bid_size(), engine.book.best_ask_size()),
11594            expected_sizes,
11595        );
11596
11597        if custom_size.is_some() {
11598            assert!(engine.bid_consumption.is_empty());
11599            assert!(engine.ask_consumption.is_empty());
11600        }
11601
11602        if expected_qty.is_zero() {
11603            assert!(engine.cached_filled_qty.is_empty());
11604            assert!(engine.pending_fills.is_empty());
11605            assert_eq!(
11606                engine
11607                    .cache
11608                    .borrow()
11609                    .positions_total_count(None, None, None, None, None),
11610                0
11611            );
11612        }
11613    }
11614
11615    #[rstest]
11616    #[case::empty("0.000", false, "0.000")]
11617    #[case::outside_limit("3.000", true, "0.000")]
11618    #[case::partial("1.000", false, "1.000")]
11619    fn test_l2_custom_book_limit_waits_for_replenishment(
11620        #[case] custom_size: &str,
11621        #[case] outside_limit: bool,
11622        #[case] initial_filled_qty: &str,
11623        #[values(OrderSide::Buy, OrderSide::Sell)] side: OrderSide,
11624        #[values(TimeInForce::Gtc, TimeInForce::Ioc, TimeInForce::Fok)] time_in_force: TimeInForce,
11625        #[values(false, true)] liquidity_consumption: bool,
11626    ) {
11627        let (mut engine, events) =
11628            custom_book_engine(BookType::L2_MBP, liquidity_consumption, false);
11629        set_custom_fill_book(&mut engine, custom_size, outside_limit);
11630
11631        let price = Price::from(if side == OrderSide::Buy {
11632            "1501.00"
11633        } else {
11634            "1499.00"
11635        });
11636
11637        let mut order = OrderTestBuilder::new(OrderType::Limit)
11638            .instrument_id(engine.instrument.id())
11639            .side(side)
11640            .price(price)
11641            .quantity(Quantity::from("3.000"))
11642            .time_in_force(time_in_force)
11643            .submit(true)
11644            .build();
11645        let id = order.client_order_id();
11646        engine
11647            .cache
11648            .borrow_mut()
11649            .add_order(order.clone(), None, None, false)
11650            .unwrap();
11651        engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
11652
11653        let initial_filled_qty = if time_in_force == TimeInForce::Fok {
11654            Quantity::from("0.000")
11655        } else {
11656            Quantity::from(initial_filled_qty)
11657        };
11658
11659        assert_eq!(
11660            engine.cache.borrow().order(&id).unwrap().filled_qty(),
11661            initial_filled_qty
11662        );
11663        events.borrow_mut().clear();
11664
11665        set_custom_fill_book(&mut engine, "0.000", false);
11666        engine.iterate(UnixNanos::from(2), AggressorSide::NoAggressor);
11667        assert!(events.borrow().is_empty());
11668        assert_eq!(engine.order_exists(id), time_in_force == TimeInForce::Gtc);
11669
11670        set_custom_fill_book(&mut engine, "3.000", false);
11671        engine.iterate(UnixNanos::from(3), AggressorSide::NoAggressor);
11672
11673        let cache = engine.cache.borrow();
11674        let cached = cache.order(&id).unwrap();
11675        assert_eq!(cached.price(), Some(price));
11676        assert!(engine.bid_consumption.is_empty());
11677        assert!(engine.ask_consumption.is_empty());
11678
11679        if time_in_force == TimeInForce::Gtc {
11680            assert_eq!(cached.status(), OrderStatus::Filled);
11681            assert_eq!(cached.filled_qty(), Quantity::from("3.000"));
11682            let events = events.borrow();
11683            assert_eq!(events.len(), 1);
11684
11685            let OrderEventAny::Filled(fill) = &events[0] else {
11686                panic!("Expected fill")
11687            };
11688
11689            assert_eq!(fill.last_px, price);
11690            assert_eq!(fill.last_qty, Quantity::from("3.000") - initial_filled_qty);
11691        } else {
11692            assert_eq!(cached.status(), OrderStatus::Canceled);
11693            assert_eq!(cached.filled_qty(), initial_filled_qty);
11694            assert!(events.borrow().is_empty());
11695        }
11696    }
11697
11698    #[rstest]
11699    fn test_l2_custom_book_empty_market_before_acceptance(
11700        #[values(
11701            OrderStatus::Initialized,
11702            OrderStatus::Released,
11703            OrderStatus::Submitted
11704        )]
11705        status: OrderStatus,
11706        #[values(TimeInForce::Gtc, TimeInForce::Ioc, TimeInForce::Fok)] time_in_force: TimeInForce,
11707        #[values(OrderSide::Buy, OrderSide::Sell)] side: OrderSide,
11708        #[values(false, true)] liquidity_consumption: bool,
11709    ) {
11710        let (mut engine, events) =
11711            custom_book_engine(BookType::L2_MBP, liquidity_consumption, false);
11712        set_custom_fill_book(&mut engine, "0.000", false);
11713        let mut order = OrderTestBuilder::new(OrderType::Market)
11714            .instrument_id(engine.instrument.id())
11715            .side(side)
11716            .quantity(Quantity::from("3.000"))
11717            .time_in_force(time_in_force)
11718            .submit(status == OrderStatus::Submitted)
11719            .build();
11720
11721        if status == OrderStatus::Released {
11722            order
11723                .apply(OrderEventAny::Released(OrderReleased::new(
11724                    order.trader_id(),
11725                    order.strategy_id(),
11726                    order.instrument_id(),
11727                    order.client_order_id(),
11728                    Price::from("1500.00"),
11729                    UUID4::new(),
11730                    UnixNanos::default(),
11731                    UnixNanos::default(),
11732                )))
11733                .unwrap();
11734        }
11735
11736        engine
11737            .cache
11738            .borrow_mut()
11739            .add_order(order.clone(), None, None, false)
11740            .unwrap();
11741
11742        engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
11743
11744        let expected_status =
11745            if time_in_force == TimeInForce::Gtc && status != OrderStatus::Released {
11746                OrderStatus::Rejected
11747            } else {
11748                OrderStatus::Canceled
11749            };
11750
11751        let events = events.borrow();
11752        assert_eq!(events.len(), 1);
11753        assert!(matches!(
11754            &events[0],
11755            OrderEventAny::Rejected(_) | OrderEventAny::Canceled(_)
11756        ));
11757        let cache = engine.cache.borrow();
11758        let cached = cache.order(&order.client_order_id()).unwrap();
11759        assert_eq!(cached.status(), expected_status);
11760        assert!(cached.filled_qty().is_zero());
11761        assert_eq!(cache.positions_total_count(None, None, None, None, None), 0);
11762        assert!(engine.bid_consumption.is_empty());
11763        assert!(engine.ask_consumption.is_empty());
11764        assert!(engine.cached_filled_qty.is_empty());
11765    }
11766
11767    #[rstest]
11768    fn test_l1_custom_book_partial_market_respects_liquidity(
11769        #[values(OrderSide::Buy, OrderSide::Sell)] side: OrderSide,
11770        #[values(false, true)] one_sided: bool,
11771    ) {
11772        let (mut engine, events) = custom_book_engine(BookType::L1_MBP, false, false);
11773        engine.process_quote_tick(&QuoteTick::new(
11774            engine.instrument.id(),
11775            Price::from("1499.00"),
11776            Price::from("1501.00"),
11777            Quantity::from("10.000"),
11778            Quantity::from("10.000"),
11779            UnixNanos::from(1),
11780            UnixNanos::from(1),
11781        ));
11782
11783        if one_sided {
11784            match side {
11785                OrderSide::Buy => {
11786                    engine.book.clear_bids(2, UnixNanos::from(2));
11787                    engine.core.bid = None;
11788                }
11789                OrderSide::Sell => {
11790                    engine.book.clear_asks(2, UnixNanos::from(2));
11791                    engine.core.ask = None;
11792                }
11793            }
11794
11795            engine.iterate(UnixNanos::from(2), AggressorSide::NoAggressor);
11796        }
11797
11798        set_custom_fill_book(&mut engine, "1.000", false);
11799        let mut order = OrderTestBuilder::new(OrderType::Market)
11800            .instrument_id(engine.instrument.id())
11801            .side(side)
11802            .quantity(Quantity::from("3.000"))
11803            .time_in_force(TimeInForce::Gtc)
11804            .submit(true)
11805            .build();
11806        engine
11807            .cache
11808            .borrow_mut()
11809            .add_order(order.clone(), None, None, false)
11810            .unwrap();
11811
11812        engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
11813
11814        let fills: Vec<_> = events
11815            .borrow()
11816            .iter()
11817            .filter_map(|event| match event {
11818                OrderEventAny::Filled(fill) => Some((fill.last_px, fill.last_qty)),
11819                _ => None,
11820            })
11821            .collect();
11822
11823        let price = Price::from(if side == OrderSide::Buy {
11824            "1501.00"
11825        } else {
11826            "1499.00"
11827        });
11828
11829        assert_eq!(fills, vec![(price, Quantity::from("1.000"))]);
11830        let cache = engine.cache.borrow();
11831        let cached = cache.order(&order.client_order_id()).unwrap();
11832        assert_eq!(cached.status(), OrderStatus::PartiallyFilled);
11833        assert_eq!(cached.filled_qty(), Quantity::from("1.000"));
11834        assert_eq!(cached.leaves_qty(), Quantity::from("2.000"));
11835    }
11836
11837    struct RecordingFillModel {
11838        calls: Rc<Cell<u32>>,
11839    }
11840
11841    impl FillModel for RecordingFillModel {
11842        fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
11843            Ok(true)
11844        }
11845
11846        fn is_slipped(&mut self) -> anyhow::Result<bool> {
11847            Ok(false)
11848        }
11849
11850        fn get_orderbook_for_fill_simulation(
11851            &mut self,
11852            _instrument: &InstrumentAny,
11853            _order: &OrderAny,
11854            _best_bid: Option<Price>,
11855            _best_ask: Option<Price>,
11856        ) -> anyhow::Result<Option<OrderBook>> {
11857            self.calls.set(self.calls.get() + 1);
11858            Ok(None)
11859        }
11860    }
11861
11862    #[rstest]
11863    fn test_fee_underlying_price_uses_valid_cached_greeks_price() {
11864        let instrument = InstrumentAny::CryptoOption(crypto_option_btc_deribit(
11865            3,
11866            1,
11867            Price::from("0.001"),
11868            Quantity::from("0.1"),
11869        ));
11870        let cache = Rc::new(RefCell::new(Cache::default()));
11871        cache.borrow_mut().add_option_greeks(OptionGreeks {
11872            instrument_id: instrument.id(),
11873            underlying_price: Some(50_000.0),
11874            ..Default::default()
11875        });
11876        let clock = Rc::new(RefCell::new(VirtualClock::new()));
11877        let engine = OrderMatchingEngine::new(
11878            instrument,
11879            1,
11880            FillModelHandle::default(),
11881            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
11882            BookType::L1_MBP,
11883            OmsType::Netting,
11884            AccountType::Margin,
11885            clock,
11886            cache,
11887            Default::default(),
11888        );
11889
11890        let price = engine
11891            .fee_underlying_price()
11892            .unwrap()
11893            .expect("expected underlying price");
11894
11895        assert_eq!(price.precision, FIXED_PRECISION);
11896        assert_eq!(price.as_decimal(), Decimal::from(50_000));
11897    }
11898
11899    #[rstest]
11900    fn test_fee_underlying_price_rejects_invalid_cached_greeks_price() {
11901        let instrument = InstrumentAny::CryptoOption(crypto_option_btc_deribit(
11902            3,
11903            1,
11904            Price::from("0.001"),
11905            Quantity::from("0.1"),
11906        ));
11907        let cache = Rc::new(RefCell::new(Cache::default()));
11908        cache.borrow_mut().add_option_greeks(OptionGreeks {
11909            instrument_id: instrument.id(),
11910            underlying_price: Some(f64::NAN),
11911            ..Default::default()
11912        });
11913        let clock = Rc::new(RefCell::new(VirtualClock::new()));
11914        let engine = OrderMatchingEngine::new(
11915            instrument,
11916            1,
11917            FillModelHandle::default(),
11918            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
11919            BookType::L1_MBP,
11920            OmsType::Netting,
11921            AccountType::Margin,
11922            clock,
11923            cache,
11924            Default::default(),
11925        );
11926
11927        let error = engine.fee_underlying_price().unwrap_err();
11928
11929        assert_eq!(
11930            error,
11931            CorrectnessError::InvalidValue {
11932                param: "value".to_string(),
11933                value: "NaN".to_string(),
11934                type_name: "f64",
11935            }
11936        );
11937    }
11938
11939    #[rstest]
11940    fn test_bar_tick_sizes_divisible() {
11941        // precision=3, units=100_000: exactly divisible by 4, no rounding.
11942        let volume = Quantity::from("100.000");
11943        let increment = Quantity::from("0.001");
11944        let sizes = BarTickSizes::from_volume(volume, increment);
11945        assert_eq!(sizes.open, Quantity::from("25.000"));
11946        assert_eq!(sizes.high, Quantity::from("25.000"));
11947        assert_eq!(sizes.low, Quantity::from("25.000"));
11948        assert_eq!(sizes.close, Quantity::from("25.000"));
11949        assert_valid_bar_tick_sizes(volume, increment);
11950    }
11951
11952    #[rstest]
11953    fn test_bar_tick_sizes_indivisible_with_remainder() {
11954        // precision=2, units=5: quarter_units=1, remainder=1; close carries 2 units.
11955        let volume = Quantity::from("0.05");
11956        let increment = Quantity::from("0.01");
11957        let sizes = BarTickSizes::from_volume(volume, increment);
11958        assert_eq!(sizes.open, Quantity::from("0.01"));
11959        assert_eq!(sizes.high, Quantity::from("0.01"));
11960        assert_eq!(sizes.low, Quantity::from("0.01"));
11961        assert_eq!(sizes.close, Quantity::from("0.02"));
11962        assert_valid_bar_tick_sizes(volume, increment);
11963        assert_eq!(
11964            sizes.open.raw() + sizes.high.raw() + sizes.low.raw() + sizes.close.raw(),
11965            volume.raw()
11966        );
11967    }
11968
11969    #[rstest]
11970    #[case("1", "0", "0", "0", "1")]
11971    #[case("2", "0", "1", "1", "0")]
11972    #[case("3", "1", "1", "1", "0")]
11973    fn test_bar_tick_sizes_units_less_than_four_preserves_volume(
11974        #[case] volume: &str,
11975        #[case] open_size: &str,
11976        #[case] high_size: &str,
11977        #[case] low_size: &str,
11978        #[case] close_size: &str,
11979    ) {
11980        let volume = Quantity::from(volume);
11981        let increment = Quantity::from("1");
11982        let sizes = BarTickSizes::from_volume(volume, increment);
11983
11984        assert_eq!(sizes.open, Quantity::from(open_size));
11985        assert_eq!(sizes.high, Quantity::from(high_size));
11986        assert_eq!(sizes.low, Quantity::from(low_size));
11987        assert_eq!(sizes.close, Quantity::from(close_size));
11988        assert_valid_bar_tick_sizes(volume, increment);
11989        assert_eq!(
11990            sizes.open.raw() + sizes.high.raw() + sizes.low.raw() + sizes.close.raw(),
11991            volume.raw()
11992        );
11993    }
11994
11995    #[rstest]
11996    fn test_bar_tick_sizes_zero_volume_remains_zero() {
11997        let volume = Quantity::zero(3);
11998        let increment = Quantity::from("0.001");
11999        let sizes = BarTickSizes::from_volume(volume, increment);
12000        assert_eq!(sizes.open, Quantity::zero(3));
12001        assert_eq!(sizes.high, Quantity::zero(3));
12002        assert_eq!(sizes.low, Quantity::zero(3));
12003        assert_eq!(sizes.close, Quantity::zero(3));
12004        assert_valid_bar_tick_sizes(volume, increment);
12005    }
12006
12007    #[rstest]
12008    fn test_bar_tick_sizes_rounds_down_to_size_increment() {
12009        let volume = Quantity::from("1.07");
12010        let increment = Quantity::from("0.10");
12011        let sizes = BarTickSizes::from_volume(volume, increment);
12012        assert_eq!(sizes.open, Quantity::from("0.20"));
12013        assert_eq!(sizes.high, Quantity::from("0.20"));
12014        assert_eq!(sizes.low, Quantity::from("0.20"));
12015        assert_eq!(sizes.close, Quantity::from("0.40"));
12016        assert_valid_bar_tick_sizes(volume, increment);
12017    }
12018
12019    #[rstest]
12020    fn test_bar_tick_sizes_at_fixed_precision() {
12021        // When volume.precision == FIXED_PRECISION the scale is 1 and the formula
12022        // degenerates to a plain raw-space quartering.
12023        let units: QuantityRaw = 17;
12024        let volume = Quantity::from_raw(units, FIXED_PRECISION);
12025        let increment = Quantity::from_raw(1, FIXED_PRECISION);
12026        let sizes = BarTickSizes::from_volume(volume, increment);
12027        assert_eq!(sizes.open.raw(), 4);
12028        assert_eq!(sizes.high.raw(), 4);
12029        assert_eq!(sizes.low.raw(), 4);
12030        assert_eq!(sizes.close.raw(), 5);
12031        assert_valid_bar_tick_sizes(volume, increment);
12032    }
12033
12034    fn get_queue_engine(
12035        instrument: InstrumentAny,
12036        book_type: BookType,
12037    ) -> (OrderMatchingEngine, Rc<RefCell<Cache>>) {
12038        let clock = Rc::new(RefCell::new(VirtualClock::new()));
12039        let cache = Rc::new(RefCell::new(Cache::default()));
12040        let config = OrderMatchingEngineConfig {
12041            trade_execution: true,
12042            queue_position: true,
12043            ..Default::default()
12044        };
12045
12046        let mut engine = OrderMatchingEngine::new(
12047            instrument,
12048            1,
12049            FillModelHandle::default(),
12050            FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into(),
12051            book_type,
12052            OmsType::Netting,
12053            AccountType::Margin,
12054            clock,
12055            Rc::clone(&cache),
12056            config,
12057        );
12058
12059        let handler_cache = Rc::clone(&cache);
12060        engine.set_event_handler(Rc::new(move |event: OrderEventAny| {
12061            if let Ok(mut cache) = handler_cache.try_borrow_mut() {
12062                let _ = cache.update_order(&event);
12063            }
12064        }));
12065
12066        (engine, cache)
12067    }
12068
12069    fn get_l3_queue_engine(instrument: InstrumentAny) -> (OrderMatchingEngine, Rc<RefCell<Cache>>) {
12070        get_queue_engine(instrument, BookType::L3_MBO)
12071    }
12072
12073    fn assert_l3_queue_synced(engine: &OrderMatchingEngine) {
12074        for (client_order_id, orders_ahead) in &engine.queue_ahead_orders {
12075            let set_sum: QuantityRaw = orders_ahead.values().sum();
12076            let counter = engine
12077                .queue_ahead_total
12078                .get(client_order_id)
12079                .map_or(0, |&(_, ahead_raw)| ahead_raw);
12080            assert_eq!(
12081                set_sum, counter,
12082                "tracked orders out of sync with quantity-ahead counter for {client_order_id}",
12083            );
12084        }
12085
12086        for (client_order_id, price_raw) in &engine.queue_pending {
12087            assert!(
12088                engine
12089                    .queue_ids_by_price
12090                    .get(price_raw)
12091                    .is_some_and(|ids| ids.contains(client_order_id)),
12092                "pending order {client_order_id} missing from price index",
12093            );
12094        }
12095
12096        for (client_order_id, (price_raw, _)) in &engine.queue_ahead_total {
12097            assert!(
12098                engine
12099                    .queue_ids_by_price
12100                    .get(price_raw)
12101                    .is_some_and(|ids| ids.contains(client_order_id)),
12102                "tracked order {client_order_id} missing from price index",
12103            );
12104        }
12105
12106        for (price_raw, client_order_ids) in &engine.queue_ids_by_price {
12107            for client_order_id in client_order_ids {
12108                let pending_at_price = engine.queue_pending.get(client_order_id) == Some(price_raw);
12109                let tracked_at_price = engine
12110                    .queue_ahead_total
12111                    .get(client_order_id)
12112                    .is_some_and(|(tracked_price_raw, _)| tracked_price_raw == price_raw);
12113                assert!(
12114                    pending_at_price || tracked_at_price,
12115                    "price index contains stale order {client_order_id}",
12116                );
12117            }
12118        }
12119    }
12120
12121    #[rstest]
12122    fn test_reset_clears_queue_positions() {
12123        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
12124        let (mut engine, _cache) = get_l3_queue_engine(instrument);
12125        let price = Price::from("100.00");
12126        let client_order_id = ClientOrderId::from("O-RESET-QUEUE");
12127
12128        rest_l3_queue_order(&mut engine, price, 1, client_order_id);
12129
12130        assert!(engine.queue_ahead_total.contains_key(&client_order_id));
12131        assert!(engine.queue_ahead_orders.contains_key(&client_order_id));
12132        assert!(
12133            engine
12134                .queue_ids_by_price
12135                .get(&price.raw())
12136                .is_some_and(|ids| ids.contains(&client_order_id)),
12137        );
12138
12139        engine.reset();
12140
12141        assert!(engine.queue_pending.is_empty());
12142        assert!(engine.queue_ahead_total.is_empty());
12143        assert!(engine.queue_ahead_orders.is_empty());
12144        assert!(engine.queue_excess.is_empty());
12145        assert!(engine.queue_ids_by_price.is_empty());
12146    }
12147
12148    #[rstest]
12149    fn test_cancel_removes_queue_position() {
12150        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
12151        let (mut engine, _cache) = get_l3_queue_engine(instrument);
12152        let price = Price::from("100.00");
12153        let order =
12154            rest_l3_queue_order(&mut engine, price, 1, ClientOrderId::from("O-CANCEL-QUEUE"));
12155        let client_order_id = order.client_order_id();
12156
12157        assert!(engine.queue_ahead_total.contains_key(&client_order_id));
12158        assert!(engine.queue_ahead_orders.contains_key(&client_order_id));
12159        assert!(
12160            engine
12161                .queue_ids_by_price
12162                .get(&price.raw())
12163                .is_some_and(|ids| ids.contains(&client_order_id)),
12164        );
12165
12166        engine.cancel_order(&order, None);
12167
12168        assert!(!engine.queue_pending.contains_key(&client_order_id));
12169        assert!(!engine.queue_ahead_total.contains_key(&client_order_id));
12170        assert!(!engine.queue_ahead_orders.contains_key(&client_order_id));
12171        assert!(!engine.queue_excess.contains_key(&client_order_id));
12172        assert!(!engine.queue_ids_by_price.contains_key(&price.raw()));
12173    }
12174
12175    #[rstest]
12176    fn test_modify_reindexes_queue_position() {
12177        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
12178        let (mut engine, _cache) = get_l3_queue_engine(instrument);
12179        let old_price = Price::from("100.00");
12180        let new_price = Price::from("101.00");
12181        let client_order_id = ClientOrderId::from("O-MODIFY-QUEUE");
12182        let order = rest_l3_queue_order(&mut engine, old_price, 1, client_order_id);
12183        let new_level = OrderBookDelta::new(
12184            engine.instrument.id(),
12185            BookAction::Add,
12186            BookOrder::new(OrderSide::Sell, new_price, Quantity::from("10.000"), 2),
12187            0,
12188            2,
12189            UnixNanos::from(2),
12190            UnixNanos::from(2),
12191        );
12192        engine.process_order_book_delta(&new_level).unwrap();
12193
12194        let command = ModifyOrder::new(
12195            order.trader_id(),
12196            None,
12197            order.strategy_id(),
12198            order.instrument_id(),
12199            client_order_id,
12200            order.venue_order_id(),
12201            None,
12202            Some(new_price),
12203            None,
12204            UUID4::new(),
12205            UnixNanos::from(3),
12206            None,
12207            None,
12208        );
12209        engine.process_modify(&command, AccountId::from("SIM-001"));
12210
12211        assert!(!engine.queue_ids_by_price.contains_key(&old_price.raw()));
12212        assert_eq!(
12213            engine
12214                .queue_ids_by_price
12215                .get(&new_price.raw())
12216                .map(|ids| ids.iter().copied().collect::<Vec<_>>()),
12217            Some(vec![client_order_id]),
12218        );
12219        assert_eq!(
12220            engine.queue_ahead_total.get(&client_order_id),
12221            Some(&(new_price.raw(), Quantity::from("10.000").raw())),
12222        );
12223        assert_eq!(
12224            engine
12225                .queue_ahead_orders
12226                .get(&client_order_id)
12227                .map(|orders| orders.keys().copied().collect::<Vec<_>>()),
12228            Some(vec![2]),
12229        );
12230    }
12231
12232    #[rstest]
12233    fn test_snapshot_rebases_l2_queue_position_after_size_decrease() {
12234        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
12235        let instrument_id = instrument.id();
12236        let (mut engine, cache) = get_queue_engine(instrument, BookType::L2_MBP);
12237
12238        let initial = OrderBookDelta::new(
12239            instrument_id,
12240            BookAction::Add,
12241            BookOrder::new(
12242                OrderSide::Sell,
12243                Price::from("100.00"),
12244                Quantity::from("10.000"),
12245                0,
12246            ),
12247            0,
12248            1,
12249            UnixNanos::from(1_u64),
12250            UnixNanos::from(1_u64),
12251        );
12252        engine.process_order_book_delta(&initial).unwrap();
12253
12254        let client_order_id = ClientOrderId::from("O-SNAPSHOT-DECREASE");
12255        let mut order = OrderTestBuilder::new(OrderType::Limit)
12256            .instrument_id(instrument_id)
12257            .side(OrderSide::Sell)
12258            .price(Price::from("100.00"))
12259            .quantity(Quantity::from("1.000"))
12260            .client_order_id(client_order_id)
12261            .submit(true)
12262            .build();
12263        process_cached_order(&mut engine, &mut order, AccountId::from("SIM-001"));
12264        assert_eq!(
12265            engine.queue_ahead_total.get(&client_order_id),
12266            Some(&(Price::from("100.00").raw(), Quantity::from("10.000").raw())),
12267        );
12268
12269        let clear = OrderBookDelta::clear(
12270            instrument_id,
12271            2,
12272            UnixNanos::from(2_u64),
12273            UnixNanos::from(2_u64),
12274        );
12275        engine.process_order_book_delta(&clear).unwrap();
12276        assert_eq!(
12277            engine.queue_ahead_total.get(&client_order_id),
12278            Some(&(Price::from("100.00").raw(), Quantity::from("10.000").raw())),
12279            "partial snapshot must not discard the old queue estimate",
12280        );
12281
12282        let snapshot = OrderBookDelta::new(
12283            instrument_id,
12284            BookAction::Add,
12285            BookOrder::new(
12286                OrderSide::Sell,
12287                Price::from("100.00"),
12288                Quantity::from("8.000"),
12289                0,
12290            ),
12291            RecordFlag::F_LAST as u8,
12292            2,
12293            UnixNanos::from(2_u64),
12294            UnixNanos::from(2_u64),
12295        );
12296        engine.process_order_book_delta(&snapshot).unwrap();
12297
12298        assert_eq!(
12299            engine.queue_ahead_total.get(&client_order_id),
12300            Some(&(Price::from("100.00").raw(), Quantity::from("8.000").raw())),
12301        );
12302        assert!(cache.borrow().order(&client_order_id).is_some());
12303    }
12304
12305    #[rstest]
12306    fn test_snapshot_rebase_does_not_increase_l2_queue_position() {
12307        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
12308        let instrument_id = instrument.id();
12309        let (mut engine, _cache) = get_queue_engine(instrument, BookType::L2_MBP);
12310
12311        let initial = OrderBookDelta::new(
12312            instrument_id,
12313            BookAction::Add,
12314            BookOrder::new(
12315                OrderSide::Sell,
12316                Price::from("100.00"),
12317                Quantity::from("10.000"),
12318                0,
12319            ),
12320            0,
12321            1,
12322            UnixNanos::from(1_u64),
12323            UnixNanos::from(1_u64),
12324        );
12325        engine.process_order_book_delta(&initial).unwrap();
12326
12327        let client_order_id = ClientOrderId::from("O-SNAPSHOT-INCREASE");
12328        let mut order = OrderTestBuilder::new(OrderType::Limit)
12329            .instrument_id(instrument_id)
12330            .side(OrderSide::Sell)
12331            .price(Price::from("100.00"))
12332            .quantity(Quantity::from("1.000"))
12333            .client_order_id(client_order_id)
12334            .submit(true)
12335            .build();
12336        process_cached_order(&mut engine, &mut order, AccountId::from("SIM-001"));
12337
12338        let snapshot = OrderBookDelta::new(
12339            instrument_id,
12340            BookAction::Add,
12341            BookOrder::new(
12342                OrderSide::Sell,
12343                Price::from("100.00"),
12344                Quantity::from("15.000"),
12345                0,
12346            ),
12347            RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
12348            2,
12349            UnixNanos::from(2_u64),
12350            UnixNanos::from(2_u64),
12351        );
12352        engine.process_order_book_delta(&snapshot).unwrap();
12353
12354        assert_eq!(
12355            engine.queue_ahead_total.get(&client_order_id),
12356            Some(&(Price::from("100.00").raw(), Quantity::from("10.000").raw())),
12357        );
12358    }
12359
12360    #[rstest]
12361    fn test_depth_rebases_l2_queue_position() {
12362        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
12363        let instrument_id = instrument.id();
12364        let (mut engine, _cache) = get_queue_engine(instrument, BookType::L2_MBP);
12365
12366        let mut asks = [BookOrder::default(); DEPTH10_LEN];
12367        asks[0] = BookOrder::new(
12368            OrderSide::Sell,
12369            Price::from("100.00"),
12370            Quantity::from("10.000"),
12371            0,
12372        );
12373        let initial = OrderBookDepth::new(
12374            instrument_id,
12375            [BookOrder::default(); DEPTH10_LEN],
12376            asks,
12377            [0; DEPTH10_LEN],
12378            [0; DEPTH10_LEN],
12379            0,
12380            1,
12381            UnixNanos::from(1_u64),
12382            UnixNanos::from(1_u64),
12383        );
12384        engine.process_order_book_depth(&initial).unwrap();
12385
12386        let client_order_id = ClientOrderId::from("O-DEPTH-REBASE");
12387        let mut order = OrderTestBuilder::new(OrderType::Limit)
12388            .instrument_id(instrument_id)
12389            .side(OrderSide::Sell)
12390            .price(Price::from("100.00"))
12391            .quantity(Quantity::from("1.000"))
12392            .client_order_id(client_order_id)
12393            .submit(true)
12394            .build();
12395        process_cached_order(&mut engine, &mut order, AccountId::from("SIM-001"));
12396
12397        asks[0] = BookOrder::new(
12398            OrderSide::Sell,
12399            Price::from("100.00"),
12400            Quantity::from("8.000"),
12401            0,
12402        );
12403        let replacement = OrderBookDepth::new(
12404            instrument_id,
12405            [BookOrder::default(); DEPTH10_LEN],
12406            asks,
12407            [0; DEPTH10_LEN],
12408            [0; DEPTH10_LEN],
12409            0,
12410            2,
12411            UnixNanos::from(2_u64),
12412            UnixNanos::from(2_u64),
12413        );
12414        engine.process_order_book_depth(&replacement).unwrap();
12415
12416        assert_eq!(
12417            engine.queue_ahead_total.get(&client_order_id),
12418            Some(&(Price::from("100.00").raw(), Quantity::from("8.000").raw())),
12419        );
12420    }
12421
12422    #[rstest]
12423    fn test_snapshot_rebases_each_l3_order_independently() {
12424        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
12425        let instrument_id = instrument.id();
12426        let (mut engine, _cache) = get_l3_queue_engine(instrument);
12427
12428        for (order_id, sequence) in [(1, 1), (2, 2)] {
12429            let delta = OrderBookDelta::new(
12430                instrument_id,
12431                BookAction::Add,
12432                BookOrder::new(
12433                    OrderSide::Sell,
12434                    Price::from("100.00"),
12435                    Quantity::from("5.000"),
12436                    order_id,
12437                ),
12438                0,
12439                sequence,
12440                UnixNanos::from(sequence),
12441                UnixNanos::from(sequence),
12442            );
12443            engine.process_order_book_delta(&delta).unwrap();
12444        }
12445
12446        let client_order_id = ClientOrderId::from("O-SNAPSHOT-L3");
12447        let order = rest_l3_queue_order(&mut engine, Price::from("100.00"), 3, client_order_id);
12448        assert_eq!(
12449            engine.queue_ahead_orders[&client_order_id]
12450                .keys()
12451                .copied()
12452                .collect::<Vec<_>>(),
12453            vec![1, 2, 3],
12454        );
12455        assert_eq!(
12456            engine.queue_ahead_total.get(&client_order_id),
12457            Some(&(Price::from("100.00").raw(), Quantity::from("20.000").raw())),
12458        );
12459
12460        let snapshot = OrderBookDeltas::new(
12461            instrument_id,
12462            vec![
12463                OrderBookDelta::clear(
12464                    instrument_id,
12465                    4,
12466                    UnixNanos::from(4_u64),
12467                    UnixNanos::from(4_u64),
12468                ),
12469                OrderBookDelta::new(
12470                    instrument_id,
12471                    BookAction::Add,
12472                    BookOrder::new(
12473                        OrderSide::Sell,
12474                        Price::from("100.00"),
12475                        Quantity::from("10.000"),
12476                        1,
12477                    ),
12478                    RecordFlag::F_SNAPSHOT as u8,
12479                    4,
12480                    UnixNanos::from(4_u64),
12481                    UnixNanos::from(4_u64),
12482                ),
12483                OrderBookDelta::new(
12484                    instrument_id,
12485                    BookAction::Add,
12486                    BookOrder::new(
12487                        OrderSide::Sell,
12488                        Price::from("100.00"),
12489                        Quantity::from("5.000"),
12490                        2,
12491                    ),
12492                    RecordFlag::F_LAST as u8,
12493                    4,
12494                    UnixNanos::from(4_u64),
12495                    UnixNanos::from(4_u64),
12496                ),
12497            ],
12498        );
12499        engine.process_order_book_deltas(&snapshot).unwrap();
12500
12501        assert_eq!(
12502            engine.queue_ahead_orders[&client_order_id]
12503                .iter()
12504                .map(|(&order_id, &size)| (order_id, size))
12505                .collect::<Vec<_>>(),
12506            vec![
12507                (1, Quantity::from("5.000").raw()),
12508                (2, Quantity::from("5.000").raw())
12509            ],
12510        );
12511        assert_eq!(
12512            engine.queue_ahead_total.get(&client_order_id),
12513            Some(&(Price::from("100.00").raw(), Quantity::from("10.000").raw())),
12514        );
12515
12516        let delete_a = OrderBookDelta::new(
12517            instrument_id,
12518            BookAction::Delete,
12519            BookOrder::new(
12520                OrderSide::Sell,
12521                Price::from("100.00"),
12522                Quantity::from("10.000"),
12523                1,
12524            ),
12525            0,
12526            5,
12527            UnixNanos::from(5_u64),
12528            UnixNanos::from(5_u64),
12529        );
12530        engine.process_order_book_delta(&delete_a).unwrap();
12531
12532        assert_eq!(
12533            engine.queue_ahead_orders[&client_order_id]
12534                .keys()
12535                .copied()
12536                .collect::<Vec<_>>(),
12537            vec![2],
12538        );
12539        assert_eq!(
12540            engine.queue_ahead_total.get(&client_order_id),
12541            Some(&(Price::from("100.00").raw(), Quantity::from("5.000").raw())),
12542        );
12543        assert_eq!(order.client_order_id(), client_order_id);
12544    }
12545
12546    #[rstest]
12547    fn test_queue_price_index_filters_other_prices() {
12548        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
12549        let (mut engine, _cache) = get_l3_queue_engine(instrument);
12550        let target_price = Price::from("100.00");
12551        let other_price = Price::from("101.00");
12552        let target_id = ClientOrderId::from("O-QUEUE-TARGET");
12553        let other_id = ClientOrderId::from("O-QUEUE-OTHER");
12554
12555        rest_l3_queue_order(&mut engine, target_price, 1, target_id);
12556        rest_l3_queue_order(&mut engine, other_price, 2, other_id);
12557
12558        let indexed_ids = engine.take_queue_ids_at_price(target_price.raw());
12559
12560        assert_eq!(indexed_ids, vec![target_id]);
12561        assert!(
12562            engine
12563                .queue_ids_by_price
12564                .get(&other_price.raw())
12565                .is_some_and(|ids| ids.contains(&other_id)),
12566        );
12567    }
12568
12569    fn rest_l3_queue_order(
12570        engine: &mut OrderMatchingEngine,
12571        price: Price,
12572        sequence: u64,
12573        client_order_id: ClientOrderId,
12574    ) -> OrderAny {
12575        let instrument_id = engine.instrument.id();
12576        let delta = OrderBookDelta::new(
12577            instrument_id,
12578            BookAction::Add,
12579            BookOrder::new(OrderSide::Sell, price, Quantity::from("10.000"), sequence),
12580            0,
12581            sequence,
12582            UnixNanos::from(sequence),
12583            UnixNanos::from(sequence),
12584        );
12585        engine.process_order_book_delta(&delta).unwrap();
12586
12587        let mut order = OrderTestBuilder::new(OrderType::Limit)
12588            .instrument_id(instrument_id)
12589            .side(OrderSide::Sell)
12590            .price(price)
12591            .quantity(Quantity::from("5.000"))
12592            .client_order_id(client_order_id)
12593            .submit(true)
12594            .build();
12595        process_cached_order(engine, &mut order, AccountId::from("SIM-001"));
12596
12597        order
12598    }
12599
12600    #[derive(Debug, Clone, Copy)]
12601    enum QueueEvent {
12602        Add { id: OrderId, size: u64 },
12603        Update { id: OrderId, size: u64 },
12604        MoveAway { id: OrderId },
12605        Delete { id: OrderId },
12606        Trade { size: u64, aggressor: u8 },
12607        AggregateCap { size: u64 },
12608        AggregateDelete,
12609        RestOrder,
12610    }
12611
12612    fn granular_queue_event() -> impl Strategy<Value = QueueEvent> {
12613        prop_oneof![
12614            3 => (1u64..=6, 1u64..=9).prop_map(|(id, size)| QueueEvent::Add { id, size }),
12615            3 => (1u64..=6, 1u64..=9).prop_map(|(id, size)| QueueEvent::Update { id, size }),
12616            1 => (1u64..=6).prop_map(|id| QueueEvent::MoveAway { id }),
12617            2 => (1u64..=6).prop_map(|id| QueueEvent::Delete { id }),
12618            2 => Just(QueueEvent::RestOrder),
12619        ]
12620    }
12621
12622    fn any_queue_event() -> impl Strategy<Value = QueueEvent> {
12623        prop_oneof![
12624            5 => granular_queue_event(),
12625            3 => (1u64..=9, 0u8..3).prop_map(|(size, aggressor)| QueueEvent::Trade {
12626                size,
12627                aggressor,
12628            }),
12629            1 => (1u64..=9).prop_map(|size| QueueEvent::AggregateCap { size }),
12630            1 => Just(QueueEvent::AggregateDelete),
12631        ]
12632    }
12633
12634    // Drives generated events through an L3 queue_position engine; the
12635    // shadow id maps sanitize the feed to what real MBO feeds guarantee
12636    struct L3QueueSim {
12637        engine: OrderMatchingEngine,
12638        account_id: AccountId,
12639        live_main: HashMap<OrderId, u64>,
12640        live_away: HashSet<OrderId>,
12641        rest_snapshots: HashMap<ClientOrderId, HashSet<OrderId>>,
12642        rested: usize,
12643        sequence: u64,
12644    }
12645
12646    impl L3QueueSim {
12647        const MAIN_PRICE: &'static str = "100.00";
12648        const AWAY_PRICE: &'static str = "101.00";
12649
12650        fn new() -> Self {
12651            let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
12652            let (engine, _cache) = get_l3_queue_engine(instrument);
12653
12654            Self {
12655                engine,
12656                account_id: AccountId::from("SIM-001"),
12657                live_main: HashMap::new(),
12658                live_away: HashSet::new(),
12659                rest_snapshots: HashMap::new(),
12660                rested: 0,
12661                sequence: 0,
12662            }
12663        }
12664
12665        fn quantity(size: u64) -> Quantity {
12666            Quantity::from(format!("{size}.000").as_str())
12667        }
12668
12669        fn process_delta(
12670            &mut self,
12671            action: BookAction,
12672            price: &str,
12673            size: u64,
12674            order_id: OrderId,
12675            flags: u8,
12676        ) {
12677            self.sequence += 1;
12678            let delta = OrderBookDelta::new(
12679                self.engine.instrument.id(),
12680                action,
12681                BookOrder::new(
12682                    OrderSide::Sell,
12683                    Price::from(price),
12684                    Self::quantity(size),
12685                    order_id,
12686                ),
12687                flags,
12688                self.sequence,
12689                UnixNanos::from(self.sequence),
12690                UnixNanos::from(self.sequence),
12691            );
12692            self.engine.process_order_book_delta(&delta).unwrap();
12693        }
12694
12695        fn apply(&mut self, event: QueueEvent) {
12696            match event {
12697                QueueEvent::Add { id, size } => {
12698                    if self.live_main.contains_key(&id) || self.live_away.contains(&id) {
12699                        return;
12700                    }
12701                    self.process_delta(BookAction::Add, Self::MAIN_PRICE, size, id, 0);
12702                    self.live_main.insert(id, size);
12703                }
12704                QueueEvent::Update { id, size } => {
12705                    if !self.live_main.contains_key(&id) {
12706                        return;
12707                    }
12708                    self.process_delta(BookAction::Update, Self::MAIN_PRICE, size, id, 0);
12709                    self.live_main.insert(id, size);
12710                }
12711                QueueEvent::MoveAway { id } => {
12712                    let Some(size) = self.live_main.remove(&id) else {
12713                        return;
12714                    };
12715                    self.process_delta(BookAction::Update, Self::AWAY_PRICE, size, id, 0);
12716                    self.live_away.insert(id);
12717                }
12718                QueueEvent::Delete { id } => {
12719                    if let Some(size) = self.live_main.remove(&id) {
12720                        self.process_delta(BookAction::Delete, Self::MAIN_PRICE, size, id, 0);
12721                    } else if self.live_away.remove(&id) {
12722                        self.process_delta(BookAction::Delete, Self::AWAY_PRICE, 1, id, 0);
12723                    } else {
12724                        // Unknown id exercises the ignore path
12725                        self.process_delta(BookAction::Delete, Self::MAIN_PRICE, 1, id, 0);
12726                    }
12727
12728                    // A later Add reusing this id is a new order, not the
12729                    // snapshot-time one (real feeds never reuse ids)
12730                    for snapshot_ids in self.rest_snapshots.values_mut() {
12731                        snapshot_ids.remove(&id);
12732                    }
12733                }
12734                QueueEvent::Trade { size, aggressor } => {
12735                    self.sequence += 1;
12736                    let aggressor_side = match aggressor {
12737                        0 => AggressorSide::Buy,
12738                        1 => AggressorSide::Sell,
12739                        _ => AggressorSide::NoAggressor,
12740                    };
12741                    let trade = TradeTick::new(
12742                        self.engine.instrument.id(),
12743                        Price::from(Self::MAIN_PRICE),
12744                        Self::quantity(size),
12745                        aggressor_side,
12746                        TradeId::new(format!("T-{}", self.sequence).as_str()),
12747                        UnixNanos::from(self.sequence),
12748                        UnixNanos::from(self.sequence),
12749                    );
12750                    self.engine.process_trade_tick(&trade);
12751                }
12752                QueueEvent::AggregateCap { size } => {
12753                    self.process_delta(
12754                        BookAction::Update,
12755                        Self::MAIN_PRICE,
12756                        size,
12757                        0,
12758                        RecordFlag::F_MBP as u8,
12759                    );
12760                }
12761                QueueEvent::AggregateDelete => {
12762                    self.process_delta(
12763                        BookAction::Delete,
12764                        Self::MAIN_PRICE,
12765                        1,
12766                        0,
12767                        RecordFlag::F_MBP as u8,
12768                    );
12769                }
12770                QueueEvent::RestOrder => {
12771                    if self.rested >= 3 {
12772                        return;
12773                    }
12774                    self.rested += 1;
12775                    let mut order = OrderTestBuilder::new(OrderType::Limit)
12776                        .instrument_id(self.engine.instrument.id())
12777                        .side(OrderSide::Sell)
12778                        .price(Price::from(Self::MAIN_PRICE))
12779                        .quantity(Self::quantity(5))
12780                        .client_order_id(ClientOrderId::from(
12781                            format!("O-PROP-{}", self.rested).as_str(),
12782                        ))
12783                        .submit(true)
12784                        .build();
12785                    process_cached_order(&mut self.engine, &mut order, self.account_id);
12786
12787                    assert!(
12788                        self.engine
12789                            .queue_ahead_orders
12790                            .contains_key(&order.client_order_id()),
12791                        "L3 snapshot must track the resting order",
12792                    );
12793
12794                    self.rest_snapshots.insert(
12795                        order.client_order_id(),
12796                        self.live_main.keys().copied().collect(),
12797                    );
12798                }
12799            }
12800        }
12801
12802        // Without trades or aggregate rows, tracked orders must mirror the book
12803        // exactly, and equal the rest-time snapshot ids still at the level
12804        fn assert_tracked_orders_match_book(&self) {
12805            let level: HashMap<OrderId, QuantityRaw> = self
12806                .engine
12807                .book
12808                .get_orders_at_level(Price::from(Self::MAIN_PRICE), OrderSide::Buy)
12809                .iter()
12810                .map(|order| (order.order_id, order.size.raw()))
12811                .collect();
12812
12813            for (client_order_id, orders_ahead) in &self.engine.queue_ahead_orders {
12814                for (order_id, size_raw) in orders_ahead {
12815                    let book_size = level.get(order_id).copied().unwrap_or_else(|| {
12816                        panic!("tracked order {order_id} for {client_order_id} not in book level")
12817                    });
12818                    assert_eq!(
12819                        book_size, *size_raw,
12820                        "tracked size diverged from book for order {order_id}",
12821                    );
12822                }
12823
12824                let tracked: HashSet<OrderId> = orders_ahead.keys().copied().collect();
12825                let expected: HashSet<OrderId> = self.rest_snapshots[client_order_id]
12826                    .iter()
12827                    .filter(|id| self.live_main.contains_key(id))
12828                    .copied()
12829                    .collect();
12830                assert_eq!(
12831                    tracked, expected,
12832                    "tracked set incomplete or stale for {client_order_id}",
12833                );
12834            }
12835        }
12836    }
12837
12838    #[rstest]
12839    fn prop_test_l3_queue_tracking_stays_synced_with_counter() {
12840        proptest!(|(events in prop::collection::vec(any_queue_event(), 1..=80))| {
12841            let mut sim = L3QueueSim::new();
12842            for event in events {
12843                sim.apply(event);
12844                assert_l3_queue_synced(&sim.engine);
12845            }
12846        });
12847    }
12848
12849    #[rstest]
12850    fn prop_test_l3_queue_tracking_mirrors_book_without_trades() {
12851        proptest!(|(events in prop::collection::vec(granular_queue_event(), 1..=80))| {
12852            let mut sim = L3QueueSim::new();
12853            for event in events {
12854                sim.apply(event);
12855                assert_l3_queue_synced(&sim.engine);
12856                sim.assert_tracked_orders_match_book();
12857            }
12858        });
12859    }
12860
12861    // Replays real GLBX MBO flow (records 9150..10650 of
12862    // test_data/databento/esh4-glbx-mdp3-20231225.mbo.dbn.zst as JSON),
12863    // joining the touch periodically; the mid-stream start also exercises
12864    // unseen-id ignore paths
12865    #[rstest]
12866    fn test_l3_queue_position_replay_databento_mbo_stays_synced() {
12867        let json = include_str!("../../../../test_data/databento/esh4-glbx-mdp3-20231225.mbo.json");
12868        let records: Vec<serde_json::Value> = serde_json::from_str(json).unwrap();
12869        assert!(records.len() > 1000);
12870
12871        let instrument = InstrumentAny::FuturesContract(futures_contract_es(None, None));
12872        let instrument_id = instrument.id();
12873        let (mut engine, cache) = get_l3_queue_engine(instrument);
12874        let account_id = AccountId::from("SIM-001");
12875
12876        let mut rested = 0usize;
12877        let mut trades = 0usize;
12878
12879        for (index, record) in records.iter().enumerate() {
12880            match record.get("type").and_then(serde_json::Value::as_str) {
12881                Some("OrderBookDelta") => {
12882                    let mut delta: OrderBookDelta = serde_json::from_value(record.clone()).unwrap();
12883                    delta.instrument_id = instrument_id;
12884                    engine.process_order_book_delta(&delta).unwrap();
12885                }
12886                Some("TradeTick") => {
12887                    let mut trade: TradeTick = serde_json::from_value(record.clone()).unwrap();
12888                    trade.instrument_id = instrument_id;
12889                    engine.process_trade_tick(&trade);
12890                    trades += 1;
12891                }
12892                other => panic!("unexpected record type {other:?}"),
12893            }
12894
12895            if index % 150 == 100 {
12896                let (side, price) = if rested.is_multiple_of(2) {
12897                    (OrderSide::Sell, engine.book.best_ask_price())
12898                } else {
12899                    (OrderSide::Buy, engine.book.best_bid_price())
12900                };
12901
12902                if let Some(price) = price {
12903                    rested += 1;
12904                    let mut order = OrderTestBuilder::new(OrderType::Limit)
12905                        .instrument_id(instrument_id)
12906                        .side(side)
12907                        .price(price)
12908                        .quantity(Quantity::from("1"))
12909                        .client_order_id(ClientOrderId::from(format!("O-MBO-{rested}").as_str()))
12910                        .submit(true)
12911                        .build();
12912                    engine.process_order(&mut order, account_id);
12913
12914                    // A crossed mid-stream book can fill a joined order on
12915                    // arrival; only open orders are tracked
12916                    let is_open = cache
12917                        .borrow()
12918                        .order(&order.client_order_id())
12919                        .is_some_and(|order| order.is_open());
12920                    if is_open {
12921                        assert!(
12922                            engine
12923                                .queue_ahead_orders
12924                                .contains_key(&order.client_order_id()),
12925                            "L3 snapshot must track the resting order",
12926                        );
12927                    }
12928                }
12929            }
12930
12931            assert_l3_queue_synced(&engine);
12932        }
12933
12934        assert!(rested >= 5, "replay must exercise resting orders");
12935        assert!(trades >= 50, "replay must exercise trade interleavings");
12936    }
12937}