Skip to main content

nautilus_execution/matching_engine/
mod.rs

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