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