Skip to main content

nautilus_execution/matching_engine/
engine.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
16use std::{
17    cell::RefCell,
18    cmp::min,
19    fmt::Debug,
20    ops::{Add, Sub},
21    rc::Rc,
22};
23
24use chrono::TimeDelta;
25use indexmap::{IndexMap, IndexSet};
26use nautilus_common::{
27    cache::Cache,
28    clock::Clock,
29    messages::execution::{
30        BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, ModifyOrder,
31    },
32    msgbus::{self, MessagingSwitchboard},
33};
34use nautilus_core::{UUID4, UnixNanos, correctness::CorrectnessResult};
35use nautilus_model::{
36    data::{
37        Bar, BarType, InstrumentClose, OrderBookDelta, OrderBookDeltas, OrderBookDepth10,
38        QuoteTick, TradeTick,
39        order::{BookOrder, OrderId},
40    },
41    enums::{
42        AccountType, AggregationSource, AggressorSide, BookAction, BookType, ContingencyType,
43        InstrumentCloseType, LiquiditySide, MarketStatus, MarketStatusAction, OmsType, OptionKind,
44        OrderSide, OrderSideSpecified, OrderStatus, OrderType, PositionSide, PriceType, RecordFlag,
45        TimeInForce, TriggerType,
46    },
47    events::{
48        OrderAccepted, OrderCancelRejected, OrderCanceled, OrderEventAny, OrderExpired,
49        OrderFilled, OrderModifyRejected, OrderRejected, OrderSubmitted, OrderTriggered,
50        OrderUpdated,
51    },
52    identifiers::{
53        AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId, Venue,
54        VenueOrderId,
55    },
56    instruments::{Instrument, InstrumentAny},
57    orderbook::OrderBook,
58    orders::{MarketOrder, Order, OrderAny, OrderCore},
59    position::Position,
60    types::{
61        Currency, Money, Price, Quantity, fixed::FIXED_PRECISION, price::PriceRaw,
62        quantity::QuantityRaw,
63    },
64};
65use rust_decimal::Decimal;
66use ustr::Ustr;
67
68use crate::{
69    matching_core::{MatchAction, OrderMatchingCore, RestingOrder},
70    matching_engine::{config::OrderMatchingEngineConfig, ids_generator::IdsGenerator},
71    models::{
72        fee::{FeeModel, FeeModelHandle},
73        fill::{FillModel, FillModelHandle},
74    },
75    protection::protection_price_calculate,
76    trailing::trailing_stop_calculate,
77};
78
79/// An order matching engine for a single market.
80pub struct OrderMatchingEngine {
81    /// The venue for the matching engine.
82    pub venue: Venue,
83    /// The instrument for the matching engine.
84    pub instrument: InstrumentAny,
85    /// The instruments raw integer ID for the venue.
86    pub raw_id: u32,
87    /// The order book type for the matching engine.
88    pub book_type: BookType,
89    /// The order management system (OMS) type for the matching engine.
90    pub oms_type: OmsType,
91    /// The account type for the matching engine.
92    pub account_type: AccountType,
93    /// The market status for the matching engine.
94    pub market_status: MarketStatus,
95    /// The config for the matching engine.
96    pub config: OrderMatchingEngineConfig,
97    core: OrderMatchingCore,
98    clock: Rc<RefCell<dyn Clock>>,
99    cache: Rc<RefCell<Cache>>,
100    book: OrderBook,
101    fill_model: FillModelHandle,
102    fee_model: FeeModelHandle,
103    event_handler: Option<Rc<dyn Fn(OrderEventAny)>>,
104    target_bid: Option<Price>,
105    target_ask: Option<Price>,
106    target_last: Option<Price>,
107    last_bar_bid: Option<Bar>,
108    last_bar_ask: Option<Bar>,
109    fill_at_market: bool,
110    execution_bar_types: IndexMap<InstrumentId, BarType>,
111    execution_bar_deltas: IndexMap<BarType, TimeDelta>,
112    account_ids: IndexMap<TraderId, AccountId>,
113    cached_filled_qty: IndexMap<ClientOrderId, Quantity>,
114    post_match_order_ids: IndexSet<ClientOrderId>,
115    ids_generator: IdsGenerator,
116    last_trade_size: Option<Quantity>,
117    trade_consumption: QuantityRaw,
118    bid_consumption: IndexMap<PriceRaw, (QuantityRaw, QuantityRaw)>,
119    ask_consumption: IndexMap<PriceRaw, (QuantityRaw, QuantityRaw)>,
120    queue_pending: IndexMap<ClientOrderId, PriceRaw>,
121    queue_ahead_orders: IndexMap<ClientOrderId, IndexMap<OrderId, QuantityRaw>>,
122    queue_ahead_total: IndexMap<ClientOrderId, (PriceRaw, QuantityRaw)>,
123    queue_excess: IndexMap<ClientOrderId, QuantityRaw>,
124    prev_bid_price_raw: PriceRaw,
125    prev_bid_size_raw: QuantityRaw,
126    prev_ask_price_raw: PriceRaw,
127    prev_ask_size_raw: QuantityRaw,
128    tob_initialized: bool,
129    last_quote_bid: Option<Price>,
130    last_quote_ask: Option<Price>,
131    precision_mismatch_streak: u32,
132    instrument_close: Option<InstrumentClose>,
133    pending_resolution: bool,
134    settlement_price: Option<Price>,
135    expiration_processed: bool,
136}
137
138impl Debug for OrderMatchingEngine {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        f.debug_struct(stringify!(OrderMatchingEngine))
141            .field("venue", &self.venue)
142            .field("instrument", &self.instrument.id())
143            .finish()
144    }
145}
146
147impl OrderMatchingEngine {
148    /// Creates a new [`OrderMatchingEngine`] instance.
149    #[expect(clippy::too_many_arguments)]
150    pub fn new(
151        instrument: InstrumentAny,
152        raw_id: u32,
153        fill_model: FillModelHandle,
154        fee_model: FeeModelHandle,
155        book_type: BookType,
156        oms_type: OmsType,
157        account_type: AccountType,
158        clock: Rc<RefCell<dyn Clock>>,
159        cache: Rc<RefCell<Cache>>,
160        config: OrderMatchingEngineConfig,
161    ) -> Self {
162        let book = OrderBook::new(instrument.id(), book_type);
163        let mut core = OrderMatchingCore::new(instrument.id(), instrument.price_increment());
164        core.set_fill_limit_inside_spread(Self::fill_limit_inside_spread_or_false(&fill_model));
165        let ids_generator = IdsGenerator::new(
166            instrument.id().venue,
167            oms_type,
168            raw_id,
169            config.use_random_ids,
170            config.use_position_ids,
171            cache.clone(),
172        );
173
174        Self {
175            venue: instrument.id().venue,
176            instrument,
177            raw_id,
178            fill_model,
179            fee_model,
180            event_handler: None,
181            book_type,
182            oms_type,
183            account_type,
184            clock,
185            cache,
186            book,
187            market_status: MarketStatus::Open,
188            config,
189            core,
190            target_bid: None,
191            target_ask: None,
192            target_last: None,
193            last_bar_bid: None,
194            last_bar_ask: None,
195            fill_at_market: true,
196            execution_bar_types: IndexMap::new(),
197            execution_bar_deltas: IndexMap::new(),
198            account_ids: IndexMap::new(),
199            cached_filled_qty: IndexMap::new(),
200            post_match_order_ids: IndexSet::new(),
201            ids_generator,
202            last_trade_size: None,
203            trade_consumption: 0,
204            bid_consumption: IndexMap::new(),
205            ask_consumption: IndexMap::new(),
206            queue_pending: IndexMap::new(),
207            queue_ahead_orders: IndexMap::new(),
208            queue_ahead_total: IndexMap::new(),
209            queue_excess: IndexMap::new(),
210            prev_bid_price_raw: 0,
211            prev_bid_size_raw: 0,
212            prev_ask_price_raw: 0,
213            prev_ask_size_raw: 0,
214            tob_initialized: false,
215            last_quote_bid: None,
216            last_quote_ask: None,
217            precision_mismatch_streak: 0,
218            instrument_close: None,
219            pending_resolution: false,
220            settlement_price: None,
221            expiration_processed: false,
222        }
223    }
224
225    /// Sets the event handler for dispatching order events.
226    ///
227    /// When set, events are routed through the handler instead of directly
228    /// through the message bus. This allows sandbox execution clients to
229    /// dispatch events through the async runner channel, avoiding `RefCell`
230    /// re-entrancy panics.
231    pub fn set_event_handler(&mut self, handler: Rc<dyn Fn(OrderEventAny)>) {
232        self.event_handler = Some(handler);
233    }
234
235    fn dispatch_order_event(&self, event: OrderEventAny) {
236        if let Some(handler) = &self.event_handler {
237            handler(event);
238        } else {
239            let endpoint = MessagingSwitchboard::exec_engine_process();
240            msgbus::send_order_event(endpoint, event);
241        }
242    }
243
244    /// Resets the matching engine to its initial state.
245    ///
246    /// Clears the order book, execution state, cached data, and resets all
247    /// internal components. This is typically used for backtesting scenarios
248    /// where the engine needs to be reset between test runs.
249    pub fn reset(&mut self) {
250        self.book.reset();
251        self.execution_bar_types.clear();
252        self.execution_bar_deltas.clear();
253        self.account_ids.clear();
254        self.cached_filled_qty.clear();
255        self.post_match_order_ids.clear();
256        self.core.reset();
257        self.target_bid = None;
258        self.target_ask = None;
259        self.target_last = None;
260        self.last_trade_size = None;
261        self.trade_consumption = 0;
262        self.bid_consumption.clear();
263        self.ask_consumption.clear();
264        self.queue_pending.clear();
265        self.queue_ahead_orders.clear();
266        self.queue_ahead_total.clear();
267        self.queue_excess.clear();
268        self.prev_bid_price_raw = 0;
269        self.prev_bid_size_raw = 0;
270        self.prev_ask_price_raw = 0;
271        self.prev_ask_size_raw = 0;
272        self.tob_initialized = false;
273        self.last_quote_bid = None;
274        self.last_quote_ask = None;
275        self.last_bar_bid = None;
276        self.last_bar_ask = None;
277        self.precision_mismatch_streak = 0;
278        self.instrument_close = None;
279        self.pending_resolution = false;
280        self.settlement_price = None;
281        self.expiration_processed = false;
282        self.fill_at_market = true;
283        self.ids_generator.reset();
284
285        log::info!("Reset {}", self.instrument.id());
286    }
287
288    fn apply_liquidity_consumption(
289        &mut self,
290        fills: Vec<(Price, Quantity)>,
291        order_side: OrderSide,
292        leaves_qty: Quantity,
293        book_prices: Option<&[Price]>,
294    ) -> Vec<(Price, Quantity)> {
295        if !self.config.liquidity_consumption {
296            return fills;
297        }
298
299        let consumption = match order_side {
300            OrderSide::Buy => &mut self.ask_consumption,
301            OrderSide::Sell => &mut self.bid_consumption,
302            _ => return fills,
303        };
304
305        let mut adjusted_fills = Vec::with_capacity(fills.len());
306        let mut remaining_qty = leaves_qty.raw;
307
308        for (fill_idx, (price, qty)) in fills.into_iter().enumerate() {
309            if remaining_qty == 0 {
310                break;
311            }
312
313            // Use book_price for consumption tracking (original price before MAKER adjustment),
314            // but use price (potentially adjusted) for the output fill.
315            let book_price = book_prices
316                .and_then(|bp| bp.get(fill_idx).copied())
317                .unwrap_or(price);
318
319            let book_price_raw = book_price.raw;
320            let level_size = self
321                .book
322                .get_quantity_at_level(book_price, order_side, qty.precision);
323
324            let (original_size, consumed) = consumption
325                .entry(book_price_raw)
326                .or_insert((level_size.raw, 0));
327
328            // Reset consumption when book size changes (fresh data)
329            if *original_size != level_size.raw {
330                *original_size = level_size.raw;
331                *consumed = 0;
332            }
333
334            let available = original_size.saturating_sub(*consumed);
335            if available == 0 {
336                continue;
337            }
338
339            let adjusted_qty_raw = min(min(qty.raw, available), remaining_qty);
340            if adjusted_qty_raw == 0 {
341                continue;
342            }
343
344            *consumed += adjusted_qty_raw;
345            remaining_qty -= adjusted_qty_raw;
346
347            let adjusted_qty = Quantity::from_raw(adjusted_qty_raw, qty.precision);
348            adjusted_fills.push((price, adjusted_qty));
349        }
350
351        adjusted_fills
352    }
353
354    fn seed_trade_consumption(
355        &mut self,
356        trade_price_raw: PriceRaw,
357        trade_size_raw: QuantityRaw,
358        trade_ts_event: UnixNanos,
359        aggressor_side: AggressorSide,
360    ) {
361        if trade_size_raw == 0 {
362            return;
363        }
364
365        // If the book was updated after the trade's event time, depth deltas
366        // already reflect this trade's consumed volume, skip to avoid double-counting
367        if self.book.ts_last > trade_ts_event {
368            return;
369        }
370
371        let consumption = match aggressor_side {
372            AggressorSide::Buyer => &mut self.ask_consumption,
373            AggressorSide::Seller => &mut self.bid_consumption,
374            AggressorSide::NoAggressor => return,
375        };
376
377        let levels: Vec<_> = match aggressor_side {
378            AggressorSide::Buyer => self
379                .book
380                .asks(None)
381                .take_while(|l| l.price.value.raw <= trade_price_raw)
382                .collect(),
383            AggressorSide::Seller => self
384                .book
385                .bids(None)
386                .take_while(|l| l.price.value.raw >= trade_price_raw)
387                .collect(),
388            _ => unreachable!(),
389        };
390
391        let mut remaining = trade_size_raw;
392        for level in &levels {
393            if remaining == 0 {
394                break;
395            }
396            let level_size = level.size_raw();
397            let entry = consumption
398                .entry(level.price.value.raw)
399                .or_insert((level_size, 0));
400
401            // Reconcile stale level size to prevent reset in apply_liquidity_consumption
402            if entry.0 != level_size {
403                entry.0 = level_size;
404                entry.1 = 0;
405            }
406
407            let available = level_size.saturating_sub(entry.1);
408            let consume = min(remaining, available);
409            entry.1 += consume;
410            remaining -= consume;
411        }
412    }
413
414    /// Sets the fill model for the matching engine.
415    pub fn set_fill_model(&mut self, fill_model: FillModelHandle) {
416        self.core
417            .set_fill_limit_inside_spread(Self::fill_limit_inside_spread_or_false(&fill_model));
418        self.fill_model = fill_model;
419    }
420
421    fn fill_limit_inside_spread_or_false(fill_model: &FillModelHandle) -> bool {
422        fill_model.fill_limit_inside_spread().unwrap_or_else(|e| {
423            log::error!("Failed to query fill model spread behavior: {e}");
424            false
425        })
426    }
427
428    pub fn set_settlement_price(&mut self, price: Price) {
429        self.settlement_price = Some(price);
430    }
431
432    fn snapshot_queue_position(&mut self, order: &OrderAny, price: Price) {
433        if !self.config.queue_position {
434            return;
435        }
436        let size_prec = self.instrument.size_precision();
437
438        // Pass opposite side because get_quantity_at_level flips internally
439        // (BUY reads asks, SELL reads bids). We want the resting side depth.
440        let qty_ahead = self.book.get_quantity_at_level(
441            price,
442            OrderCore::opposite_side(order.order_side()),
443            size_prec,
444        );
445
446        let client_order_id = order.client_order_id();
447
448        // Clear stale entries from all maps (e.g. order modified to new price)
449        self.queue_pending.shift_remove(&client_order_id);
450        self.queue_ahead_total.shift_remove(&client_order_id);
451        self.queue_ahead_orders.shift_remove(&client_order_id);
452
453        // For L1 books, levels behind the BBO have no visible depth. Track
454        // these orders separately so fills are blocked until the BBO reaches
455        // this price. Only truly behind-BBO prices are pending (BUY below
456        // best bid / SELL above best ask); inside-spread and no-book keep 0.
457        if self.book_type == BookType::L1_MBP && qty_ahead.raw == 0 {
458            let behind_bbo = match order.order_side() {
459                OrderSide::Buy => self.book.best_bid_price().is_some_and(|bid| price < bid),
460                OrderSide::Sell => self.book.best_ask_price().is_some_and(|ask| price > ask),
461                _ => false,
462            };
463
464            if behind_bbo {
465                self.queue_pending.insert(client_order_id, price.raw);
466                return;
467            }
468        }
469
470        self.queue_ahead_total
471            .insert(client_order_id, (price.raw, qty_ahead.raw));
472
473        // L3 books identify orders, so track which specific orders are ahead
474        if self.book_type == BookType::L3_MBO {
475            let orders_ahead: IndexMap<OrderId, QuantityRaw> = self
476                .book
477                .get_orders_at_level(price, OrderCore::opposite_side(order.order_side()))
478                .iter()
479                .map(|book_order| (book_order.order_id, book_order.size.raw))
480                .collect();
481            self.queue_ahead_orders
482                .insert(client_order_id, orders_ahead);
483        }
484    }
485
486    fn decrement_queue_on_trade(
487        &mut self,
488        price_raw: PriceRaw,
489        trade_size_raw: QuantityRaw,
490        aggressor_side: AggressorSide,
491    ) {
492        if !self.config.queue_position {
493            return;
494        }
495
496        self.queue_excess.clear();
497
498        let keys: Vec<ClientOrderId> = self.queue_ahead_total.keys().copied().collect();
499        let mut entries: Vec<(ClientOrderId, QuantityRaw, QuantityRaw)> = Vec::new();
500        let mut stale: Vec<ClientOrderId> = Vec::new();
501
502        for client_order_id in keys {
503            let (order_price_raw, ahead_raw) =
504                match self.queue_ahead_total.get(&client_order_id).copied() {
505                    Some(v) => v,
506                    None => continue,
507                };
508
509            let cache = self.cache.borrow();
510            let order_info = cache.order(&client_order_id).and_then(|order| {
511                if order.is_closed() {
512                    None
513                } else {
514                    Some((order.order_side(), order.leaves_qty().raw))
515                }
516            });
517            drop(cache);
518
519            let Some((order_side, leaves_raw)) = order_info else {
520                stale.push(client_order_id);
521                continue;
522            };
523
524            if order_price_raw != price_raw || ahead_raw == 0 {
525                continue;
526            }
527
528            let should_decrement = matches!(aggressor_side, AggressorSide::NoAggressor)
529                || (aggressor_side == AggressorSide::Buyer && order_side == OrderSide::Sell)
530                || (aggressor_side == AggressorSide::Seller && order_side == OrderSide::Buy);
531
532            if should_decrement {
533                entries.push((client_order_id, ahead_raw, leaves_raw));
534            }
535        }
536
537        for id in stale {
538            self.queue_ahead_total.shift_remove(&id);
539            self.queue_ahead_orders.shift_remove(&id);
540        }
541
542        // Sort by queue position (earliest first) for shared budget allocation
543        entries.sort_by_key(|&(_, ahead, _)| ahead);
544
545        let mut remaining = trade_size_raw;
546        let mut prev_position: QuantityRaw = 0;
547
548        for (client_order_id, ahead_raw, leaves_raw) in &entries {
549            if remaining == 0 {
550                let new_ahead = ahead_raw.saturating_sub(trade_size_raw);
551                self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, new_ahead);
552                if new_ahead == 0 {
553                    // Queue cleared but no trade volume left for this order
554                    self.queue_excess.insert(*client_order_id, 0);
555                }
556                continue;
557            }
558
559            // Consume the gap between previous position and this order's depth
560            let gap = ahead_raw.saturating_sub(prev_position);
561            let queue_consumed = remaining.min(gap);
562            remaining -= queue_consumed;
563
564            if remaining == 0 && queue_consumed < gap {
565                let new_ahead = ahead_raw.saturating_sub(trade_size_raw);
566                self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, new_ahead);
567                continue;
568            }
569
570            self.reduce_queue_ahead(*client_order_id, price_raw, *ahead_raw, 0);
571            let excess = remaining.min(*leaves_raw);
572            self.queue_excess.insert(*client_order_id, excess);
573            remaining -= excess;
574            prev_position = ahead_raw + excess;
575        }
576    }
577
578    /// Reduces an order's quantity ahead, front-consuming its tracked orders by
579    /// the same amount so the pair stays in sync and later granular deltas for
580    /// consumed orders cannot advance the queue again.
581    fn reduce_queue_ahead(
582        &mut self,
583        client_order_id: ClientOrderId,
584        price_raw: PriceRaw,
585        ahead_raw: QuantityRaw,
586        new_ahead_raw: QuantityRaw,
587    ) {
588        self.queue_ahead_total
589            .insert(client_order_id, (price_raw, new_ahead_raw));
590        self.consume_queue_ahead_orders(client_order_id, ahead_raw.saturating_sub(new_ahead_raw));
591    }
592
593    /// Front-consumes (FIFO) the tracked orders in step with `queue_ahead_total`.
594    fn consume_queue_ahead_orders(
595        &mut self,
596        client_order_id: ClientOrderId,
597        mut amount_raw: QuantityRaw,
598    ) {
599        let Some(orders_ahead) = self.queue_ahead_orders.get_mut(&client_order_id) else {
600            return;
601        };
602
603        while amount_raw > 0 {
604            let Some((&book_order_id, &size_raw)) = orders_ahead.get_index(0) else {
605                break;
606            };
607
608            if size_raw <= amount_raw {
609                orders_ahead.shift_remove(&book_order_id);
610                amount_raw -= size_raw;
611            } else {
612                orders_ahead.insert(book_order_id, size_raw - amount_raw);
613                amount_raw = 0;
614            }
615        }
616    }
617
618    fn determine_trade_fill_qty(&self, order: &OrderAny) -> Option<QuantityRaw> {
619        if !self.config.queue_position {
620            return Some(order.leaves_qty().raw);
621        }
622
623        let client_order_id = order.client_order_id();
624
625        // Block fills for L1 orders pending a deferred snapshot
626        if self.queue_pending.contains_key(&client_order_id) {
627            return None;
628        }
629
630        if let Some(&(tracked_price_raw, ahead_raw)) = self.queue_ahead_total.get(&client_order_id)
631            && let Some(order_price) = order.price()
632            && order_price.raw == tracked_price_raw
633            && ahead_raw > 0
634        {
635            return None;
636        }
637
638        let leaves_raw = order.leaves_qty().raw;
639        if leaves_raw == 0 {
640            return None;
641        }
642
643        let mut available_raw = leaves_raw;
644
645        // Cap by remaining trade volume and queue excess (only during trade processing)
646        if let Some(trade_size) = self.last_trade_size {
647            let remaining = trade_size.raw.saturating_sub(self.trade_consumption);
648            available_raw = available_raw.min(remaining);
649
650            if let Some(&excess_raw) = self.queue_excess.get(&client_order_id) {
651                if excess_raw == 0 {
652                    return None;
653                }
654                available_raw = available_raw.min(excess_raw);
655            }
656        }
657
658        if available_raw == 0 {
659            return None;
660        }
661
662        Some(available_raw)
663    }
664
665    fn clear_all_queue_positions(&mut self) {
666        for (_, (_, ahead_raw)) in &mut self.queue_ahead_total {
667            *ahead_raw = 0;
668        }
669
670        for orders_ahead in self.queue_ahead_orders.values_mut() {
671            orders_ahead.clear();
672        }
673    }
674
675    fn adjust_queue_for_delta(&mut self, delta: &OrderBookDelta) {
676        if delta.action == BookAction::Delete {
677            if self.is_order_granular_delta(delta.flags) {
678                self.advance_l3_queue_on_delete(delta.order.order_id);
679            } else {
680                self.clear_queue_on_delete(delta.order.price.raw, delta.order.side);
681            }
682        } else if delta.action == BookAction::Update {
683            if self.is_order_granular_delta(delta.flags) {
684                self.adjust_l3_queue_on_update(&delta.order);
685            } else {
686                self.cap_queue_ahead(
687                    delta.order.price.raw,
688                    delta.order.size.raw,
689                    delta.order.side,
690                );
691            }
692        }
693    }
694
695    fn clear_queue_on_delete(&mut self, deleted_price_raw: PriceRaw, deleted_side: OrderSide) {
696        let keys: Vec<ClientOrderId> = self.queue_ahead_total.keys().copied().collect();
697        for client_order_id in keys {
698            if let Some(&(order_price_raw, ahead_raw)) =
699                self.queue_ahead_total.get(&client_order_id)
700                && order_price_raw == deleted_price_raw
701            {
702                let matches_side = self
703                    .cache
704                    .borrow()
705                    .order(&client_order_id)
706                    .is_some_and(|o| o.order_side() == deleted_side);
707
708                if matches_side {
709                    self.reduce_queue_ahead(client_order_id, order_price_raw, ahead_raw, 0);
710                }
711            }
712        }
713    }
714
715    /// Returns `true` when the delta identifies a single book order (pure MBO);
716    /// TOB/MBP-flagged deltas use level-wide handling instead.
717    fn is_order_granular_delta(&self, flags: u8) -> bool {
718        self.book_type == BookType::L3_MBO
719            && !RecordFlag::F_TOB.matches(flags)
720            && !RecordFlag::F_MBP.matches(flags)
721    }
722
723    fn advance_l3_queue_on_delete(&mut self, book_order_id: OrderId) {
724        for (client_order_id, orders_ahead) in &mut self.queue_ahead_orders {
725            let Some(size_raw) = orders_ahead.shift_remove(&book_order_id) else {
726                continue;
727            };
728
729            if let Some((_, ahead_raw)) = self.queue_ahead_total.get_mut(client_order_id) {
730                *ahead_raw = ahead_raw.saturating_sub(size_raw);
731            }
732        }
733    }
734
735    /// Adjusts tracked queues for a per-order update. A size decrease retains
736    /// time priority and advances the queue by the difference. A size increase
737    /// keeps its book FIFO slot, so it stays ahead with the larger size
738    /// (pessimistic versus venues that demote, but consistent with the book
739    /// that later snapshots read). A price move leaves the level.
740    fn adjust_l3_queue_on_update(&mut self, book_order: &BookOrder) {
741        for (client_order_id, orders_ahead) in &mut self.queue_ahead_orders {
742            let Some(&tracked_size_raw) = orders_ahead.get(&book_order.order_id) else {
743                continue;
744            };
745            let Some((tracked_price_raw, ahead_raw)) =
746                self.queue_ahead_total.get_mut(client_order_id)
747            else {
748                continue;
749            };
750
751            if book_order.price.raw != *tracked_price_raw {
752                *ahead_raw = ahead_raw.saturating_sub(tracked_size_raw);
753                orders_ahead.shift_remove(&book_order.order_id);
754            } else if book_order.size.raw < tracked_size_raw {
755                // Size decrease retains time priority
756                *ahead_raw = ahead_raw.saturating_sub(tracked_size_raw - book_order.size.raw);
757                orders_ahead.insert(book_order.order_id, book_order.size.raw);
758            } else if book_order.size.raw > tracked_size_raw {
759                *ahead_raw = ahead_raw.saturating_add(book_order.size.raw - tracked_size_raw);
760                orders_ahead.insert(book_order.order_id, book_order.size.raw);
761            }
762        }
763    }
764
765    fn cap_queue_ahead(
766        &mut self,
767        price_raw: PriceRaw,
768        size_raw: QuantityRaw,
769        order_side: OrderSide,
770    ) {
771        let keys: Vec<ClientOrderId> = self.queue_ahead_total.keys().copied().collect();
772        let mut stale: Vec<ClientOrderId> = Vec::new();
773
774        for client_order_id in keys {
775            let (order_price_raw, ahead_raw) =
776                match self.queue_ahead_total.get(&client_order_id).copied() {
777                    Some(v) => v,
778                    None => continue,
779                };
780
781            if order_price_raw != price_raw || ahead_raw <= size_raw {
782                continue;
783            }
784
785            let cache = self.cache.borrow();
786            let order_info = cache.order(&client_order_id).and_then(|order| {
787                if order.is_closed() {
788                    None
789                } else {
790                    Some(order.order_side())
791                }
792            });
793            drop(cache);
794
795            let Some(side) = order_info else {
796                stale.push(client_order_id);
797                continue;
798            };
799
800            if side != order_side {
801                continue;
802            }
803
804            self.reduce_queue_ahead(client_order_id, order_price_raw, ahead_raw, size_raw);
805        }
806
807        for id in stale {
808            self.queue_ahead_total.shift_remove(&id);
809            self.queue_ahead_orders.shift_remove(&id);
810        }
811    }
812
813    fn seed_tob_baseline(&mut self) {
814        let bid = self.book.best_bid_price();
815        let ask = self.book.best_ask_price();
816        self.prev_bid_price_raw = bid.map_or(0, |p| p.raw);
817        self.prev_bid_size_raw = self.book.best_bid_size().map_or(0, |q| q.raw);
818        self.prev_ask_price_raw = ask.map_or(0, |p| p.raw);
819        self.prev_ask_size_raw = self.book.best_ask_size().map_or(0, |q| q.raw);
820        self.tob_initialized = bid.is_some() || ask.is_some();
821    }
822
823    fn decrement_l1_queue_on_quote(
824        &mut self,
825        bid_price_raw: PriceRaw,
826        bid_size_raw: QuantityRaw,
827        ask_price_raw: PriceRaw,
828        ask_size_raw: QuantityRaw,
829    ) {
830        if !self.config.queue_position {
831            return;
832        }
833
834        // Price-move detection requires a valid prior TOB snapshot
835        if self.tob_initialized {
836            // BID side (BUY limit orders): handle price drops (crossed/snapshot)
837            if bid_price_raw < self.prev_bid_price_raw {
838                self.adjust_l1_queue_on_price_move(bid_price_raw, bid_size_raw, OrderSide::Buy);
839            }
840
841            // ASK side (SELL limit orders): handle price rises (crossed/snapshot)
842            if ask_price_raw > self.prev_ask_price_raw {
843                self.adjust_l1_queue_on_price_move(ask_price_raw, ask_size_raw, OrderSide::Sell);
844            }
845        }
846
847        // Resolve pending snapshots when BBO reaches a tracked order's price
848        self.resolve_pending_l1_snapshots(bid_price_raw, bid_size_raw, ask_price_raw, ask_size_raw);
849    }
850
851    fn adjust_l1_queue_on_price_move(
852        &mut self,
853        new_price_raw: PriceRaw,
854        new_size_raw: QuantityRaw,
855        order_side: OrderSide,
856    ) {
857        let keys: Vec<ClientOrderId> = self.queue_ahead_total.keys().copied().collect();
858        let mut stale: Vec<ClientOrderId> = Vec::new();
859
860        for client_order_id in keys {
861            let Some(&(order_price_raw, ahead_raw)) = self.queue_ahead_total.get(&client_order_id)
862            else {
863                continue;
864            };
865
866            let cache = self.cache.borrow();
867            let order_info = cache.order(&client_order_id).and_then(|order| {
868                if order.is_closed() {
869                    None
870                } else {
871                    Some(order.order_side())
872                }
873            });
874            drop(cache);
875
876            let Some(side) = order_info else {
877                stale.push(client_order_id);
878                continue;
879            };
880
881            if side != order_side {
882                continue;
883            }
884
885            // BUY orders crossed when bid drops below order price
886            // SELL orders crossed when ask rises above order price
887            let crossed = match order_side {
888                OrderSide::Buy => order_price_raw > new_price_raw,
889                _ => order_price_raw < new_price_raw,
890            };
891
892            if crossed {
893                self.queue_ahead_total
894                    .insert(client_order_id, (order_price_raw, 0));
895            } else if order_price_raw == new_price_raw && ahead_raw > new_size_raw {
896                self.queue_ahead_total
897                    .insert(client_order_id, (order_price_raw, new_size_raw));
898            }
899        }
900
901        for id in stale {
902            self.queue_ahead_total.shift_remove(&id);
903        }
904
905        // Also resolve pending L1 orders affected by this price move
906        let pending_keys: Vec<ClientOrderId> = self.queue_pending.keys().copied().collect();
907        let mut pending_stale: Vec<ClientOrderId> = Vec::new();
908
909        for client_order_id in pending_keys {
910            let Some(&order_price_raw) = self.queue_pending.get(&client_order_id) else {
911                continue;
912            };
913
914            let cache = self.cache.borrow();
915            let order_info = cache.order(&client_order_id).and_then(|order| {
916                if order.is_closed() {
917                    None
918                } else {
919                    Some(order.order_side())
920                }
921            });
922            drop(cache);
923
924            let Some(side) = order_info else {
925                pending_stale.push(client_order_id);
926                continue;
927            };
928
929            if side != order_side {
930                continue;
931            }
932
933            let crossed = match order_side {
934                OrderSide::Buy => order_price_raw > new_price_raw,
935                _ => order_price_raw < new_price_raw,
936            };
937
938            if crossed {
939                self.queue_pending.shift_remove(&client_order_id);
940                self.queue_ahead_total
941                    .insert(client_order_id, (order_price_raw, 0));
942            } else if order_price_raw == new_price_raw {
943                self.queue_pending.shift_remove(&client_order_id);
944                self.queue_ahead_total
945                    .insert(client_order_id, (order_price_raw, new_size_raw));
946            }
947        }
948
949        for id in pending_stale {
950            self.queue_pending.shift_remove(&id);
951        }
952    }
953
954    fn resolve_pending_l1_snapshots(
955        &mut self,
956        bid_price_raw: PriceRaw,
957        bid_size_raw: QuantityRaw,
958        ask_price_raw: PriceRaw,
959        ask_size_raw: QuantityRaw,
960    ) {
961        let keys: Vec<ClientOrderId> = self.queue_pending.keys().copied().collect();
962        let mut stale: Vec<ClientOrderId> = Vec::new();
963
964        for client_order_id in keys {
965            let Some(&order_price_raw) = self.queue_pending.get(&client_order_id) else {
966                continue;
967            };
968
969            let cache = self.cache.borrow();
970            let order_info = cache.order(&client_order_id).and_then(|order| {
971                if order.is_closed() {
972                    None
973                } else {
974                    Some(order.order_side())
975                }
976            });
977            drop(cache);
978
979            let Some(side) = order_info else {
980                stale.push(client_order_id);
981                continue;
982            };
983
984            // Initialize snapshot when BBO reaches the order's price level
985            let matched_size = match side {
986                OrderSide::Buy if order_price_raw == bid_price_raw => Some(bid_size_raw),
987                OrderSide::Sell if order_price_raw == ask_price_raw => Some(ask_size_raw),
988                _ => None,
989            };
990
991            if let Some(size) = matched_size {
992                self.queue_pending.shift_remove(&client_order_id);
993                self.queue_ahead_total
994                    .insert(client_order_id, (order_price_raw, size));
995            }
996        }
997
998        for id in stale {
999            self.queue_pending.shift_remove(&id);
1000        }
1001    }
1002
1003    fn resolve_pending_on_trade(&mut self, trade_price_raw: PriceRaw) {
1004        let keys: Vec<ClientOrderId> = self.queue_pending.keys().copied().collect();
1005        let mut stale: Vec<ClientOrderId> = Vec::new();
1006
1007        for client_order_id in keys {
1008            let Some(&order_price_raw) = self.queue_pending.get(&client_order_id) else {
1009                continue;
1010            };
1011
1012            let cache = self.cache.borrow();
1013            let order_side = cache.order(&client_order_id).and_then(|order| {
1014                if order.is_closed() {
1015                    None
1016                } else {
1017                    Some(order.order_side())
1018                }
1019            });
1020            drop(cache);
1021
1022            let Some(side) = order_side else {
1023                stale.push(client_order_id);
1024                continue;
1025            };
1026
1027            // Trade through a pending level proves the queue was crossed
1028            let crossed = match side {
1029                OrderSide::Buy => trade_price_raw < order_price_raw,
1030                OrderSide::Sell => trade_price_raw > order_price_raw,
1031                _ => false,
1032            };
1033
1034            if crossed {
1035                self.queue_pending.shift_remove(&client_order_id);
1036                self.queue_ahead_total
1037                    .insert(client_order_id, (order_price_raw, 0));
1038            }
1039        }
1040
1041        for id in stale {
1042            self.queue_pending.shift_remove(&id);
1043        }
1044    }
1045
1046    #[must_use]
1047    /// Returns the best bid price from the order book.
1048    pub fn best_bid_price(&self) -> Option<Price> {
1049        self.book.best_bid_price()
1050    }
1051
1052    #[must_use]
1053    /// Returns the best ask price from the order book.
1054    pub fn best_ask_price(&self) -> Option<Price> {
1055        self.book.best_ask_price()
1056    }
1057
1058    #[must_use]
1059    /// Returns a reference to the internal order book.
1060    pub const fn get_book(&self) -> &OrderBook {
1061        &self.book
1062    }
1063
1064    #[must_use]
1065    /// Returns all open bid orders managed by the matching core.
1066    pub fn get_open_bid_orders(&self) -> Vec<RestingOrder> {
1067        self.core.get_orders_bid()
1068    }
1069
1070    #[must_use]
1071    /// Returns all open ask orders managed by the matching core.
1072    pub fn get_open_ask_orders(&self) -> Vec<RestingOrder> {
1073        self.core.get_orders_ask()
1074    }
1075
1076    #[must_use]
1077    /// Returns all open orders from both bid and ask sides.
1078    pub fn get_open_orders(&self) -> Vec<RestingOrder> {
1079        self.core.get_orders()
1080    }
1081
1082    #[must_use]
1083    /// Returns true if an order with the given client order ID exists in the matching engine.
1084    pub fn order_exists(&self, client_order_id: ClientOrderId) -> bool {
1085        self.core.order_exists(client_order_id)
1086    }
1087
1088    #[must_use]
1089    /// Returns the number of partial-fill counters tracked by the engine.
1090    pub fn cached_filled_qty_len(&self) -> usize {
1091        self.cached_filled_qty.len()
1092    }
1093
1094    #[must_use]
1095    pub const fn get_core(&self) -> &OrderMatchingCore {
1096        &self.core
1097    }
1098
1099    pub fn set_fill_at_market(&mut self, value: bool) {
1100        self.fill_at_market = value;
1101    }
1102
1103    /// Updates the instrument definition used by this matching engine.
1104    ///
1105    /// # Errors
1106    ///
1107    /// Returns an error if `instrument.id()` does not match this engines instrument ID.
1108    pub fn update_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> {
1109        if instrument.id() != self.instrument.id() {
1110            anyhow::bail!(
1111                "Cannot update instrument {} with {}",
1112                self.instrument.id(),
1113                instrument.id()
1114            );
1115        }
1116
1117        let changed = instrument.price_increment() != self.instrument.price_increment()
1118            || instrument.price_precision() != self.instrument.price_precision()
1119            || instrument.size_precision() != self.instrument.size_precision();
1120
1121        if changed {
1122            self.core
1123                .update_price_increment(instrument.price_increment());
1124            self.book.reset();
1125            self.trade_consumption = 0;
1126            self.bid_consumption.clear();
1127            self.ask_consumption.clear();
1128            self.queue_pending.clear();
1129            self.queue_ahead_orders.clear();
1130            self.queue_ahead_total.clear();
1131            self.queue_excess.clear();
1132            self.prev_bid_price_raw = 0;
1133            self.prev_bid_size_raw = 0;
1134            self.prev_ask_price_raw = 0;
1135            self.prev_ask_size_raw = 0;
1136            self.tob_initialized = false;
1137            self.last_quote_bid = None;
1138            self.last_quote_ask = None;
1139            self.precision_mismatch_streak = 0;
1140            self.target_bid = None;
1141            self.target_ask = None;
1142            self.target_last = None;
1143            self.last_bar_bid = None;
1144            self.last_bar_ask = None;
1145            self.core.bid = None;
1146            self.core.ask = None;
1147            self.core.last = None;
1148            log::info!(
1149                "Updated instrument {} (price_precision={} size_precision={})",
1150                instrument.id(),
1151                instrument.price_precision(),
1152                instrument.size_precision()
1153            );
1154        }
1155
1156        self.instrument = instrument;
1157
1158        if changed {
1159            self.drop_incompatible_core_orders();
1160        }
1161        Ok(())
1162    }
1163
1164    fn check_price_precision(&self, actual: u8, field: &str) -> anyhow::Result<()> {
1165        let expected = self.instrument.price_precision();
1166        if actual != expected {
1167            anyhow::bail!(
1168                "Invalid {field} precision {actual}, expected {expected} for {}",
1169                self.instrument.id()
1170            );
1171        }
1172        Ok(())
1173    }
1174
1175    fn check_size_precision(&self, actual: u8, field: &str) -> anyhow::Result<()> {
1176        let expected = self.instrument.size_precision();
1177        if actual != expected {
1178            anyhow::bail!(
1179                "Invalid {field} precision {actual}, expected {expected} for {}",
1180                self.instrument.id()
1181            );
1182        }
1183        Ok(())
1184    }
1185
1186    fn log_precision_mismatch(
1187        &mut self,
1188        data_type: &str,
1189        instrument_id: InstrumentId,
1190        err: &anyhow::Error,
1191    ) {
1192        self.precision_mismatch_streak = self.precision_mismatch_streak.saturating_add(1);
1193        let streak = self.precision_mismatch_streak;
1194
1195        if streak <= 3 || streak.is_multiple_of(100) {
1196            log::warn!(
1197                "Skipping {data_type} for {instrument_id}: {err} \
1198                 (consecutive_precision_mismatches={streak})"
1199            );
1200        }
1201
1202        if streak == 20 {
1203            log::error!(
1204                "Precision mismatches reached {streak} consecutive events for \
1205                 {instrument_id}; check instrument update flow and upstream market data"
1206            );
1207        }
1208    }
1209
1210    fn drop_incompatible_core_orders(&mut self) {
1211        let client_order_ids: Vec<ClientOrderId> = self
1212            .core
1213            .iter_orders()
1214            .filter(|order| {
1215                !self.resting_order_matches_current_instrument(order)
1216                    || !self.cached_order_matches_current_instrument(order.client_order_id)
1217            })
1218            .map(|order| order.client_order_id)
1219            .collect();
1220
1221        for client_order_id in client_order_ids {
1222            let order = self
1223                .cache
1224                .borrow()
1225                .order(&client_order_id)
1226                .map(|o| o.clone());
1227
1228            if let Some(order) = order
1229                && (order.is_inflight() || order.is_open())
1230            {
1231                log::warn!(
1232                    "Canceling order {client_order_id} after instrument update: \
1233                     price, trigger price, or quantity is not compatible with {}",
1234                    self.instrument.id()
1235                );
1236                self.cancel_order(&order, None);
1237            } else {
1238                self.delete_core_order(client_order_id);
1239                self.cached_filled_qty.swap_remove(&client_order_id);
1240            }
1241        }
1242    }
1243
1244    fn cached_order_matches_current_instrument(&self, client_order_id: ClientOrderId) -> bool {
1245        self.cache
1246            .borrow()
1247            .order(&client_order_id)
1248            .is_none_or(|order| {
1249                Self::quantity_matches_precision(order.quantity(), self.instrument.size_precision())
1250            })
1251    }
1252
1253    fn resting_order_matches_current_instrument(&self, order: &RestingOrder) -> bool {
1254        order
1255            .limit_price
1256            .is_none_or(|price| self.price_matches_current_instrument(price))
1257            && order
1258                .trigger_price
1259                .is_none_or(|price| self.price_matches_current_instrument(price))
1260    }
1261
1262    fn price_matches_current_instrument(&self, price: Price) -> bool {
1263        Self::price_matches_precision(price, self.instrument.price_precision())
1264            && Self::price_matches_tick(price, self.instrument.price_increment())
1265    }
1266
1267    fn price_matches_precision(price: Price, precision: u8) -> bool {
1268        let precision_diff = FIXED_PRECISION.saturating_sub(precision);
1269        let scale = PriceRaw::pow(10, u32::from(precision_diff));
1270        price.raw % scale == 0
1271    }
1272
1273    fn price_matches_tick(price: Price, increment: Price) -> bool {
1274        let increment_raw = increment.raw.abs();
1275        increment_raw == 0 || price.raw % increment_raw == 0
1276    }
1277
1278    fn quantity_matches_precision(quantity: Quantity, precision: u8) -> bool {
1279        let precision_diff = FIXED_PRECISION.saturating_sub(precision);
1280        let scale = QuantityRaw::pow(10, u32::from(precision_diff));
1281        quantity.raw.is_multiple_of(scale)
1282    }
1283
1284    fn normalize_price_for_current_instrument(&self, price: Price) -> Option<Price> {
1285        if !self.price_matches_current_instrument(price) {
1286            return None;
1287        }
1288
1289        Some(Price::from_raw(
1290            price.raw,
1291            self.instrument.price_precision(),
1292        ))
1293    }
1294
1295    fn normalize_quantity_for_current_instrument(&self, quantity: Quantity) -> Option<Quantity> {
1296        let precision = self.instrument.size_precision();
1297        if !Self::quantity_matches_precision(quantity, precision) {
1298            return None;
1299        }
1300
1301        Some(Quantity::from_raw(quantity.raw, precision))
1302    }
1303
1304    /// Process the venues market for the given order book delta.
1305    ///
1306    /// # Errors
1307    ///
1308    /// - If delta order price precision does not match the instrument (for Add/Update actions).
1309    /// - If delta order size precision does not match the instrument (for Add/Update actions).
1310    /// - If applying the delta to the book fails.
1311    pub fn process_order_book_delta(&mut self, delta: &OrderBookDelta) -> anyhow::Result<()> {
1312        log::debug!("Processing {delta}");
1313
1314        // Validate precision for Add and Update actions (Delete/Clear may have NULL_ORDER)
1315        if matches!(delta.action, BookAction::Add | BookAction::Update) {
1316            self.check_price_precision(delta.order.price.precision, "delta order price")?;
1317            self.check_size_precision(delta.order.size.precision, "delta order size")?;
1318        }
1319
1320        // L1 books are driven by top-of-book data only, ignore deltas
1321        if self.book_type == BookType::L1_MBP {
1322            self.iterate(delta.ts_init, AggressorSide::NoAggressor);
1323            return Ok(());
1324        }
1325
1326        self.book.apply_delta(delta)?;
1327
1328        let delta_snapshot_or_clear = (delta.flags & 32) != 0 || delta.action == BookAction::Clear;
1329
1330        if self.config.queue_position {
1331            if delta_snapshot_or_clear {
1332                self.clear_all_queue_positions();
1333            } else {
1334                self.adjust_queue_for_delta(delta);
1335            }
1336        }
1337
1338        if self.config.queue_position && delta_snapshot_or_clear {
1339            self.seed_tob_baseline();
1340        }
1341
1342        self.iterate(delta.ts_init, AggressorSide::NoAggressor);
1343        Ok(())
1344    }
1345
1346    /// Process the venues market for the given order book deltas.
1347    ///
1348    /// # Errors
1349    ///
1350    /// - If any delta order price precision does not match the instrument (for Add/Update actions).
1351    /// - If any delta order size precision does not match the instrument (for Add/Update actions).
1352    /// - If applying the deltas to the book fails.
1353    pub fn process_order_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
1354        log::debug!("Processing {deltas}");
1355
1356        // Validate precision for Add and Update actions (Delete/Clear may have NULL_ORDER)
1357        for delta in &deltas.deltas {
1358            if matches!(delta.action, BookAction::Add | BookAction::Update) {
1359                self.check_price_precision(delta.order.price.precision, "delta order price")?;
1360                self.check_size_precision(delta.order.size.precision, "delta order size")?;
1361            }
1362        }
1363
1364        // L1 books are driven by top-of-book data only, ignore deltas
1365        if self.book_type == BookType::L1_MBP {
1366            self.iterate(deltas.ts_init, AggressorSide::NoAggressor);
1367            return Ok(());
1368        }
1369
1370        self.book.apply_deltas(deltas)?;
1371
1372        let mut has_snapshot_or_clear = false;
1373
1374        if self.config.queue_position {
1375            for delta in &deltas.deltas {
1376                if (delta.flags & 32) != 0 || delta.action == BookAction::Clear {
1377                    self.clear_all_queue_positions();
1378                    has_snapshot_or_clear = true;
1379                    break;
1380                }
1381                self.adjust_queue_for_delta(delta);
1382            }
1383        }
1384
1385        if self.config.queue_position && has_snapshot_or_clear {
1386            self.seed_tob_baseline();
1387        }
1388
1389        self.iterate(deltas.ts_init, AggressorSide::NoAggressor);
1390        Ok(())
1391    }
1392
1393    /// Process the venues market for the given order book depth10.
1394    ///
1395    /// # Errors
1396    ///
1397    /// - If any bid/ask price precision does not match the instrument.
1398    /// - If any bid/ask size precision does not match the instrument.
1399    /// - If applying the depth to the book fails.
1400    /// - If updating the L1 order book with the top-of-book quote fails.
1401    pub fn process_order_book_depth10(&mut self, depth: &OrderBookDepth10) -> anyhow::Result<()> {
1402        log::debug!("Processing OrderBookDepth10 for {}", depth.instrument_id);
1403
1404        // Validate precision for non-padding entries
1405        for order in &depth.bids {
1406            if order.side == OrderSide::NoOrderSide || !order.size.is_positive() {
1407                continue;
1408            }
1409            self.check_price_precision(order.price.precision, "bid price")?;
1410            self.check_size_precision(order.size.precision, "bid size")?;
1411        }
1412
1413        for order in &depth.asks {
1414            if order.side == OrderSide::NoOrderSide || !order.size.is_positive() {
1415                continue;
1416            }
1417            self.check_price_precision(order.price.precision, "ask price")?;
1418            self.check_size_precision(order.size.precision, "ask size")?;
1419        }
1420
1421        let top_bid = Self::first_valid_depth_order(&depth.bids, OrderSide::Buy);
1422        let top_ask = Self::first_valid_depth_order(&depth.asks, OrderSide::Sell);
1423
1424        // For L1 books, only apply top-of-book to avoid mispricing
1425        // against worst-level entries when full depth is applied
1426        if self.book_type == BookType::L1_MBP {
1427            let quote = QuoteTick::new(
1428                depth.instrument_id,
1429                Self::depth_quote_price(top_bid, self.instrument.price_precision()),
1430                Self::depth_quote_price(top_ask, self.instrument.price_precision()),
1431                Self::depth_quote_size(top_bid, self.instrument.size_precision()),
1432                Self::depth_quote_size(top_ask, self.instrument.size_precision()),
1433                depth.ts_event,
1434                depth.ts_init,
1435            );
1436            self.book.update_quote_tick(&quote)?;
1437            self.last_quote_bid = top_bid.map(|order| order.price);
1438            self.last_quote_ask = top_ask.map(|order| order.price);
1439        } else {
1440            self.book.apply_depth(depth)?;
1441        }
1442
1443        // Depth10 always replaces the full book via apply_depth regardless of flags
1444        if self.config.queue_position {
1445            self.clear_all_queue_positions();
1446            let bid_price_raw = top_bid.map_or(0, |order| order.price.raw);
1447            let bid_size_raw = top_bid.map_or(0, |order| order.size.raw);
1448            let ask_price_raw = top_ask.map_or(0, |order| order.price.raw);
1449            let ask_size_raw = top_ask.map_or(0, |order| order.size.raw);
1450
1451            // Handle crossed/matched pending orders (same as quote path)
1452            if self.tob_initialized {
1453                if bid_price_raw < self.prev_bid_price_raw {
1454                    self.adjust_l1_queue_on_price_move(bid_price_raw, bid_size_raw, OrderSide::Buy);
1455                }
1456
1457                if ask_price_raw > self.prev_ask_price_raw {
1458                    self.adjust_l1_queue_on_price_move(
1459                        ask_price_raw,
1460                        ask_size_raw,
1461                        OrderSide::Sell,
1462                    );
1463                }
1464            }
1465
1466            self.resolve_pending_l1_snapshots(
1467                bid_price_raw,
1468                bid_size_raw,
1469                ask_price_raw,
1470                ask_size_raw,
1471            );
1472
1473            self.prev_bid_price_raw = bid_price_raw;
1474            self.prev_bid_size_raw = bid_size_raw;
1475            self.prev_ask_price_raw = ask_price_raw;
1476            self.prev_ask_size_raw = ask_size_raw;
1477            self.tob_initialized = true;
1478        }
1479
1480        self.iterate(depth.ts_init, AggressorSide::NoAggressor);
1481        Ok(())
1482    }
1483
1484    fn first_valid_depth_order(orders: &[BookOrder], side: OrderSide) -> Option<BookOrder> {
1485        orders
1486            .iter()
1487            .copied()
1488            .find(|order| order.side == side && order.size.is_positive())
1489    }
1490
1491    fn depth_quote_price(order: Option<BookOrder>, price_precision: u8) -> Price {
1492        order.map_or_else(|| Price::zero(price_precision), |order| order.price)
1493    }
1494
1495    fn depth_quote_size(order: Option<BookOrder>, size_precision: u8) -> Quantity {
1496        order.map_or_else(|| Quantity::zero(size_precision), |order| order.size)
1497    }
1498
1499    /// Processes a quote tick to update the market state.
1500    pub fn process_quote_tick(&mut self, quote: &QuoteTick) {
1501        log::debug!("Processing {quote}");
1502
1503        if let Err(e) = self.check_price_precision(quote.bid_price.precision, "bid_price") {
1504            self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1505            return;
1506        }
1507
1508        if let Err(e) = self.check_price_precision(quote.ask_price.precision, "ask_price") {
1509            self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1510            return;
1511        }
1512
1513        if let Err(e) = self.check_size_precision(quote.bid_size.precision, "bid_size") {
1514            self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1515            return;
1516        }
1517
1518        if let Err(e) = self.check_size_precision(quote.ask_size.precision, "ask_size") {
1519            self.log_precision_mismatch("quote tick", quote.instrument_id, &e);
1520            return;
1521        }
1522
1523        self.precision_mismatch_streak = 0;
1524
1525        if self.book_type == BookType::L1_MBP {
1526            // Stale update: skip book mutation and cache updates
1527            if quote.ts_event < self.book.ts_last {
1528                log::warn!(
1529                    "Skipping stale quote: ts_event {} < book.ts_last {} for {}",
1530                    quote.ts_event,
1531                    self.book.ts_last,
1532                    self.book.instrument_id,
1533                );
1534                self.iterate(quote.ts_init, AggressorSide::NoAggressor);
1535                return;
1536            }
1537
1538            if !self.update_quote_tick_or_skip(quote, "quote tick") {
1539                return;
1540            }
1541
1542            if self.config.queue_position {
1543                self.decrement_l1_queue_on_quote(
1544                    quote.bid_price.raw,
1545                    quote.bid_size.raw,
1546                    quote.ask_price.raw,
1547                    quote.ask_size.raw,
1548                );
1549                self.prev_bid_price_raw = quote.bid_price.raw;
1550                self.prev_bid_size_raw = quote.bid_size.raw;
1551                self.prev_ask_price_raw = quote.ask_price.raw;
1552                self.prev_ask_size_raw = quote.ask_size.raw;
1553                self.tob_initialized = true;
1554            }
1555            self.last_quote_bid = Some(quote.bid_price);
1556            self.last_quote_ask = Some(quote.ask_price);
1557        }
1558
1559        self.iterate(quote.ts_init, AggressorSide::NoAggressor);
1560    }
1561
1562    /// Processes a bar and simulates market dynamics by creating synthetic ticks.
1563    ///
1564    /// For L1 books with bar execution enabled, generates synthetic trade or quote
1565    /// ticks from bar OHLC data to drive order matching.
1566    ///
1567    /// # Panics
1568    ///
1569    /// - If the bar type configuration is missing a time delta.
1570    pub fn process_bar(&mut self, bar: &Bar) {
1571        log::debug!("Processing {bar}");
1572
1573        debug_assert!(
1574            bar.high >= bar.open
1575                && bar.high >= bar.low
1576                && bar.high >= bar.close
1577                && bar.low <= bar.open
1578                && bar.low <= bar.close,
1579            "OHLC invariant violated for {bar}"
1580        );
1581
1582        // Check if configured for bar execution can only process an L1 book with bars
1583        if !self.config.bar_execution || self.book_type != BookType::L1_MBP {
1584            return;
1585        }
1586
1587        let bar_type = bar.bar_type;
1588        // Do not process internally aggregated bars
1589        if bar_type.aggregation_source() == AggregationSource::Internal {
1590            return;
1591        }
1592
1593        if let Err(e) = self.check_price_precision(bar.open.precision, "bar open") {
1594            self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1595            return;
1596        }
1597
1598        if let Err(e) = self.check_price_precision(bar.high.precision, "bar high") {
1599            self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1600            return;
1601        }
1602
1603        if let Err(e) = self.check_price_precision(bar.low.precision, "bar low") {
1604            self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1605            return;
1606        }
1607
1608        if let Err(e) = self.check_price_precision(bar.close.precision, "bar close") {
1609            self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1610            return;
1611        }
1612
1613        if let Err(e) = self.check_size_precision(bar.volume.precision, "bar volume") {
1614            self.log_precision_mismatch("bar", bar.instrument_id(), &e);
1615            return;
1616        }
1617
1618        self.precision_mismatch_streak = 0;
1619
1620        let price_type = bar_type.spec().price_type;
1621        if price_type == PriceType::Mark {
1622            log::warn!(
1623                "Cannot process bar for {} with `PriceType::Mark`, mark price bars are not supported for bar execution",
1624                bar.instrument_id(),
1625            );
1626            return;
1627        }
1628
1629        let execution_bar_type =
1630            if let Some(execution_bar_type) = self.execution_bar_types.get(&bar.instrument_id()) {
1631                execution_bar_type.to_owned()
1632            } else {
1633                self.execution_bar_types
1634                    .insert(bar.instrument_id(), bar_type);
1635                self.execution_bar_deltas
1636                    .insert(bar_type, bar_type.spec().timedelta());
1637                bar_type
1638            };
1639
1640        if execution_bar_type != bar_type {
1641            let mut bar_type_timedelta = self.execution_bar_deltas.get(&bar_type).copied();
1642            if bar_type_timedelta.is_none() {
1643                bar_type_timedelta = Some(bar_type.spec().timedelta());
1644                self.execution_bar_deltas
1645                    .insert(bar_type, bar_type_timedelta.unwrap());
1646            }
1647
1648            if self.execution_bar_deltas.get(&execution_bar_type).unwrap()
1649                >= &bar_type_timedelta.unwrap()
1650            {
1651                self.execution_bar_types
1652                    .insert(bar_type.instrument_id(), bar_type);
1653            } else {
1654                return;
1655            }
1656        }
1657
1658        match price_type {
1659            PriceType::Last | PriceType::Mid => self.process_trade_ticks_from_bar(bar),
1660            PriceType::Bid => {
1661                self.last_bar_bid = Some(bar.to_owned());
1662                self.process_quote_ticks_from_bar();
1663            }
1664            PriceType::Ask => {
1665                self.last_bar_ask = Some(bar.to_owned());
1666                self.process_quote_ticks_from_bar();
1667            }
1668            PriceType::Mark => {
1669                unreachable!("PriceType::Mark bars return before execution bar state updates")
1670            }
1671        }
1672    }
1673
1674    fn process_trade_ticks_from_bar(&mut self, bar: &Bar) {
1675        let sizes = BarTickSizes::from_volume(bar.volume, self.instrument.size_increment());
1676
1677        let aggressor_side = if self.core.last.is_none_or(|last| bar.open > last) {
1678            AggressorSide::Buyer
1679        } else {
1680            AggressorSide::Seller
1681        };
1682
1683        // Open: fill at market price (gap from previous bar)
1684        if self.core.last.is_none() {
1685            self.fill_at_market = true;
1686
1687            if !self.process_bar_trade_tick(
1688                bar,
1689                bar.open,
1690                sizes.open,
1691                aggressor_side,
1692                "bar open trade tick",
1693            ) {
1694                return;
1695            }
1696            self.core.set_last_raw(bar.open);
1697        } else if self.core.last.is_some_and(|last| bar.open != last) {
1698            // Gap between previous close and this bar's open
1699            self.fill_at_market = true;
1700
1701            if !self.process_bar_trade_tick(
1702                bar,
1703                bar.open,
1704                sizes.open,
1705                aggressor_side,
1706                "bar gap-open trade tick",
1707            ) {
1708                return;
1709            }
1710            self.core.set_last_raw(bar.open);
1711        }
1712
1713        // Determine high/low processing order.
1714        // Default: O > H > L > C. With adaptive ordering, swap if low is closer to open.
1715        let high_first = !self.config.bar_adaptive_high_low_ordering
1716            || (bar.high.raw - bar.open.raw).abs() < (bar.low.raw - bar.open.raw).abs();
1717
1718        if high_first {
1719            self.process_bar_high(bar, sizes.high);
1720            self.process_bar_low(bar, sizes.low);
1721        } else {
1722            self.process_bar_low(bar, sizes.low);
1723            self.process_bar_high(bar, sizes.high);
1724        }
1725
1726        // Close: fill at trigger price (market moving through prices)
1727        if self.core.last.is_some_and(|last| bar.close != last) {
1728            self.fill_at_market = false;
1729
1730            let aggressor_side = if bar.close > self.core.last.unwrap() {
1731                AggressorSide::Buyer
1732            } else {
1733                AggressorSide::Seller
1734            };
1735
1736            if !self.process_bar_trade_tick(
1737                bar,
1738                bar.close,
1739                sizes.close,
1740                aggressor_side,
1741                "bar close trade tick",
1742            ) {
1743                return;
1744            }
1745
1746            self.core.set_last_raw(bar.close);
1747        }
1748
1749        self.fill_at_market = true;
1750    }
1751
1752    fn process_bar_high(&mut self, bar: &Bar, size: Quantity) {
1753        if self.core.last.is_some_and(|last| bar.high > last) {
1754            self.fill_at_market = false;
1755
1756            if !self.process_bar_trade_tick(
1757                bar,
1758                bar.high,
1759                size,
1760                AggressorSide::Buyer,
1761                "bar high trade tick",
1762            ) {
1763                return;
1764            }
1765
1766            self.core.set_last_raw(bar.high);
1767        }
1768    }
1769
1770    fn process_bar_low(&mut self, bar: &Bar, size: Quantity) {
1771        if self.core.last.is_some_and(|last| bar.low < last) {
1772            self.fill_at_market = false;
1773
1774            if !self.process_bar_trade_tick(
1775                bar,
1776                bar.low,
1777                size,
1778                AggressorSide::Seller,
1779                "bar low trade tick",
1780            ) {
1781                return;
1782            }
1783
1784            self.core.set_last_raw(bar.low);
1785        }
1786    }
1787
1788    fn process_bar_trade_tick(
1789        &mut self,
1790        bar: &Bar,
1791        price: Price,
1792        size: Quantity,
1793        aggressor_side: AggressorSide,
1794        context: &str,
1795    ) -> bool {
1796        if size.is_zero() {
1797            return true;
1798        }
1799
1800        let trade_tick = TradeTick::new(
1801            bar.instrument_id(),
1802            price,
1803            size,
1804            aggressor_side,
1805            self.ids_generator.generate_trade_id(bar.ts_init),
1806            bar.ts_init,
1807            bar.ts_init,
1808        );
1809
1810        if !self.update_trade_tick_or_skip(&trade_tick, context) {
1811            return false;
1812        }
1813
1814        self.iterate(trade_tick.ts_init, AggressorSide::NoAggressor);
1815        true
1816    }
1817
1818    fn process_quote_ticks_from_bar(&mut self) {
1819        // Wait for next bar
1820        if self.last_bar_bid.is_none()
1821            || self.last_bar_ask.is_none()
1822            || self.last_bar_bid.unwrap().ts_init != self.last_bar_ask.unwrap().ts_init
1823        {
1824            return;
1825        }
1826        let bid_bar = self.last_bar_bid.unwrap();
1827        let ask_bar = self.last_bar_ask.unwrap();
1828
1829        let size_increment = self.instrument.size_increment();
1830        let bid_sizes = BarTickSizes::from_volume(bid_bar.volume, size_increment);
1831        let ask_sizes = BarTickSizes::from_volume(ask_bar.volume, size_increment);
1832        let mut has_current_bid = false;
1833        let mut has_current_ask = false;
1834
1835        let mut quote_tick = QuoteTick::new(
1836            self.book.instrument_id,
1837            bid_bar.open,
1838            ask_bar.open,
1839            bid_sizes.open,
1840            ask_sizes.open,
1841            bid_bar.ts_init,
1842            bid_bar.ts_init,
1843        );
1844
1845        // Open: fill at market price (gap from previous bar)
1846        self.fill_at_market = true;
1847
1848        if !self.process_bar_quote_tick(
1849            &quote_tick,
1850            "bar open quote tick",
1851            &mut has_current_bid,
1852            &mut has_current_ask,
1853        ) {
1854            return;
1855        }
1856
1857        // Determine high/low processing order from the bid bar (v1 parity).
1858        // Default: O > H > L > C. With adaptive ordering, swap if low is closer to open
1859        let high_first = !self.config.bar_adaptive_high_low_ordering
1860            || (bid_bar.high.raw - bid_bar.open.raw).abs()
1861                < (bid_bar.low.raw - bid_bar.open.raw).abs();
1862
1863        let high_leg = (
1864            bid_bar.high,
1865            ask_bar.high,
1866            bid_sizes.high,
1867            ask_sizes.high,
1868            "bar high quote tick",
1869        );
1870        let low_leg = (
1871            bid_bar.low,
1872            ask_bar.low,
1873            bid_sizes.low,
1874            ask_sizes.low,
1875            "bar low quote tick",
1876        );
1877        let legs = if high_first {
1878            [high_leg, low_leg]
1879        } else {
1880            [low_leg, high_leg]
1881        };
1882
1883        // High/low: fill at trigger price (market moving through prices)
1884        for (bid_price, ask_price, bid_size, ask_size, context) in legs {
1885            self.fill_at_market = false;
1886            quote_tick.bid_price = bid_price;
1887            quote_tick.ask_price = ask_price;
1888            quote_tick.bid_size = bid_size;
1889            quote_tick.ask_size = ask_size;
1890
1891            if !self.process_bar_quote_tick(
1892                &quote_tick,
1893                context,
1894                &mut has_current_bid,
1895                &mut has_current_ask,
1896            ) {
1897                return;
1898            }
1899        }
1900
1901        // Close: fill at trigger price (market moving through prices)
1902        self.fill_at_market = false;
1903        quote_tick.bid_price = bid_bar.close;
1904        quote_tick.ask_price = ask_bar.close;
1905        quote_tick.bid_size = bid_sizes.close;
1906        quote_tick.ask_size = ask_sizes.close;
1907
1908        if !self.process_bar_quote_tick(
1909            &quote_tick,
1910            "bar close quote tick",
1911            &mut has_current_bid,
1912            &mut has_current_ask,
1913        ) {
1914            return;
1915        }
1916
1917        self.last_bar_bid = None;
1918        self.last_bar_ask = None;
1919        self.fill_at_market = true;
1920    }
1921
1922    fn process_bar_quote_tick(
1923        &mut self,
1924        quote: &QuoteTick,
1925        context: &str,
1926        has_current_bid: &mut bool,
1927        has_current_ask: &mut bool,
1928    ) -> bool {
1929        let has_bid_size = !quote.bid_size.is_zero();
1930        let has_ask_size = !quote.ask_size.is_zero();
1931        let mut book_changed = false;
1932        let mut bid_cleared = false;
1933        let mut ask_cleared = false;
1934
1935        match (has_bid_size, has_ask_size) {
1936            (true, true) => {
1937                if !self.update_quote_tick_or_skip(quote, context) {
1938                    return false;
1939                }
1940                *has_current_bid = true;
1941                *has_current_ask = true;
1942                book_changed = true;
1943            }
1944            _ => {
1945                if has_bid_size {
1946                    self.update_bar_quote_bid(quote);
1947                    *has_current_bid = true;
1948                    book_changed = true;
1949                } else if !*has_current_bid {
1950                    self.clear_bar_quote_bid(quote);
1951                    *has_current_bid = true;
1952                    book_changed = true;
1953                    bid_cleared = true;
1954                }
1955
1956                if has_ask_size {
1957                    self.update_bar_quote_ask(quote);
1958                    *has_current_ask = true;
1959                    book_changed = true;
1960                } else if !*has_current_ask {
1961                    self.clear_bar_quote_ask(quote);
1962                    *has_current_ask = true;
1963                    book_changed = true;
1964                    ask_cleared = true;
1965                }
1966            }
1967        }
1968
1969        if book_changed
1970            && let (Some(best_bid), Some(best_ask)) =
1971                (self.book.best_bid_price(), self.book.best_ask_price())
1972            && best_bid > best_ask
1973        {
1974            if has_bid_size && !has_ask_size {
1975                self.clear_bar_quote_ask(quote);
1976                ask_cleared = true;
1977            } else if has_ask_size && !has_bid_size {
1978                self.clear_bar_quote_bid(quote);
1979                bid_cleared = true;
1980            }
1981        }
1982
1983        if has_bid_size {
1984            self.last_quote_bid = Some(quote.bid_price);
1985        } else if bid_cleared {
1986            self.last_quote_bid = None;
1987        }
1988
1989        if has_ask_size {
1990            self.last_quote_ask = Some(quote.ask_price);
1991        } else if ask_cleared {
1992            self.last_quote_ask = None;
1993        }
1994
1995        if !book_changed {
1996            return true;
1997        }
1998
1999        self.iterate(quote.ts_init, AggressorSide::NoAggressor);
2000        true
2001    }
2002
2003    fn update_bar_quote_bid(&mut self, quote: &QuoteTick) {
2004        let bid = BookOrder::new(
2005            OrderSide::Buy,
2006            quote.bid_price,
2007            quote.bid_size,
2008            OrderSide::Buy as u64,
2009        );
2010        self.book
2011            .add(bid, 0, self.book.sequence.saturating_add(1), quote.ts_event);
2012    }
2013
2014    fn clear_bar_quote_bid(&mut self, quote: &QuoteTick) {
2015        self.book
2016            .clear_bids(self.book.sequence.saturating_add(1), quote.ts_event);
2017    }
2018
2019    fn update_bar_quote_ask(&mut self, quote: &QuoteTick) {
2020        let ask = BookOrder::new(
2021            OrderSide::Sell,
2022            quote.ask_price,
2023            quote.ask_size,
2024            OrderSide::Sell as u64,
2025        );
2026        self.book
2027            .add(ask, 0, self.book.sequence.saturating_add(1), quote.ts_event);
2028    }
2029
2030    fn clear_bar_quote_ask(&mut self, quote: &QuoteTick) {
2031        self.book
2032            .clear_asks(self.book.sequence.saturating_add(1), quote.ts_event);
2033    }
2034
2035    /// Processes a trade tick to update the market state.
2036    ///
2037    /// For L1 books, always updates the order book with the trade tick to maintain
2038    /// market state. When `trade_execution` is disabled, order matching and maintenance
2039    /// operations (GTD order expiry, trailing stop activation, instrument expiration)
2040    /// are skipped. These maintenance operations will run on the next quote tick or bar.
2041    pub fn process_trade_tick(&mut self, trade: &TradeTick) {
2042        log::debug!("Processing {trade}");
2043
2044        if let Err(e) = self.check_price_precision(trade.price.precision, "trade price") {
2045            self.log_precision_mismatch("trade tick", trade.instrument_id, &e);
2046            return;
2047        }
2048
2049        if let Err(e) = self.check_size_precision(trade.size.precision, "trade size") {
2050            self.log_precision_mismatch("trade tick", trade.instrument_id, &e);
2051            return;
2052        }
2053
2054        self.precision_mismatch_streak = 0;
2055
2056        let price_raw = trade.price.raw;
2057
2058        if self.book_type == BookType::L1_MBP {
2059            // Stale update: skip book mutation and trade execution
2060            if trade.ts_event < self.book.ts_last {
2061                log::warn!(
2062                    "Skipping stale trade: ts_event {} < book.ts_last {} for {}",
2063                    trade.ts_event,
2064                    self.book.ts_last,
2065                    self.book.instrument_id,
2066                );
2067                self.iterate(trade.ts_init, AggressorSide::NoAggressor);
2068                return;
2069            }
2070
2071            if !self.update_trade_tick_or_skip(trade, "trade tick") {
2072                return;
2073            }
2074        }
2075
2076        self.core.set_last_raw(trade.price);
2077
2078        if !self.config.trade_execution {
2079            // Sync core to L1 book, skip order matching
2080            if self.book_type == BookType::L1_MBP {
2081                if let Some(bid) = self.book.best_bid_price() {
2082                    self.core.set_bid_raw(bid);
2083                }
2084
2085                if let Some(ask) = self.book.best_ask_price() {
2086                    self.core.set_ask_raw(ask);
2087                }
2088            }
2089            return;
2090        }
2091
2092        let aggressor_side = trade.aggressor_side;
2093
2094        match aggressor_side {
2095            AggressorSide::Buyer => {
2096                // Buyer lifted the ask: ask was at trade.price, post-trade
2097                // ask is at least this level (only widen)
2098                if self.core.ask.is_none() || price_raw > self.core.ask.map_or(0, |p| p.raw) {
2099                    self.core.set_ask_raw(trade.price);
2100                }
2101
2102                // Initialize bid from first trade if needed
2103                if self.core.bid.is_none() {
2104                    self.core.set_bid_raw(trade.price);
2105                }
2106            }
2107            AggressorSide::Seller => {
2108                // Seller hit the bid: bid was at trade.price, post-trade
2109                // bid is at most this level (only narrow)
2110                if self.core.bid.is_none()
2111                    || price_raw < self.core.bid.map_or(PriceRaw::MAX, |p| p.raw)
2112                {
2113                    self.core.set_bid_raw(trade.price);
2114                }
2115
2116                // Initialize ask from first trade if needed
2117                if self.core.ask.is_none() {
2118                    self.core.set_ask_raw(trade.price);
2119                }
2120            }
2121            AggressorSide::NoAggressor => {
2122                if self.core.bid.is_none()
2123                    || price_raw <= self.core.bid.map_or(PriceRaw::MAX, |p| p.raw)
2124                {
2125                    self.core.set_bid_raw(trade.price);
2126                }
2127
2128                if self.core.ask.is_none() || price_raw >= self.core.ask.map_or(0, |p| p.raw) {
2129                    self.core.set_ask_raw(trade.price);
2130                }
2131            }
2132        }
2133
2134        let original_bid = self.core.bid;
2135        let original_ask = self.core.ask;
2136
2137        match aggressor_side {
2138            AggressorSide::Seller => {
2139                if original_ask.is_some_and(|ask| price_raw < ask.raw) {
2140                    self.core.set_ask_raw(trade.price);
2141                }
2142            }
2143            AggressorSide::Buyer => {
2144                if original_bid.is_some_and(|bid| price_raw > bid.raw) {
2145                    self.core.set_bid_raw(trade.price);
2146                }
2147            }
2148            AggressorSide::NoAggressor => {
2149                // Force both sides to trade price (parity with Cython)
2150                self.core.set_bid_raw(trade.price);
2151                self.core.set_ask_raw(trade.price);
2152            }
2153        }
2154
2155        self.last_trade_size = Some(trade.size);
2156        self.trade_consumption = 0;
2157
2158        if self.config.liquidity_consumption && self.book_type != BookType::L1_MBP {
2159            self.seed_trade_consumption(price_raw, trade.size.raw, trade.ts_event, aggressor_side);
2160        }
2161
2162        self.resolve_pending_on_trade(price_raw);
2163        self.decrement_queue_on_trade(price_raw, trade.size.raw, aggressor_side);
2164
2165        self.iterate(trade.ts_init, aggressor_side);
2166
2167        self.last_trade_size = None;
2168        self.trade_consumption = 0;
2169
2170        // Restore the non-aggressor side after temporary trade price override.
2171        // For L2/L3 books the book has independent depth so restore from originals.
2172        // For L1_MBP restore from the last quote values (not originals, which are
2173        // polluted by iterate's L1 book sync). Without quotes, skip the restore
2174        // so the core tracks the latest trade price.
2175        if self.book_type == BookType::L1_MBP {
2176            match aggressor_side {
2177                AggressorSide::Seller => {
2178                    if let Some(ask) = self.last_quote_ask {
2179                        self.core.ask = Some(ask);
2180                    }
2181                }
2182                AggressorSide::Buyer => {
2183                    if let Some(bid) = self.last_quote_bid {
2184                        self.core.bid = Some(bid);
2185                    }
2186                }
2187                AggressorSide::NoAggressor => {}
2188            }
2189        } else {
2190            match aggressor_side {
2191                AggressorSide::Seller => {
2192                    if let Some(ask) = original_ask
2193                        && price_raw < ask.raw
2194                    {
2195                        self.core.ask = Some(ask);
2196                    }
2197                }
2198                AggressorSide::Buyer => {
2199                    if let Some(bid) = original_bid
2200                        && price_raw > bid.raw
2201                    {
2202                        self.core.bid = Some(bid);
2203                    }
2204                }
2205                AggressorSide::NoAggressor => {}
2206            }
2207        }
2208    }
2209
2210    fn update_quote_tick_or_skip(&mut self, quote: &QuoteTick, context: &str) -> bool {
2211        if let Err(e) = self.book.update_quote_tick(quote) {
2212            log::warn!(
2213                "Skipping {context} for {}: update_quote_tick failed: {e}",
2214                quote.instrument_id,
2215            );
2216            return false;
2217        }
2218        true
2219    }
2220
2221    fn update_trade_tick_or_skip(&mut self, trade: &TradeTick, context: &str) -> bool {
2222        if let Err(e) = self.book.update_trade_tick(trade) {
2223            log::warn!(
2224                "Skipping {context} for {}: update_trade_tick failed: {e}",
2225                trade.instrument_id,
2226            );
2227            return false;
2228        }
2229        true
2230    }
2231
2232    /// Processes a market status action to update the market state.
2233    pub fn process_status(&mut self, action: MarketStatusAction) {
2234        log::debug!("Processing {action}");
2235
2236        match action {
2237            MarketStatusAction::Trading | MarketStatusAction::PreOpen
2238                if matches!(
2239                    self.market_status,
2240                    MarketStatus::Closed | MarketStatus::Paused | MarketStatus::Suspended
2241                ) =>
2242            {
2243                self.market_status = MarketStatus::Open;
2244            }
2245            MarketStatusAction::Pause if self.market_status == MarketStatus::Open => {
2246                self.market_status = MarketStatus::Paused;
2247            }
2248            MarketStatusAction::Suspend if self.market_status == MarketStatus::Open => {
2249                self.market_status = MarketStatus::Suspended;
2250            }
2251            MarketStatusAction::Halt | MarketStatusAction::Close
2252                if self.market_status == MarketStatus::Open =>
2253            {
2254                self.market_status = MarketStatus::Closed;
2255            }
2256            _ => {}
2257        }
2258    }
2259
2260    /// Processes an instrument close event.
2261    ///
2262    /// For `ContractExpired` close types, stores the close and triggers expiration
2263    /// processing which cancels all open orders and closes all open positions.
2264    pub fn process_instrument_close(&mut self, close: InstrumentClose) {
2265        if close.instrument_id != self.instrument.id() {
2266            log::warn!(
2267                "Received instrument close for unknown instrument_id: {}",
2268                close.instrument_id
2269            );
2270            return;
2271        }
2272
2273        if close.close_type == InstrumentCloseType::ContractExpired {
2274            self.instrument_close = Some(close);
2275            self.iterate(close.ts_init, AggressorSide::NoAggressor);
2276        }
2277    }
2278
2279    /// Processes instrument expiration at the given timestamp.
2280    pub fn process_instrument_expiration(&mut self, timestamp_ns: UnixNanos) {
2281        self.check_instrument_expiration(timestamp_ns);
2282    }
2283
2284    /// Returns whether instrument expiration has already been processed.
2285    #[must_use]
2286    pub const fn is_expiration_processed(&self) -> bool {
2287        self.expiration_processed
2288    }
2289
2290    fn requires_pending_resolution(&self) -> bool {
2291        matches!(self.instrument, InstrumentAny::BinaryOption(_))
2292    }
2293
2294    fn cancel_open_orders_for_expiration(&mut self) {
2295        // Build a single de-duplicated cancellation set across the matching
2296        // core and cache. Resting orders may still only be represented in the
2297        // core while inflight orders can remain cache-only during the
2298        // submitted/pending transition window.
2299        let instrument_id = self.instrument.id();
2300        let expiration_order_ids: IndexSet<ClientOrderId> = {
2301            let cache = self.cache.borrow();
2302            let mut order_ids = IndexSet::new();
2303
2304            for order_info in self.get_open_orders() {
2305                order_ids.insert(order_info.client_order_id);
2306            }
2307
2308            for order in cache.orders(None, Some(&instrument_id), None, None, None) {
2309                if order.is_open() || order.is_inflight() {
2310                    order_ids.insert(order.client_order_id());
2311                }
2312            }
2313
2314            order_ids
2315        };
2316
2317        for client_order_id in expiration_order_ids {
2318            let order = {
2319                let cache = self.cache.borrow();
2320                cache.order(&client_order_id).map(|order| order.clone())
2321            };
2322
2323            if let Some(order) = order {
2324                self.cancel_order(&order, None);
2325            }
2326        }
2327    }
2328
2329    fn enter_pending_resolution(&mut self) {
2330        if self.pending_resolution {
2331            return;
2332        }
2333
2334        self.pending_resolution = true;
2335        self.market_status = MarketStatus::Closed;
2336        self.cancel_open_orders_for_expiration();
2337        log::info!(
2338            "{} expired and is now pending resolution; open orders canceled and new orders blocked",
2339            self.instrument.id()
2340        );
2341    }
2342
2343    fn check_instrument_expiration(&mut self, timestamp_ns: UnixNanos) {
2344        if self.expiration_processed {
2345            return;
2346        }
2347
2348        let timestamp_triggered = self
2349            .instrument
2350            .expiration_ns()
2351            .is_some_and(|ns| timestamp_ns >= ns);
2352
2353        if !timestamp_triggered && self.instrument_close.is_none() {
2354            return;
2355        }
2356
2357        if self.instrument_close.is_none()
2358            && timestamp_triggered
2359            && self.requires_pending_resolution()
2360        {
2361            self.enter_pending_resolution();
2362            return;
2363        }
2364
2365        self.expiration_processed = true;
2366        self.pending_resolution = false;
2367        let close = self.instrument_close.take();
2368        log::info!("{} reached expiration", self.instrument.id());
2369        self.cancel_open_orders_for_expiration();
2370
2371        if matches!(
2372            self.instrument,
2373            InstrumentAny::OptionContract(_) | InstrumentAny::CryptoOption(_)
2374        ) {
2375            self.process_option_expiry(timestamp_ns);
2376            return;
2377        }
2378
2379        let instrument_id = self.instrument.id();
2380        let positions: Vec<(TraderId, StrategyId, PositionId, OrderSide, Quantity)> = {
2381            let cache = self.cache.borrow();
2382            cache
2383                .positions_open(None, Some(&instrument_id), None, None, None)
2384                .into_iter()
2385                .map(|pos| {
2386                    let closing_side = match pos.side {
2387                        PositionSide::Long => OrderSide::Sell,
2388                        PositionSide::Short => OrderSide::Buy,
2389                        _ => OrderSide::NoOrderSide,
2390                    };
2391                    (
2392                        pos.trader_id,
2393                        pos.strategy_id,
2394                        pos.id,
2395                        closing_side,
2396                        pos.quantity,
2397                    )
2398                })
2399                .collect()
2400        };
2401
2402        let ts_now = self.clock.borrow().timestamp_ns();
2403        let close_price_fallback = close.as_ref().map(|c| c.close_price);
2404
2405        for (trader_id, strategy_id, position_id, closing_side, quantity) in positions {
2406            let client_order_id =
2407                ClientOrderId::from(format!("EXPIRATION-{}-{}", self.venue, UUID4::new()).as_str());
2408            let mut order = OrderAny::Market(MarketOrder::new(
2409                trader_id,
2410                strategy_id,
2411                instrument_id,
2412                client_order_id,
2413                closing_side,
2414                quantity,
2415                TimeInForce::Gtc,
2416                UUID4::new(),
2417                ts_now,
2418                true, // reduce_only
2419                false,
2420                None,
2421                None,
2422                None,
2423                None,
2424                None,
2425                None,
2426                None,
2427                Some(vec![Ustr::from(&format!(
2428                    "EXPIRATION_{}_CLOSE",
2429                    self.venue
2430                ))]),
2431            ));
2432            order.set_liquidity_side(LiquiditySide::Taker);
2433
2434            let add_result =
2435                self.cache
2436                    .borrow_mut()
2437                    .add_order(order.clone(), Some(position_id), None, false);
2438            if add_result.is_err() {
2439                log::debug!("Expiration order already in cache: {client_order_id}");
2440            } else {
2441                self.publish_order_initialized(&order);
2442            }
2443
2444            let venue_order_id = self.ids_generator.get_venue_order_id(&order).unwrap();
2445            self.generate_order_accepted(&order, venue_order_id);
2446
2447            let fill_price = self.settlement_price.or(close_price_fallback);
2448            if let Some(fill_price) = fill_price {
2449                if let Err(e) = self.apply_fills(
2450                    &order,
2451                    &[(fill_price, quantity)],
2452                    LiquiditySide::Taker,
2453                    Some(position_id),
2454                    None,
2455                    None,
2456                ) {
2457                    log::error!("Cannot fill expiration order {client_order_id}: {e}");
2458                }
2459            } else {
2460                self.fill_market_order(client_order_id);
2461            }
2462        }
2463    }
2464
2465    fn process_option_expiry(&mut self, ts_now: UnixNanos) {
2466        let instrument_id = self.instrument.id();
2467
2468        let positions: Vec<Position> = {
2469            let cache = self.cache.borrow();
2470            cache
2471                .positions_open(None, Some(&instrument_id), None, None, None)
2472                .into_iter()
2473                .map(|p| p.cloned())
2474                .collect()
2475        };
2476
2477        if positions.is_empty() {
2478            return;
2479        }
2480
2481        let underlying = match self.instrument.underlying() {
2482            Some(u) => u,
2483            None => {
2484                log::error!("No underlying for option {instrument_id}");
2485                return;
2486            }
2487        };
2488        let underlying_id = InstrumentId::from(format!("{underlying}.{}", self.venue).as_str());
2489
2490        let underlying_instrument = {
2491            let cache = self.cache.borrow();
2492            cache.instrument(&underlying_id).cloned()
2493        };
2494
2495        let underlying_instrument = match underlying_instrument {
2496            Some(u) => u,
2497            None => {
2498                log::error!("No underlying instrument for option {instrument_id}");
2499                return;
2500            }
2501        };
2502
2503        // Resolve the underlying price by the underlying's instrument type. An index
2504        // is disseminated via `IndexPriceUpdate` (it does not trade), so its level is
2505        // held in the cache's index-price store rather than the trade/quote `price(...)`
2506        // store; a tradeable underlying (e.g. an equity) keeps the `Last` trade lookup.
2507        let underlying_price = {
2508            let cache = self.cache.borrow();
2509            if matches!(underlying_instrument, InstrumentAny::IndexInstrument(_)) {
2510                cache.index_price(&underlying_id).map(|ip| ip.value)
2511            } else {
2512                cache.price(&underlying_id, PriceType::Last)
2513            }
2514        };
2515
2516        let underlying_price = match underlying_price {
2517            Some(p) => p,
2518            None => {
2519                log::error!("No underlying price for option {instrument_id}");
2520                return;
2521            }
2522        };
2523
2524        let custom_option_price = self.settlement_price;
2525        let should_exercise = self.option_should_exercise(underlying_price);
2526
2527        for position in positions {
2528            self.account_ids
2529                .insert(position.trader_id, position.account_id);
2530
2531            if should_exercise {
2532                self.option_exercise_position(
2533                    &position,
2534                    &underlying_instrument,
2535                    underlying_price,
2536                    ts_now,
2537                    custom_option_price,
2538                );
2539            } else {
2540                self.option_otm_expiry(&position, ts_now, custom_option_price);
2541            }
2542        }
2543    }
2544
2545    fn option_should_exercise(&self, underlying_price: Price) -> bool {
2546        let strike = match self.instrument.strike_price() {
2547            Some(p) => p.as_decimal(),
2548            None => return false,
2549        };
2550        let spot = underlying_price.as_decimal();
2551        match self.instrument.option_kind() {
2552            Some(OptionKind::Call) => spot > strike,
2553            Some(OptionKind::Put) => strike > spot,
2554            None => false,
2555        }
2556    }
2557
2558    fn option_settlement_price(&self, underlying_price: Price, cash_settled: bool) -> Price {
2559        let strike = self
2560            .instrument
2561            .strike_price()
2562            .expect("option must have strike");
2563        if !cash_settled {
2564            return strike;
2565        }
2566
2567        let spot = underlying_price.as_decimal();
2568        let strike_value = strike.as_decimal();
2569        let value = match self.instrument.option_kind() {
2570            Some(OptionKind::Call) => (spot - strike_value).max(Decimal::ZERO),
2571            _ => (strike_value - spot).max(Decimal::ZERO),
2572        };
2573        Price::from_decimal_dp(value, strike.precision).expect("Invalid option settlement price")
2574    }
2575
2576    fn option_exercise_position(
2577        &self,
2578        position: &Position,
2579        underlying_instrument: &InstrumentAny,
2580        underlying_price: Price,
2581        ts_now: UnixNanos,
2582        custom_option_price: Option<Price>,
2583    ) {
2584        if matches!(underlying_instrument, InstrumentAny::IndexInstrument(_)) {
2585            self.option_cash_settlement(position, underlying_price, ts_now, custom_option_price);
2586        } else {
2587            self.option_physical_settlement(
2588                position,
2589                underlying_instrument,
2590                underlying_price,
2591                ts_now,
2592                custom_option_price,
2593            );
2594        }
2595    }
2596
2597    fn option_cash_settlement(
2598        &self,
2599        position: &Position,
2600        underlying_price: Price,
2601        ts_now: UnixNanos,
2602        custom_option_price: Option<Price>,
2603    ) {
2604        let venue = self.venue;
2605        let trade_id = format!("{venue}-LEG-CASH-{}", &UUID4::new().to_string()[..8]);
2606        let close_px = custom_option_price
2607            .unwrap_or_else(|| self.option_settlement_price(underlying_price, true));
2608        let close_side = OrderCore::closing_side(position.side);
2609        self.option_register_settlement_order(
2610            position,
2611            self.instrument.id(),
2612            close_side,
2613            position.quantity,
2614            ClientOrderId::from(trade_id.as_str()),
2615            VenueOrderId::from(trade_id.as_str()),
2616            Some(position.id),
2617            true,
2618            &format!("EXPIRATION_{venue}_CASH"),
2619        );
2620        let fill = self.option_create_close_fill(position, close_px, &trade_id, ts_now);
2621        self.dispatch_order_event(OrderEventAny::Filled(fill));
2622    }
2623
2624    fn option_physical_settlement(
2625        &self,
2626        position: &Position,
2627        underlying_instrument: &InstrumentAny,
2628        underlying_price: Price,
2629        ts_now: UnixNanos,
2630        custom_option_price: Option<Price>,
2631    ) {
2632        let multiplier = self.instrument.multiplier();
2633        let underlying_qty = Quantity::from_decimal_dp(
2634            position.quantity.as_decimal() * multiplier.as_decimal(),
2635            underlying_instrument.size_precision(),
2636        )
2637        .expect("Invalid underlying settlement quantity");
2638
2639        let underlying_side = if self.instrument.option_kind() == Some(OptionKind::Call) {
2640            position.side
2641        } else {
2642            match position.side {
2643                PositionSide::Long => PositionSide::Short,
2644                PositionSide::Short => PositionSide::Long,
2645                other => other,
2646            }
2647        };
2648
2649        let venue = self.venue;
2650        let trade_base = format!("{venue}-LEG-EX-{}", &UUID4::new().to_string()[..8]);
2651        let close_trade_id = format!("{trade_base}-CLOSE");
2652        let open_trade_id = format!("{trade_base}-OPEN");
2653        let settlement_px = self.option_settlement_price(underlying_price, false);
2654        let option_close_px =
2655            custom_option_price.unwrap_or_else(|| Price::zero(self.instrument.price_precision()));
2656        let close_side = OrderCore::closing_side(position.side);
2657        let underlying_order_side = match underlying_side {
2658            PositionSide::Long => OrderSide::Buy,
2659            _ => OrderSide::Sell,
2660        };
2661
2662        self.option_register_settlement_order(
2663            position,
2664            self.instrument.id(),
2665            close_side,
2666            position.quantity,
2667            ClientOrderId::from(close_trade_id.as_str()),
2668            VenueOrderId::from(close_trade_id.as_str()),
2669            Some(position.id),
2670            true,
2671            &format!("EXPIRATION_{venue}_PHYSICAL_CLOSE"),
2672        );
2673        self.option_register_settlement_order(
2674            position,
2675            underlying_instrument.id(),
2676            underlying_order_side,
2677            underlying_qty,
2678            ClientOrderId::from(open_trade_id.as_str()),
2679            VenueOrderId::from(open_trade_id.as_str()),
2680            None,
2681            false,
2682            &format!("EXPIRATION_{venue}_PHYSICAL_OPEN"),
2683        );
2684
2685        let option_fill =
2686            self.option_create_close_fill(position, option_close_px, &close_trade_id, ts_now);
2687        let underlying_fill = self.option_create_underlying_fill(
2688            position,
2689            underlying_instrument,
2690            underlying_qty,
2691            underlying_side,
2692            settlement_px,
2693            &open_trade_id,
2694            ts_now,
2695        );
2696        self.dispatch_order_event(OrderEventAny::Filled(option_fill));
2697        self.dispatch_order_event(OrderEventAny::Filled(underlying_fill));
2698    }
2699
2700    fn option_otm_expiry(
2701        &self,
2702        position: &Position,
2703        ts_now: UnixNanos,
2704        custom_option_price: Option<Price>,
2705    ) {
2706        let venue = self.venue;
2707        let trade_id = format!("{venue}-LEG-OTM-{}", &UUID4::new().to_string()[..8]);
2708        let close_px =
2709            custom_option_price.unwrap_or_else(|| Price::zero(self.instrument.price_precision()));
2710        let close_side = OrderCore::closing_side(position.side);
2711        self.option_register_settlement_order(
2712            position,
2713            self.instrument.id(),
2714            close_side,
2715            position.quantity,
2716            ClientOrderId::from(trade_id.as_str()),
2717            VenueOrderId::from(trade_id.as_str()),
2718            Some(position.id),
2719            true,
2720            &format!("EXPIRATION_{venue}_OTM"),
2721        );
2722        let fill = self.option_create_close_fill(position, close_px, &trade_id, ts_now);
2723        self.dispatch_order_event(OrderEventAny::Filled(fill));
2724    }
2725
2726    #[expect(clippy::too_many_arguments)]
2727    fn option_register_settlement_order(
2728        &self,
2729        position: &Position,
2730        instrument_id: InstrumentId,
2731        order_side: OrderSide,
2732        quantity: Quantity,
2733        client_order_id: ClientOrderId,
2734        venue_order_id: VenueOrderId,
2735        position_id: Option<PositionId>,
2736        reduce_only: bool,
2737        tag: &str,
2738    ) {
2739        let ts_now = self.clock.borrow().timestamp_ns();
2740        let order = OrderAny::Market(MarketOrder::new(
2741            position.trader_id,
2742            position.strategy_id,
2743            instrument_id,
2744            client_order_id,
2745            order_side,
2746            quantity,
2747            TimeInForce::Gtc,
2748            UUID4::new(),
2749            ts_now,
2750            reduce_only,
2751            false,
2752            None,
2753            None,
2754            None,
2755            None,
2756            None,
2757            None,
2758            None,
2759            Some(vec![Ustr::from(tag)]),
2760        ));
2761
2762        {
2763            let mut cache = self.cache.borrow_mut();
2764            if let Err(e) = cache.add_order(order.clone(), position_id, None, false) {
2765                log::debug!("Settlement order already in cache: {e}");
2766            } else {
2767                drop(cache);
2768                self.publish_order_initialized(&order);
2769                self.cache
2770                    .borrow_mut()
2771                    .add_venue_order_id(&client_order_id, &venue_order_id, false)
2772                    .ok();
2773            }
2774        }
2775
2776        self.generate_order_accepted(&order, venue_order_id);
2777    }
2778
2779    fn option_create_close_fill(
2780        &self,
2781        position: &Position,
2782        price: Price,
2783        trade_id_str: &str,
2784        ts_now: UnixNanos,
2785    ) -> OrderFilled {
2786        let close_side = OrderCore::closing_side(position.side);
2787        OrderFilled::new(
2788            position.trader_id,
2789            position.strategy_id,
2790            self.instrument.id(),
2791            ClientOrderId::from(trade_id_str),
2792            VenueOrderId::from(trade_id_str),
2793            position.account_id,
2794            TradeId::from(trade_id_str),
2795            close_side,
2796            OrderType::Market,
2797            position.quantity,
2798            price,
2799            self.instrument.quote_currency(),
2800            LiquiditySide::Taker,
2801            UUID4::new(),
2802            ts_now,
2803            ts_now,
2804            false,
2805            Some(position.id),
2806            Some(Money::zero(self.instrument.quote_currency())),
2807            None,
2808        )
2809    }
2810
2811    #[expect(clippy::too_many_arguments)]
2812    fn option_create_underlying_fill(
2813        &self,
2814        position: &Position,
2815        underlying_instrument: &InstrumentAny,
2816        quantity: Quantity,
2817        side: PositionSide,
2818        price: Price,
2819        trade_id_str: &str,
2820        ts_now: UnixNanos,
2821    ) -> OrderFilled {
2822        let order_side = match side {
2823            PositionSide::Long => OrderSide::Buy,
2824            _ => OrderSide::Sell,
2825        };
2826        OrderFilled::new(
2827            position.trader_id,
2828            position.strategy_id,
2829            underlying_instrument.id(),
2830            ClientOrderId::from(trade_id_str),
2831            VenueOrderId::from(trade_id_str),
2832            position.account_id,
2833            TradeId::from(trade_id_str),
2834            order_side,
2835            OrderType::Market,
2836            quantity,
2837            price,
2838            underlying_instrument.quote_currency(),
2839            LiquiditySide::Taker,
2840            UUID4::new(),
2841            ts_now,
2842            ts_now,
2843            false,
2844            None,
2845            Some(Money::zero(underlying_instrument.quote_currency())),
2846            None,
2847        )
2848    }
2849
2850    /// Liquidates all open positions for this instrument.
2851    ///
2852    /// Cancels open orders if `cancel_open_orders` is true, then closes every open
2853    /// position at best bid/ask or the settlement price, emitting accepted and filled
2854    /// events for each synthetic close order.
2855    ///
2856    /// # Panics
2857    ///
2858    /// Panics if the venue order ID generator cannot produce an ID for the synthetic
2859    /// liquidation order (internal state inconsistency).
2860    ///
2861    /// Only positions whose instrument settles in `settlement_currency` are closed.
2862    /// Matching engines for other settlement currencies are skipped, scoping
2863    /// liquidation to the currency whose margin account breached the threshold.
2864    pub fn liquidate_open_positions(
2865        &mut self,
2866        ts_now: UnixNanos,
2867        cancel_open_orders: bool,
2868        settlement_currency: Currency,
2869    ) {
2870        // Only liquidate positions settled in the breached currency.
2871        if self.instrument.settlement_currency() != settlement_currency {
2872            return;
2873        }
2874
2875        if cancel_open_orders {
2876            let open_orders: Vec<RestingOrder> = self.get_open_orders();
2877            for order_info in &open_orders {
2878                let order = {
2879                    let cache = self.cache.borrow();
2880                    cache.order_owned(&order_info.client_order_id)
2881                };
2882
2883                if let Some(order) = order {
2884                    self.cancel_order(&order, None);
2885                }
2886            }
2887        }
2888
2889        let instrument_id = self.instrument.id();
2890        let positions: Vec<(
2891            TraderId,
2892            StrategyId,
2893            AccountId,
2894            PositionId,
2895            OrderSide,
2896            Quantity,
2897        )> = {
2898            let cache = self.cache.borrow();
2899            cache
2900                .positions_open(None, Some(&instrument_id), None, None, None)
2901                .into_iter()
2902                .map(|pos| {
2903                    (
2904                        pos.trader_id,
2905                        pos.strategy_id,
2906                        pos.account_id,
2907                        pos.id,
2908                        OrderCore::closing_side(pos.side),
2909                        pos.quantity,
2910                    )
2911                })
2912                .collect()
2913        };
2914
2915        for (trader_id, strategy_id, account_id, position_id, closing_side, quantity) in positions {
2916            // Pre-check: ensure a price source is available before emitting events.
2917            let has_price = if closing_side == OrderSide::Sell {
2918                self.best_bid_price().is_some() || self.settlement_price.is_some()
2919            } else {
2920                self.best_ask_price().is_some() || self.settlement_price.is_some()
2921            };
2922
2923            if !has_price {
2924                log::warn!(
2925                    "LIQUIDATION: no price available for {instrument_id} position {position_id}, skipping"
2926                );
2927                continue;
2928            }
2929
2930            let client_order_id = ClientOrderId::from(
2931                format!("LIQUIDATION-{}-{}", self.venue, UUID4::new()).as_str(),
2932            );
2933            let order = OrderAny::Market(MarketOrder::new(
2934                trader_id,
2935                strategy_id,
2936                instrument_id,
2937                client_order_id,
2938                closing_side,
2939                quantity,
2940                TimeInForce::Ioc,
2941                UUID4::new(),
2942                ts_now,
2943                true, // reduce_only
2944                false,
2945                None,
2946                None,
2947                None,
2948                None,
2949                None,
2950                None,
2951                None,
2952                Some(vec![Ustr::from(&format!(
2953                    "LIQUIDATION_{}_CLOSE",
2954                    self.venue
2955                ))]),
2956            ));
2957
2958            let venue_order_id = self.ids_generator.get_venue_order_id(&order).unwrap();
2959            {
2960                let mut cache = self.cache.borrow_mut();
2961                if let Err(e) = cache.add_order(order.clone(), Some(position_id), None, false) {
2962                    log::debug!("Liquidation order already in cache: {e}");
2963                } else {
2964                    drop(cache);
2965                    self.publish_order_initialized(&order);
2966                    self.cache
2967                        .borrow_mut()
2968                        .add_venue_order_id(&client_order_id, &venue_order_id, false)
2969                        .ok();
2970                }
2971            }
2972
2973            // Route through the normal market-order fill machinery (fill model,
2974            // book depth consumption, slippage) instead of apply_fills directly.
2975            self.account_ids.insert(trader_id, account_id);
2976            self.generate_order_submitted(&order, account_id);
2977            self.generate_order_accepted(&order, venue_order_id);
2978            self.fill_market_order(client_order_id);
2979        }
2980    }
2981
2982    /// Processes a new order submission.
2983    ///
2984    /// Validates the order against instrument precision, expiration, and contingency
2985    /// rules before accepting or rejecting it.
2986    ///
2987    /// # Panics
2988    ///
2989    /// Panics if an OTO child order references a missing or non-OTO parent.
2990    pub fn process_order(&mut self, order: &mut OrderAny, account_id: AccountId) {
2991        // Idempotent: OTO children may be re-routed via `fill_order`
2992        if self.core.order_exists(order.client_order_id()) {
2993            return;
2994        }
2995
2996        // Ensure expiration semantics are enforced even when no fresh market-data
2997        // tick arrives for this instrument after expiry (e.g. after rotation).
2998        let ts_now = self.clock.borrow().timestamp_ns();
2999        self.check_instrument_expiration(ts_now);
3000
3001        // Validate inside a cache borrow scope, collecting any rejection
3002        // reason rather than emitting events while the borrow is held.
3003        // This avoids RefCell re-entrancy panics from synchronous event
3004        // dispatch that calls back into the execution engine.
3005        let reject_reason: Option<Ustr> = 'validate: {
3006            let cache_borrow = self.cache.as_ref().borrow();
3007
3008            // Index identifiers
3009            self.account_ids.insert(order.trader_id(), account_id);
3010
3011            if self.pending_resolution {
3012                break 'validate Some(
3013                    format!(
3014                        "Contract {} has expired and is pending resolution",
3015                        self.instrument.id()
3016                    )
3017                    .into(),
3018                );
3019            }
3020
3021            if self.market_status != MarketStatus::Open {
3022                break 'validate Some(
3023                    format!(
3024                        "Market {} is {}, cannot accept order {}",
3025                        self.instrument.id(),
3026                        self.market_status,
3027                        order.client_order_id()
3028                    )
3029                    .into(),
3030                );
3031            }
3032
3033            // Check for instrument expiration or activation
3034            if self.instrument.has_expiration() {
3035                if let Some(activation_ns) = self.instrument.activation_ns()
3036                    && self.clock.borrow().timestamp_ns() < activation_ns
3037                {
3038                    break 'validate Some(
3039                        format!(
3040                            "Contract {} is not yet active, activation {activation_ns}",
3041                            self.instrument.id(),
3042                        )
3043                        .into(),
3044                    );
3045                }
3046
3047                if let Some(expiration_ns) = self.instrument.expiration_ns()
3048                    && self.clock.borrow().timestamp_ns() >= expiration_ns
3049                {
3050                    break 'validate Some(
3051                        format!(
3052                            "Contract {} has expired, expiration {expiration_ns}",
3053                            self.instrument.id(),
3054                        )
3055                        .into(),
3056                    );
3057                }
3058            }
3059
3060            // Contingent orders checks
3061            if self.config.support_contingent_orders {
3062                if let Some(parent_order_id) = order.parent_order_id() {
3063                    let parent_order = match cache_borrow.order(&parent_order_id) {
3064                        Some(o) if o.contingency_type().unwrap() == ContingencyType::Oto => o,
3065                        _ => panic!("OTO parent not found"),
3066                    };
3067
3068                    if parent_order.status() == OrderStatus::Rejected && order.is_open() {
3069                        break 'validate Some(
3070                            format!("Rejected OTO order from {parent_order_id}").into(),
3071                        );
3072                    } else if parent_order.status() == OrderStatus::Accepted
3073                        || parent_order.status() == OrderStatus::Triggered
3074                        || (self.config.oto_full_trigger
3075                            && parent_order.status() == OrderStatus::PartiallyFilled)
3076                    {
3077                        log::info!(
3078                            "Pending OTO order {} triggers from {parent_order_id}",
3079                            order.client_order_id(),
3080                        );
3081                        return;
3082                    }
3083                }
3084
3085                if let Some(linked_order_ids) = order.linked_order_ids() {
3086                    for client_order_id in linked_order_ids {
3087                        match cache_borrow.order(client_order_id) {
3088                            Some(contingent_order)
3089                                if (order.contingency_type().unwrap() == ContingencyType::Oco
3090                                    || order.contingency_type().unwrap()
3091                                        == ContingencyType::Ouo)
3092                                    && !order.is_closed()
3093                                    && contingent_order.is_closed() =>
3094                            {
3095                                break 'validate Some(
3096                                    format!("Contingent order {client_order_id} already closed")
3097                                        .into(),
3098                                );
3099                            }
3100                            None => panic!("Cannot find contingent order for {client_order_id}"),
3101                            _ => {}
3102                        }
3103                    }
3104                }
3105            }
3106
3107            // Check for valid order quantity precision
3108            if order.quantity().precision != self.instrument.size_precision() {
3109                break 'validate Some(
3110                    format!(
3111                        "Invalid order quantity precision for order {}, was {} when {} size precision is {}",
3112                        order.client_order_id(),
3113                        order.quantity().precision,
3114                        self.instrument.id(),
3115                        self.instrument.size_precision()
3116                    )
3117                    .into(),
3118                );
3119            }
3120
3121            // Check for valid order display quantity precision
3122            if let Some(display_qty) = order.display_qty()
3123                && display_qty.precision != self.instrument.size_precision()
3124            {
3125                break 'validate Some(
3126                    format!(
3127                        "Invalid order display quantity precision for order {}, was {} when {} size precision is {}",
3128                        order.client_order_id(),
3129                        display_qty.precision,
3130                        self.instrument.id(),
3131                        self.instrument.size_precision()
3132                    )
3133                    .into(),
3134                );
3135            }
3136
3137            // Check for valid order price precision
3138            if let Some(price) = order.price()
3139                && price.precision != self.instrument.price_precision()
3140            {
3141                break 'validate Some(
3142                    format!(
3143                        "Invalid order price precision for order {}, was {} when {} price precision is {}",
3144                        order.client_order_id(),
3145                        price.precision,
3146                        self.instrument.id(),
3147                        self.instrument.price_precision()
3148                    )
3149                    .into(),
3150                );
3151            }
3152
3153            // Check for valid order trigger price precision
3154            if let Some(trigger_price) = order.trigger_price()
3155                && trigger_price.precision != self.instrument.price_precision()
3156            {
3157                break 'validate Some(
3158                    format!(
3159                        "Invalid order trigger price precision for order {}, was {} when {} price precision is {}",
3160                        order.client_order_id(),
3161                        trigger_price.precision,
3162                        self.instrument.id(),
3163                        self.instrument.price_precision()
3164                    )
3165                    .into(),
3166                );
3167            }
3168
3169            let position = self.position_for_order_in_cache(&cache_borrow, order);
3170
3171            // Check not shorting an equity without a MARGIN account
3172            if order.order_side() == OrderSide::Sell
3173                && self.account_type != AccountType::Margin
3174                && matches!(self.instrument, InstrumentAny::Equity(_))
3175                && position
3176                    .as_ref()
3177                    .is_none_or(|pos| !order.would_reduce_only(pos.side, pos.quantity))
3178            {
3179                let position_string = position
3180                    .as_ref()
3181                    .map_or("None".to_string(), |pos| pos.id.to_string());
3182                break 'validate Some(
3183                    format!(
3184                        "Short selling not permitted on a CASH account with position {position_string} and order {order}",
3185                    )
3186                    .into(),
3187                );
3188            }
3189
3190            // Check reduce-only instruction
3191            if self.config.use_reduce_only
3192                && order.is_reduce_only()
3193                && !order.is_closed()
3194                && position.as_ref().is_none_or(|pos| {
3195                    pos.is_closed()
3196                        || (order.is_buy() && pos.is_long())
3197                        || (order.is_sell() && pos.is_short())
3198                })
3199            {
3200                break 'validate Some(
3201                    format!(
3202                        "Reduce-only order {} ({}-{}) would have increased position",
3203                        order.client_order_id(),
3204                        order.order_type().to_string().to_uppercase(),
3205                        order.order_side().to_string().to_uppercase()
3206                    )
3207                    .into(),
3208                );
3209            }
3210
3211            None
3212        };
3213
3214        if let Some(reason) = reject_reason {
3215            self.generate_order_rejected(order, reason);
3216            return;
3217        }
3218
3219        // Convert quote-denominated quantity to base quantity for non-inverse instruments.
3220        // Mirrors live venue semantics where the quote notional is settled into a base
3221        // quantity before the order enters normal fill and state handling. Without this
3222        // conversion the book simulation would treat the quote notional as base size.
3223        // Only applies to order types with a reliable reference price at submission;
3224        // trigger-style market orders and trailing orders are left untouched so they
3225        // convert at fill time from the actual (possibly-trailed) price.
3226        if order.is_quote_quantity()
3227            && !self.instrument.is_inverse()
3228            && !matches!(
3229                order.order_type(),
3230                OrderType::TrailingStopLimit | OrderType::TrailingStopMarket,
3231            )
3232            && (order.price().is_some()
3233                || matches!(
3234                    order.order_type(),
3235                    OrderType::Market | OrderType::MarketToLimit,
3236                ))
3237            && !self.convert_quote_to_base_quantity(order)
3238        {
3239            return;
3240        }
3241
3242        match order.order_type() {
3243            OrderType::Market => self.process_market_order(order),
3244            OrderType::Limit => self.process_limit_order(order),
3245            OrderType::MarketToLimit => self.process_market_to_limit_order(order),
3246            OrderType::StopMarket => self.process_stop_market_order(order),
3247            OrderType::StopLimit => self.process_stop_limit_order(order),
3248            OrderType::MarketIfTouched => self.process_market_if_touched_order(order),
3249            OrderType::LimitIfTouched => self.process_limit_if_touched_order(order),
3250            OrderType::TrailingStopMarket => self.process_trailing_stop_order(order),
3251            OrderType::TrailingStopLimit => self.process_trailing_stop_order(order),
3252        }
3253    }
3254
3255    fn convert_quote_to_base_quantity(&self, order: &mut OrderAny) -> bool {
3256        // Pick a reference price to convert the quote notional into a base quantity.
3257        // Priced orders use their own price (worst-case execution); marketable orders
3258        // use the best opposing book level.
3259        let reference_price = if let Some(price) = order.price() {
3260            Some(price)
3261        } else {
3262            match order.order_side() {
3263                OrderSide::Buy => self.core.ask,
3264                OrderSide::Sell => self.core.bid,
3265                OrderSide::NoOrderSide => None,
3266            }
3267        };
3268
3269        let Some(reference_price) = reference_price else {
3270            self.generate_order_rejected(
3271                order,
3272                format!(
3273                    "No market for {} to convert quote quantity to base",
3274                    order.instrument_id(),
3275                )
3276                .into(),
3277            );
3278            return false;
3279        };
3280
3281        let base_quantity = self
3282            .instrument
3283            .calculate_base_quantity(order.quantity(), reference_price);
3284
3285        let ts_now = self.clock.borrow().timestamp_ns();
3286        let event = OrderEventAny::Updated(OrderUpdated::new(
3287            order.trader_id(),
3288            order.strategy_id(),
3289            order.instrument_id(),
3290            order.client_order_id(),
3291            base_quantity,
3292            UUID4::new(),
3293            ts_now,
3294            ts_now,
3295            false,
3296            order.venue_order_id(),
3297            order.account_id(),
3298            None,
3299            None,
3300            None,
3301            false,
3302        ));
3303
3304        // Apply the update to the local order so subsequent dispatch uses the base
3305        // quantity immediately (the event is also dispatched to the execution engine
3306        // for cache reconciliation).
3307        if let Err(e) = order.apply(event.clone()) {
3308            log::error!(
3309                "Failed to apply quote-to-base update for {}: {e}",
3310                order.client_order_id(),
3311            );
3312            return false;
3313        }
3314        self.dispatch_order_event(event);
3315        true
3316    }
3317
3318    /// Processes an order modify command to update quantity, price, or trigger price.
3319    pub fn process_modify(&mut self, command: &ModifyOrder, account_id: AccountId) {
3320        if !self.core.order_exists(command.client_order_id) {
3321            self.generate_order_modify_rejected(
3322                command.trader_id,
3323                command.strategy_id,
3324                command.instrument_id,
3325                command.client_order_id,
3326                Ustr::from(format!("Order {} not found", command.client_order_id).as_str()),
3327                command.venue_order_id,
3328                Some(account_id),
3329            );
3330            return;
3331        }
3332
3333        let order = match self
3334            .cache
3335            .borrow()
3336            .order(&command.client_order_id)
3337            .map(|o| o.clone())
3338        {
3339            Some(order) => order,
3340            None => {
3341                log::error!(
3342                    "Cannot modify order: order {} not found in cache",
3343                    command.client_order_id
3344                );
3345                return;
3346            }
3347        };
3348
3349        let update_success = self.update_order(
3350            &order,
3351            command.quantity,
3352            command.price,
3353            command.trigger_price,
3354            None,
3355        );
3356
3357        if !update_success {
3358            return;
3359        }
3360
3361        // Local `order` is pre-event; resync from the cache for fresh state
3362        let Some(refreshed) = self.resync_core_entry(command.client_order_id) else {
3363            return;
3364        };
3365
3366        // Skip queue reset on rejected modifies to preserve accrued position
3367        let price_changed = refreshed.price() != order.price()
3368            || refreshed.trigger_price() != order.trigger_price();
3369
3370        if price_changed
3371            && refreshed.is_open()
3372            && self.config.queue_position
3373            && let Some(new_price) = refreshed.price()
3374        {
3375            self.snapshot_queue_position(&refreshed, new_price);
3376            self.queue_excess.swap_remove(&refreshed.client_order_id());
3377        }
3378    }
3379
3380    /// Processes an order cancel command.
3381    pub fn process_cancel(&mut self, command: &CancelOrder, account_id: AccountId) {
3382        if !self.core.order_exists(command.client_order_id) {
3383            self.generate_order_cancel_rejected(
3384                command.trader_id,
3385                command.strategy_id,
3386                account_id,
3387                command.instrument_id,
3388                command.client_order_id,
3389                command.venue_order_id,
3390                Ustr::from(format!("Order {} not found", command.client_order_id).as_str()),
3391            );
3392            return;
3393        }
3394
3395        let order = match self
3396            .cache
3397            .borrow()
3398            .order(&command.client_order_id)
3399            .map(|o| o.clone())
3400        {
3401            Some(order) => order,
3402            None => {
3403                log::error!(
3404                    "Cannot cancel order: order {} not found in cache",
3405                    command.client_order_id
3406                );
3407                return;
3408            }
3409        };
3410
3411        if !order.is_inflight() && !order.is_open() {
3412            self.purge_stale_core_entry(command.client_order_id);
3413            return;
3414        }
3415
3416        self.cancel_order(&order, None);
3417    }
3418
3419    /// Processes a cancel all orders command for an instrument.
3420    pub fn process_cancel_all(&mut self, command: &CancelAllOrders, _account_id: AccountId) {
3421        let instrument_id = command.instrument_id;
3422        let order_side = if command.order_side == OrderSide::NoOrderSide {
3423            None
3424        } else {
3425            Some(command.order_side)
3426        };
3427
3428        let client_order_ids: Vec<ClientOrderId> = self
3429            .cache
3430            .borrow()
3431            .orders_open(None, Some(&instrument_id), None, None, order_side)
3432            .iter()
3433            .map(|o| o.client_order_id())
3434            .collect();
3435
3436        for client_order_id in client_order_ids {
3437            let order = match self
3438                .cache
3439                .borrow()
3440                .order(&client_order_id)
3441                .map(|o| o.clone())
3442            {
3443                Some(order) => order,
3444                None => continue,
3445            };
3446
3447            if !order.is_inflight() && !order.is_open() {
3448                self.purge_stale_core_entry(client_order_id);
3449                continue;
3450            }
3451
3452            self.cancel_order(&order, None);
3453        }
3454    }
3455
3456    // Removes a closed order's stale entry from the matching core so the next
3457    // `iterate_bids/asks` does not produce a spurious fill action.
3458    fn purge_stale_core_entry(&mut self, client_order_id: ClientOrderId) {
3459        if self.core.order_exists(client_order_id) {
3460            self.delete_core_order(client_order_id);
3461        }
3462        self.cached_filled_qty.swap_remove(&client_order_id);
3463    }
3464
3465    fn resync_core_entry(&mut self, client_order_id: ClientOrderId) -> Option<OrderAny> {
3466        let order = self
3467            .cache
3468            .borrow()
3469            .order(&client_order_id)
3470            .map(|o| o.clone())?;
3471
3472        // Gate on `is_closed`, not `is_open`: cache may transiently hold the
3473        // order in `Submitted` (process_limit_order accepts before cache add)
3474        if order.is_closed() {
3475            self.delete_core_order(client_order_id);
3476            return Some(order);
3477        }
3478
3479        let new_match_info = Self::matching_core_entry(&order);
3480
3481        // Skip the delete+add when unchanged to preserve FIFO at the level
3482        let unchanged = self
3483            .core
3484            .get_order(client_order_id)
3485            .is_some_and(|existing| *existing == new_match_info);
3486
3487        if unchanged {
3488            self.track_post_match_order(&order);
3489            return Some(order);
3490        }
3491
3492        self.delete_core_order(client_order_id);
3493        self.track_post_match_order(&order);
3494        self.core.add_order(new_match_info);
3495        Some(order)
3496    }
3497
3498    /// Processes a batch cancel orders command.
3499    pub fn process_batch_cancel(&mut self, command: &BatchCancelOrders, account_id: AccountId) {
3500        for order in &command.cancels {
3501            self.process_cancel(order, account_id);
3502        }
3503    }
3504
3505    /// Processes a batch modify orders command.
3506    pub fn process_batch_modify(&mut self, command: &BatchModifyOrders, account_id: AccountId) {
3507        for order in &command.modifies {
3508            self.process_modify(order, account_id);
3509        }
3510    }
3511
3512    fn process_market_order(&mut self, order: &OrderAny) {
3513        if order.time_in_force() == TimeInForce::AtTheOpen
3514            || order.time_in_force() == TimeInForce::AtTheClose
3515        {
3516            self.generate_order_rejected(
3517                order,
3518                format!(
3519                    "time in force {} is not currently supported",
3520                    order.time_in_force()
3521                )
3522                .into(),
3523            );
3524            return;
3525        }
3526
3527        // Check if market exists
3528        if (order.order_side() == OrderSide::Buy && self.core.ask.is_none())
3529            || (order.order_side() == OrderSide::Sell && self.core.bid.is_none())
3530        {
3531            self.generate_order_rejected(
3532                order,
3533                format!("No market for {}", order.instrument_id()).into(),
3534            );
3535            return;
3536        }
3537
3538        if self.config.use_market_order_acks {
3539            let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
3540            self.generate_order_accepted(order, venue_order_id);
3541        }
3542
3543        // Add order to cache for fill_market_order to fetch
3544        if let Err(e) = self
3545            .cache
3546            .borrow_mut()
3547            .add_order(order.clone(), None, None, false)
3548        {
3549            log::debug!("Order already in cache: {e}");
3550        }
3551
3552        self.fill_market_order(order.client_order_id());
3553    }
3554
3555    fn process_limit_order(&mut self, order: &mut OrderAny) {
3556        if order.time_in_force() == TimeInForce::AtTheOpen
3557            || order.time_in_force() == TimeInForce::AtTheClose
3558        {
3559            self.generate_order_rejected(
3560                order,
3561                format!(
3562                    "time in force {} is not currently supported",
3563                    order.time_in_force()
3564                )
3565                .into(),
3566            );
3567            return;
3568        }
3569
3570        let limit_px = order.price().expect("Limit order must have a price");
3571        if order.is_post_only()
3572            && self
3573                .core
3574                .is_limit_matched(order.order_side_specified(), limit_px)
3575        {
3576            self.generate_order_rejected(
3577                order,
3578                format!(
3579                    "POST_ONLY {} {} order limit px of {} would have been a TAKER: bid={}, ask={}",
3580                    order.order_type(),
3581                    order.order_side(),
3582                    order.price().unwrap(),
3583                    self.core
3584                        .bid
3585                        .map_or_else(|| "None".to_string(), |p| p.to_string()),
3586                    self.core
3587                        .ask
3588                        .map_or_else(|| "None".to_string(), |p| p.to_string())
3589                )
3590                .into(),
3591            );
3592            return;
3593        }
3594
3595        // Order is valid and accepted
3596        self.accept_order(order);
3597
3598        // Check for immediate fill
3599        if self
3600            .core
3601            .is_limit_matched(order.order_side_specified(), limit_px)
3602        {
3603            // Filling as liquidity taker
3604            order.set_liquidity_side(LiquiditySide::Taker);
3605
3606            if self
3607                .cache
3608                .borrow_mut()
3609                .add_order(order.clone(), None, None, false)
3610                .is_err()
3611                && let Err(e) = self.cache.borrow_mut().replace_order(order)
3612            {
3613                log::debug!("Failed to update order in cache: {e}");
3614            }
3615            self.fill_limit_order(order.client_order_id());
3616
3617            // If fill didn't execute (e.g. all liquidity consumed), revert to
3618            // maker so the fill model check applies on subsequent iterations
3619            if self.core.order_exists(order.client_order_id())
3620                && let Some(mut order) = self.cache.borrow_mut().order_mut(&order.client_order_id())
3621            {
3622                order.set_liquidity_side(LiquiditySide::Maker);
3623            }
3624        } else if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc) {
3625            self.cancel_order(order, None);
3626        } else {
3627            // Add passive order to cache for later modify/cancel operations
3628            order.set_liquidity_side(LiquiditySide::Maker);
3629
3630            if let Some(price) = order.price() {
3631                self.snapshot_queue_position(order, price);
3632            }
3633
3634            let add_result = self
3635                .cache
3636                .borrow_mut()
3637                .add_order(order.clone(), None, None, false);
3638
3639            if let Err(e) = add_result {
3640                log::debug!("Failed to add order to cache: {e}");
3641
3642                // Persist Maker side on the cached copy when exec engine
3643                // already cached the order (only if not already Maker/Taker)
3644                if let Some(mut order) = self.cache.borrow_mut().order_mut(&order.client_order_id())
3645                    && !matches!(
3646                        order.liquidity_side(),
3647                        Some(LiquiditySide::Maker | LiquiditySide::Taker)
3648                    )
3649                {
3650                    order.set_liquidity_side(LiquiditySide::Maker);
3651                }
3652            }
3653        }
3654    }
3655
3656    fn process_market_to_limit_order(&mut self, order: &OrderAny) {
3657        // Check that market exists
3658        if (order.order_side() == OrderSide::Buy && self.core.ask.is_none())
3659            || (order.order_side() == OrderSide::Sell && self.core.bid.is_none())
3660        {
3661            self.generate_order_rejected(
3662                order,
3663                format!("No market for {}", order.instrument_id()).into(),
3664            );
3665            return;
3666        }
3667
3668        if self.config.use_market_order_acks {
3669            let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
3670            self.generate_order_accepted(order, venue_order_id);
3671        }
3672
3673        // Immediately fill marketable order
3674        if let Err(e) = self
3675            .cache
3676            .borrow_mut()
3677            .add_order(order.clone(), None, None, false)
3678        {
3679            log::debug!("Order already in cache: {e}");
3680        }
3681        let client_order_id = order.client_order_id();
3682        self.fill_market_order(client_order_id);
3683
3684        // Check for remaining quantity to rest as limit order
3685        let filled_qty = self
3686            .cached_filled_qty
3687            .get(&client_order_id)
3688            .copied()
3689            .unwrap_or_default();
3690        let leaves_qty = order.quantity().saturating_sub(filled_qty);
3691        if leaves_qty.is_zero() {
3692            self.purge_cached_filled_qty_if_closed(client_order_id);
3693            return;
3694        }
3695
3696        let updated_order = self
3697            .cache
3698            .borrow()
3699            .order(&client_order_id)
3700            .map(|o| o.clone());
3701
3702        if let Some(mut updated_order) = updated_order {
3703            self.accept_order(&mut updated_order);
3704        }
3705    }
3706
3707    fn process_stop_market_order(&mut self, order: &mut OrderAny) {
3708        let stop_px = order
3709            .trigger_price()
3710            .expect("Stop order must have a trigger price");
3711
3712        if self.core.is_stop_matched_with_trigger_type(
3713            order.order_side_specified(),
3714            stop_px,
3715            order.trigger_type().unwrap_or(TriggerType::Default),
3716        ) {
3717            if self.config.reject_stop_orders {
3718                self.generate_order_rejected(
3719                    order,
3720                    format!(
3721                        "{} {} order stop px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3722                        order.order_type(),
3723                        order.order_side(),
3724                        order.trigger_price().unwrap(),
3725                        self.core
3726                            .bid
3727                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
3728                        self.core
3729                            .ask
3730                            .map_or_else(|| "None".to_string(), |p| p.to_string())
3731                    ).into(),
3732                );
3733                return;
3734            }
3735
3736            if let Err(e) = self
3737                .cache
3738                .borrow_mut()
3739                .add_order(order.clone(), None, None, false)
3740            {
3741                log::debug!("Order already in cache: {e}");
3742            }
3743            self.fill_market_order(order.client_order_id());
3744            return;
3745        }
3746
3747        // order is not matched but is valid and we accept it
3748        self.accept_order(order);
3749
3750        // Add passive order to cache for later modify/cancel operations
3751        order.set_liquidity_side(LiquiditySide::Maker);
3752
3753        if let Err(e) = self
3754            .cache
3755            .borrow_mut()
3756            .add_order(order.clone(), None, None, false)
3757        {
3758            log::debug!("Order already in cache: {e}");
3759        }
3760    }
3761
3762    fn process_stop_limit_order(&mut self, order: &mut OrderAny) {
3763        let stop_px = order
3764            .trigger_price()
3765            .expect("Stop order must have a trigger price");
3766
3767        if self.core.is_stop_matched_with_trigger_type(
3768            order.order_side_specified(),
3769            stop_px,
3770            order.trigger_type().unwrap_or(TriggerType::Default),
3771        ) {
3772            if self.config.reject_stop_orders {
3773                self.generate_order_rejected(
3774                    order,
3775                    format!(
3776                        "{} {} order stop px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3777                        order.order_type(),
3778                        order.order_side(),
3779                        order.trigger_price().unwrap(),
3780                        self.core
3781                            .bid
3782                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
3783                        self.core
3784                            .ask
3785                            .map_or_else(|| "None".to_string(), |p| p.to_string())
3786                    ).into(),
3787                );
3788                return;
3789            }
3790
3791            self.accept_triggered_limit_style_order(order);
3792            return;
3793        }
3794
3795        self.accept_order(order);
3796
3797        // Add passive order to cache for later modify/cancel operations
3798        order.set_liquidity_side(LiquiditySide::Maker);
3799
3800        if let Err(e) = self
3801            .cache
3802            .borrow_mut()
3803            .add_order(order.clone(), None, None, false)
3804        {
3805            log::debug!("Order already in cache: {e}");
3806        }
3807    }
3808
3809    fn process_market_if_touched_order(&mut self, order: &mut OrderAny) {
3810        if self.core.is_touch_triggered_with_trigger_type(
3811            order.order_side_specified(),
3812            order.trigger_price().unwrap(),
3813            order.trigger_type().unwrap_or(TriggerType::Default),
3814        ) {
3815            if self.config.reject_stop_orders {
3816                self.generate_order_rejected(
3817                    order,
3818                    format!(
3819                        "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3820                        order.order_type(),
3821                        order.order_side(),
3822                        order.trigger_price().unwrap(),
3823                        self.core
3824                            .bid
3825                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
3826                        self.core
3827                            .ask
3828                            .map_or_else(|| "None".to_string(), |p| p.to_string())
3829                    ).into(),
3830                );
3831                return;
3832            }
3833
3834            if let Err(e) = self
3835                .cache
3836                .borrow_mut()
3837                .add_order(order.clone(), None, None, false)
3838            {
3839                log::debug!("Order already in cache: {e}");
3840            }
3841            self.fill_market_order(order.client_order_id());
3842            return;
3843        }
3844
3845        // Order is valid and accepted
3846        self.accept_order(order);
3847
3848        // Add passive order to cache for later modify/cancel operations
3849        order.set_liquidity_side(LiquiditySide::Maker);
3850
3851        if let Err(e) = self
3852            .cache
3853            .borrow_mut()
3854            .add_order(order.clone(), None, None, false)
3855        {
3856            log::debug!("Order already in cache: {e}");
3857        }
3858    }
3859
3860    fn process_limit_if_touched_order(&mut self, order: &mut OrderAny) {
3861        if self.core.is_touch_triggered_with_trigger_type(
3862            order.order_side_specified(),
3863            order.trigger_price().unwrap(),
3864            order.trigger_type().unwrap_or(TriggerType::Default),
3865        ) {
3866            if self.config.reject_stop_orders {
3867                self.generate_order_rejected(
3868                    order,
3869                    format!(
3870                        "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3871                        order.order_type(),
3872                        order.order_side(),
3873                        order.trigger_price().unwrap(),
3874                        self.core
3875                            .bid
3876                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
3877                        self.core
3878                            .ask
3879                            .map_or_else(|| "None".to_string(), |p| p.to_string())
3880                    ).into(),
3881                );
3882                return;
3883            }
3884            self.accept_triggered_limit_style_order(order);
3885            return;
3886        }
3887
3888        // Order is valid and accepted
3889        self.accept_order(order);
3890
3891        // Add passive order to cache for later modify/cancel operations
3892        order.set_liquidity_side(LiquiditySide::Maker);
3893
3894        if let Err(e) = self
3895            .cache
3896            .borrow_mut()
3897            .add_order(order.clone(), None, None, false)
3898        {
3899            log::debug!("Order already in cache: {e}");
3900        }
3901    }
3902
3903    fn accept_triggered_limit_style_order(&mut self, order: &mut OrderAny) {
3904        self.accept_order(order);
3905
3906        if let Err(e) = self
3907            .cache
3908            .borrow_mut()
3909            .add_order(order.clone(), None, None, false)
3910        {
3911            log::debug!("Order already in cache: {e}");
3912        }
3913
3914        self.trigger_limit_style_stop_order(order.client_order_id(), order.clone());
3915
3916        if let Some(cached_order) = self
3917            .cache
3918            .borrow()
3919            .order(&order.client_order_id())
3920            .map(|order| order.clone())
3921        {
3922            *order = cached_order;
3923        }
3924    }
3925
3926    fn process_trailing_stop_order(&mut self, order: &mut OrderAny) {
3927        if let Some(trigger_price) = order.trigger_price()
3928            && self.core.is_stop_matched_with_trigger_type(
3929                order.order_side_specified(),
3930                trigger_price,
3931                order.trigger_type().unwrap_or(TriggerType::Default),
3932            )
3933        {
3934            self.generate_order_rejected(
3935                    order,
3936                    format!(
3937                        "{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
3938                        order.order_type(),
3939                        order.order_side(),
3940                        trigger_price,
3941                        self.core
3942                            .bid
3943                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
3944                        self.core
3945                            .ask
3946                            .map_or_else(|| "None".to_string(), |p| p.to_string())
3947                    ).into(),
3948                );
3949            return;
3950        }
3951
3952        // Set Maker before `accept_order` so trail-on-accept's cache write
3953        // captures it (a later `set_liquidity_side` would be dropped by the
3954        // `add_order` no-op below).
3955        order.set_liquidity_side(LiquiditySide::Maker);
3956
3957        self.accept_order(order);
3958
3959        if let Err(e) = self
3960            .cache
3961            .borrow_mut()
3962            .add_order(order.clone(), None, None, false)
3963        {
3964            log::debug!("Order already in cache: {e}");
3965        }
3966    }
3967
3968    /// Iterate the matching engine by processing the bid and ask order sides
3969    /// and advancing time up to the given UNIX `timestamp_ns`.
3970    ///
3971    /// The `aggressor_side` parameter is used for trade execution processing.
3972    /// When not `NoAggressor`, the book-based bid/ask reset is skipped to preserve
3973    /// transient trade price overrides.
3974    pub fn iterate(&mut self, timestamp_ns: UnixNanos, aggressor_side: AggressorSide) {
3975        // TODO implement correct clock fixed time setting self.clock.set_time(ts_now);
3976        self.purge_closed_cached_filled_qty();
3977
3978        // Only reset bid/ask from book when not processing trade execution
3979        // (preserves transient trade price override for L2/L3 books). The
3980        // `last_trade_size` gate covers the no-aggressor trade-tick path
3981        // where `process_trade_tick` overrides both sides to the trade
3982        // price; without it the override is undone here.
3983        if aggressor_side == AggressorSide::NoAggressor && self.last_trade_size.is_none() {
3984            if let Some(bid) = self.book.best_bid_price() {
3985                self.core.set_bid_raw(bid);
3986            }
3987
3988            if let Some(ask) = self.book.best_ask_price() {
3989                self.core.set_ask_raw(ask);
3990            }
3991        }
3992
3993        let mut matched_order = false;
3994
3995        if self.market_status == MarketStatus::Open {
3996            // Process bid actions before snapshotting asks so cross-side
3997            // contingencies (OCO/OUO) mutate state between sides
3998            for action in self.core.iterate_bids() {
3999                matched_order = true;
4000
4001                match action {
4002                    MatchAction::FillLimit(id) => self.fill_limit_order(id),
4003                    MatchAction::TriggerStop(id) => self.trigger_stop_order(id),
4004                }
4005            }
4006
4007            for action in self.core.iterate_asks() {
4008                matched_order = true;
4009
4010                match action {
4011                    MatchAction::FillLimit(id) => self.fill_limit_order(id),
4012                    MatchAction::TriggerStop(id) => self.trigger_stop_order(id),
4013                }
4014            }
4015        }
4016
4017        let order_ids: Vec<ClientOrderId> = if matched_order {
4018            self.core.iter_orders().map(|m| m.client_order_id).collect()
4019        } else if self.post_match_order_ids.is_empty() {
4020            Vec::new()
4021        } else {
4022            self.core
4023                .iter_orders()
4024                .filter_map(|order| {
4025                    self.post_match_order_ids
4026                        .contains(&order.client_order_id)
4027                        .then_some(order.client_order_id)
4028                })
4029                .collect()
4030        };
4031        let support_gtd_orders = self.config.support_gtd_orders;
4032
4033        for client_order_id in order_ids {
4034            let (action, keep_tracking) = {
4035                let cache = self.cache.borrow();
4036                let Some(order) = cache.order(&client_order_id) else {
4037                    self.post_match_order_ids.swap_remove(&client_order_id);
4038                    continue;
4039                };
4040
4041                (
4042                    post_match_order_action(&order, support_gtd_orders, timestamp_ns, |order| {
4043                        order.clone()
4044                    }),
4045                    Self::requires_post_match_maintenance(&order),
4046                )
4047            };
4048
4049            match action {
4050                PostMatchOrderAction::RemoveClosed => {
4051                    self.delete_core_order(client_order_id);
4052                    self.cached_filled_qty.swap_remove(&client_order_id);
4053                    continue;
4054                }
4055                PostMatchOrderAction::Expire(order) => {
4056                    self.delete_core_order(client_order_id);
4057                    self.cached_filled_qty.swap_remove(&client_order_id);
4058                    self.expire_order(&order);
4059                    continue;
4060                }
4061                PostMatchOrderAction::UpdateTrailing(mut order) => {
4062                    if self.maybe_activate_trailing_stop(
4063                        &mut order,
4064                        self.core.bid,
4065                        self.core.ask,
4066                        self.core.last,
4067                    ) {
4068                        self.update_trailing_stop_order(&order);
4069                        self.resync_core_entry(client_order_id);
4070                    }
4071                }
4072                PostMatchOrderAction::NoMaintenance => {
4073                    if !keep_tracking {
4074                        self.post_match_order_ids.swap_remove(&client_order_id);
4075                    }
4076                }
4077            }
4078
4079            // Single-shot: only the first order after a trigger fill sees
4080            // the mutated core; the restore clears the override here.
4081            if self.target_bid.is_some() || self.target_ask.is_some() || self.target_last.is_some()
4082            {
4083                if let Some(t) = self.target_bid.take() {
4084                    self.core.bid = Some(t);
4085                }
4086
4087                if let Some(t) = self.target_ask.take() {
4088                    self.core.ask = Some(t);
4089                }
4090
4091                if let Some(t) = self.target_last.take() {
4092                    self.core.last = Some(t);
4093                }
4094            }
4095        }
4096
4097        // Fallback for when the per-order loop hit no eligible order (e.g.,
4098        // all closed by the matching pass) so the fill override on
4099        // `core.last` cannot leak into the next iterate.
4100        if let Some(t) = self.target_bid.take() {
4101            self.core.bid = Some(t);
4102        }
4103
4104        if let Some(t) = self.target_ask.take() {
4105            self.core.ask = Some(t);
4106        }
4107
4108        if let Some(t) = self.target_last.take() {
4109            self.core.last = Some(t);
4110        }
4111
4112        // Restore core bid/ask to book values after iteration
4113        // (during trade execution, transient override was used for matching)
4114        self.core.bid = self.book.best_bid_price();
4115        self.core.ask = self.book.best_ask_price();
4116
4117        // Process instrument expiration last so orders at the expiration tick
4118        // get a chance to fill before positions are closed.
4119        self.check_instrument_expiration(timestamp_ns);
4120        self.purge_closed_cached_filled_qty();
4121    }
4122
4123    fn get_trailing_activation_price(
4124        &self,
4125        trigger_type: TriggerType,
4126        order_side: OrderSide,
4127        bid: Option<Price>,
4128        ask: Option<Price>,
4129        last: Option<Price>,
4130    ) -> Option<Price> {
4131        match trigger_type {
4132            TriggerType::LastPrice => last,
4133            TriggerType::LastOrBidAsk => last.or(match order_side {
4134                OrderSide::Buy => ask,
4135                OrderSide::Sell => bid,
4136                _ => None,
4137            }),
4138            // Default, BidAsk, DoubleBidAsk, DoubleLastPrice, IndexPrice, MarkPrice
4139            _ => match order_side {
4140                OrderSide::Buy => ask,
4141                OrderSide::Sell => bid,
4142                _ => None,
4143            },
4144        }
4145    }
4146
4147    fn maybe_activate_trailing_stop(
4148        &self,
4149        order: &mut OrderAny,
4150        bid: Option<Price>,
4151        ask: Option<Price>,
4152        last: Option<Price>,
4153    ) -> bool {
4154        match order {
4155            OrderAny::TrailingStopMarket(inner) => {
4156                if inner.is_activated {
4157                    return true;
4158                }
4159
4160                if inner.activation_price.is_none() {
4161                    let px = self.get_trailing_activation_price(
4162                        inner.trigger_type,
4163                        inner.order_side(),
4164                        bid,
4165                        ask,
4166                        last,
4167                    );
4168
4169                    if let Some(p) = px {
4170                        inner.activation_price = Some(p);
4171                        inner.set_activated();
4172
4173                        if let Err(e) = self.cache.borrow_mut().replace_order(order) {
4174                            log::error!("Failed to update order: {e}");
4175                        }
4176                        return true;
4177                    }
4178                    return false;
4179                }
4180
4181                let activation_price = inner.activation_price.unwrap();
4182                let hit = match inner.order_side() {
4183                    OrderSide::Buy => ask.is_some_and(|a| a <= activation_price),
4184                    OrderSide::Sell => bid.is_some_and(|b| b >= activation_price),
4185                    _ => false,
4186                };
4187
4188                if hit {
4189                    inner.set_activated();
4190
4191                    if let Err(e) = self.cache.borrow_mut().replace_order(order) {
4192                        log::error!("Failed to update order: {e}");
4193                    }
4194                }
4195                hit
4196            }
4197            OrderAny::TrailingStopLimit(inner) => {
4198                if inner.is_activated {
4199                    return true;
4200                }
4201
4202                if inner.activation_price.is_none() {
4203                    let px = self.get_trailing_activation_price(
4204                        inner.trigger_type,
4205                        inner.order_side(),
4206                        bid,
4207                        ask,
4208                        last,
4209                    );
4210
4211                    if let Some(p) = px {
4212                        inner.activation_price = Some(p);
4213                        inner.set_activated();
4214
4215                        if let Err(e) = self.cache.borrow_mut().replace_order(order) {
4216                            log::error!("Failed to update order: {e}");
4217                        }
4218                        return true;
4219                    }
4220                    return false;
4221                }
4222
4223                let activation_price = inner.activation_price.unwrap();
4224                let hit = match inner.order_side() {
4225                    OrderSide::Buy => ask.is_some_and(|a| a <= activation_price),
4226                    OrderSide::Sell => bid.is_some_and(|b| b >= activation_price),
4227                    _ => false,
4228                };
4229
4230                if hit {
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                }
4237                hit
4238            }
4239            _ => true,
4240        }
4241    }
4242
4243    fn determine_limit_price_and_volume(&mut self, order: &OrderAny) -> Vec<(Price, Quantity)> {
4244        match order.price() {
4245            Some(order_price) => {
4246                // When liquidity consumption is enabled, get ALL crossed levels so that
4247                // consumed levels can be filtered out while still finding valid ones.
4248                // Otherwise simulate_fills only returns enough levels to satisfy leaves_qty,
4249                // which may all be consumed, missing other valid crossed levels.
4250                let mut fills = if self.config.liquidity_consumption {
4251                    let size_prec = self.instrument.size_precision();
4252                    self.book
4253                        .get_all_crossed_levels(order.order_side(), order_price, size_prec)
4254                } else {
4255                    let book_order =
4256                        BookOrder::new(order.order_side(), order_price, order.quantity(), 1);
4257                    self.book.simulate_fills(&book_order)
4258                };
4259
4260                // Trade execution: use trade-driven fill when book doesn't reflect trade price
4261                if let Some(trade_size) = self.last_trade_size
4262                    && let Some(trade_price) = self.core.last
4263                {
4264                    let fills_at_trade_price = fills.iter().any(|(px, _)| *px == trade_price);
4265
4266                    if !fills_at_trade_price
4267                        && self
4268                            .core
4269                            .is_limit_matched(order.order_side_specified(), order_price)
4270                    {
4271                        // Fill model check for MAKER at limit is already handled in fill_limit_order,
4272                        // don't re-check here to avoid calling is_limit_filled() twice (p² probability).
4273                        let leaves_qty = order.leaves_qty();
4274                        let available_qty = if self.config.liquidity_consumption {
4275                            let remaining = trade_size.raw.saturating_sub(self.trade_consumption);
4276                            Quantity::from_raw(remaining, trade_size.precision)
4277                        } else {
4278                            trade_size
4279                        };
4280
4281                        let fill_qty = min(leaves_qty, available_qty);
4282
4283                        if !fill_qty.is_zero() {
4284                            log::debug!(
4285                                "Trade execution fill: {} @ {} (trade_price={}, available: {}, book had {} fills)",
4286                                fill_qty,
4287                                order_price,
4288                                trade_price,
4289                                available_qty,
4290                                fills.len()
4291                            );
4292
4293                            if self.config.liquidity_consumption {
4294                                self.trade_consumption += fill_qty.raw;
4295                            }
4296
4297                            // Fill at the limit price (conservative) rather than the trade price.
4298                            // Trade execution fills already account for consumption via trade_consumption,
4299                            // return early to bypass apply_liquidity_consumption which would incorrectly
4300                            // discard these fills when the trade price isn't in the order book.
4301                            return vec![(order_price, fill_qty)];
4302                        }
4303                    }
4304                }
4305
4306                // Return immediately if no fills
4307                if fills.is_empty() {
4308                    return fills;
4309                }
4310
4311                // Save original book prices BEFORE any fill price modifications for consumption tracking,
4312                // since the MAKER loop below may adjust fill prices. Consumption should be
4313                // tracked against the original book price levels where liquidity was sourced from.
4314                let book_prices: Vec<Price> = if self.config.liquidity_consumption {
4315                    fills.iter().map(|(px, _)| *px).collect()
4316                } else {
4317                    Vec::new()
4318                };
4319                let book_prices_ref: Option<&[Price]> = if book_prices.is_empty() {
4320                    None
4321                } else {
4322                    Some(&book_prices)
4323                };
4324
4325                // Filling as MAKER from trigger
4326                if order
4327                    .liquidity_side()
4328                    .is_some_and(|liquidity_side| liquidity_side == LiquiditySide::Maker)
4329                {
4330                    match order.order_side().as_specified() {
4331                        OrderSideSpecified::Buy => {
4332                            let target_price = if order
4333                                .trigger_price()
4334                                .is_some_and(|trigger_price| order_price > trigger_price)
4335                            {
4336                                order.trigger_price().unwrap()
4337                            } else {
4338                                order_price
4339                            };
4340
4341                            for fill in &mut fills {
4342                                let last_px = fill.0;
4343                                if last_px < order_price {
4344                                    // Marketable BUY would have filled at limit
4345                                    self.target_bid = self.core.bid;
4346                                    self.target_ask = self.core.ask;
4347                                    self.target_last = self.core.last;
4348                                    self.core.set_ask_raw(target_price);
4349                                    self.core.set_last_raw(target_price);
4350                                    fill.0 = target_price;
4351                                }
4352                            }
4353                        }
4354                        OrderSideSpecified::Sell => {
4355                            let target_price = if order
4356                                .trigger_price()
4357                                .is_some_and(|trigger_price| order_price < trigger_price)
4358                            {
4359                                order.trigger_price().unwrap()
4360                            } else {
4361                                order_price
4362                            };
4363
4364                            for fill in &mut fills {
4365                                let last_px = fill.0;
4366                                if last_px > order_price {
4367                                    // Marketable SELL would have filled at limit
4368                                    self.target_bid = self.core.bid;
4369                                    self.target_ask = self.core.ask;
4370                                    self.target_last = self.core.last;
4371                                    self.core.set_bid_raw(target_price);
4372                                    self.core.set_last_raw(target_price);
4373                                    fill.0 = target_price;
4374                                }
4375                            }
4376                        }
4377                    }
4378                }
4379
4380                self.apply_liquidity_consumption(
4381                    fills,
4382                    order.order_side(),
4383                    order.leaves_qty(),
4384                    book_prices_ref,
4385                )
4386            }
4387            None => panic!("Limit order must have a price"),
4388        }
4389    }
4390
4391    fn determine_market_price_and_volume(&self, order: &OrderAny) -> Vec<(Price, Quantity)> {
4392        let price = match order.order_side().as_specified() {
4393            OrderSideSpecified::Buy => Price::max(FIXED_PRECISION),
4394            OrderSideSpecified::Sell => Price::min(FIXED_PRECISION),
4395        };
4396
4397        // When liquidity consumption is enabled, get ALL crossed levels so that
4398        // consumed levels can be filtered out while still finding valid ones.
4399        let mut fills = if self.config.liquidity_consumption {
4400            let size_prec = self.instrument.size_precision();
4401            self.book
4402                .get_all_crossed_levels(order.order_side(), price, size_prec)
4403        } else {
4404            let book_order = BookOrder::new(order.order_side(), price, order.quantity(), 0);
4405            self.book.simulate_fills(&book_order)
4406        };
4407
4408        // For stop market and market-if-touched orders during bar H/L/C processing, fill at trigger price
4409        // (market moved through the trigger). For gaps/immediate triggers, fill at market.
4410        if !self.fill_at_market
4411            && self.book_type == BookType::L1_MBP
4412            && !fills.is_empty()
4413            && matches!(
4414                order.order_type(),
4415                OrderType::StopMarket | OrderType::TrailingStopMarket | OrderType::MarketIfTouched
4416            )
4417            && let Some(trigger_price) = order.trigger_price()
4418        {
4419            fills[0] = (trigger_price, fills[0].1);
4420
4421            // Skip liquidity consumption for trigger price fills (gap price may not exist in book).
4422            let mut remaining_qty = order.leaves_qty().raw;
4423            let mut capped_fills = Vec::with_capacity(fills.len());
4424
4425            for (price, qty) in fills {
4426                if remaining_qty == 0 {
4427                    break;
4428                }
4429
4430                let capped_qty_raw = min(qty.raw, remaining_qty);
4431                if capped_qty_raw == 0 {
4432                    continue;
4433                }
4434
4435                remaining_qty -= capped_qty_raw;
4436                capped_fills.push((price, Quantity::from_raw(capped_qty_raw, qty.precision)));
4437            }
4438
4439            return capped_fills;
4440        }
4441
4442        fills
4443    }
4444
4445    fn determine_market_fill_model_price_and_volume(
4446        &mut self,
4447        order: &OrderAny,
4448    ) -> anyhow::Result<(Vec<(Price, Quantity)>, bool)> {
4449        if let (Some(best_bid), Some(best_ask)) = (self.core.bid, self.core.ask)
4450            && let Some(book) = self.fill_model.get_orderbook_for_fill_simulation(
4451                &self.instrument,
4452                order,
4453                best_bid,
4454                best_ask,
4455            )?
4456        {
4457            let price = match order.order_side().as_specified() {
4458                OrderSideSpecified::Buy => Price::max(FIXED_PRECISION),
4459                OrderSideSpecified::Sell => Price::min(FIXED_PRECISION),
4460            };
4461            let book_order = BookOrder::new(order.order_side(), price, order.quantity(), 0);
4462            let fills = book.simulate_fills(&book_order);
4463            if !fills.is_empty() {
4464                return Ok((fills, true));
4465            }
4466        }
4467        Ok((self.determine_market_price_and_volume(order), false))
4468    }
4469
4470    fn determine_limit_fill_model_price_and_volume(
4471        &mut self,
4472        order: &OrderAny,
4473    ) -> anyhow::Result<Vec<(Price, Quantity)>> {
4474        if let (Some(best_bid), Some(best_ask)) = (self.core.bid, self.core.ask)
4475            && let Some(book) = self.fill_model.get_orderbook_for_fill_simulation(
4476                &self.instrument,
4477                order,
4478                best_bid,
4479                best_ask,
4480            )?
4481            && let Some(limit_price) = order.price()
4482        {
4483            let book_order = BookOrder::new(order.order_side(), limit_price, order.quantity(), 0);
4484            let fills = book.simulate_fills(&book_order);
4485            if !fills.is_empty() {
4486                return Ok(fills);
4487            }
4488        }
4489        Ok(self.determine_limit_price_and_volume(order))
4490    }
4491
4492    /// Fills a market order against the current order book.
4493    ///
4494    /// The order is filled as a taker against available liquidity.
4495    /// Reduce-only orders are canceled if no position exists.
4496    pub fn fill_market_order(&mut self, client_order_id: ClientOrderId) {
4497        let mut order = match self
4498            .cache
4499            .borrow()
4500            .order(&client_order_id)
4501            .map(|o| o.clone())
4502        {
4503            Some(order) => order,
4504            None => {
4505                log::error!("Cannot fill market order: order {client_order_id} not found in cache");
4506                return;
4507            }
4508        };
4509
4510        if order.is_closed() {
4511            self.purge_stale_core_entry(client_order_id);
4512            return;
4513        }
4514
4515        // Convert quote-denominated quantity at fill time for trigger-style market
4516        // orders that skipped conversion at submission. Idempotent: orders already
4517        // converted have `is_quote_quantity == false`.
4518        if order.is_quote_quantity()
4519            && !self.instrument.is_inverse()
4520            && !self.convert_quote_to_base_quantity(&mut order)
4521        {
4522            return;
4523        }
4524
4525        if let Some(filled_qty) = self.cached_filled_qty.get(&order.client_order_id())
4526            && filled_qty >= &order.quantity()
4527        {
4528            log::debug!(
4529                "Ignoring fill as already filled pending application of events: {:?}, {:?}, {:?}, {:?}",
4530                filled_qty,
4531                order.quantity(),
4532                order.filled_qty(),
4533                order.quantity()
4534            );
4535            return;
4536        }
4537
4538        let (venue_position_id, position) = self.fill_position_for_order(&order, Some(true));
4539
4540        if self.config.use_reduce_only && order.is_reduce_only() && position.is_none() {
4541            log::warn!(
4542                "Canceling REDUCE_ONLY {} as would increase position",
4543                order.order_type()
4544            );
4545            self.cancel_order(&order, None);
4546            return;
4547        }
4548
4549        order.set_liquidity_side(LiquiditySide::Taker);
4550        let (mut fills, from_synthetic) =
4551            match self.determine_market_fill_model_price_and_volume(&order) {
4552                Ok(result) => result,
4553                Err(e) => {
4554                    log::error!(
4555                        "Cannot fill market order {}: fill model failed: {e}",
4556                        order.client_order_id()
4557                    );
4558                    return;
4559                }
4560            };
4561
4562        // Apply protection price filtering at fill time (trigger-time semantics for stops)
4563        let protection_price: Option<Price> = if let Some(protection_points) =
4564            self.config.price_protection_points
4565            && matches!(
4566                order.order_type(),
4567                OrderType::Market | OrderType::StopMarket
4568            ) {
4569            protection_price_calculate(
4570                self.instrument.price_increment(),
4571                &order,
4572                protection_points,
4573                self.core.bid,
4574                self.core.ask,
4575            )
4576            .ok()
4577        } else {
4578            None
4579        };
4580
4581        if let Some(protection_price) = protection_price {
4582            fills = self.filter_fills_by_protection(fills, &order, protection_price);
4583        }
4584
4585        // Skip consumption for synthetic fill-model books (prices may not exist
4586        // in the real book) and trigger price fills (gap price may not exist)
4587        let is_trigger_price_fill = !self.fill_at_market
4588            && self.book_type == BookType::L1_MBP
4589            && matches!(
4590                order.order_type(),
4591                OrderType::StopMarket | OrderType::TrailingStopMarket | OrderType::MarketIfTouched
4592            )
4593            && order.trigger_price().is_some();
4594
4595        if !from_synthetic && !is_trigger_price_fill {
4596            fills = self.apply_liquidity_consumption(
4597                fills,
4598                order.order_side(),
4599                order.leaves_qty(),
4600                None,
4601            );
4602        }
4603
4604        if let Err(e) = self.apply_fills(
4605            &order,
4606            &fills,
4607            LiquiditySide::Taker,
4608            if self.config.use_reduce_only && order.is_reduce_only() {
4609                venue_position_id
4610            } else {
4611                None
4612            },
4613            position.as_ref(),
4614            protection_price,
4615        ) {
4616            log::error!("Cannot fill market order {}: {e}", order.client_order_id());
4617        }
4618    }
4619
4620    fn filter_fills_by_protection(
4621        &self,
4622        fills: Vec<(Price, Quantity)>,
4623        order: &OrderAny,
4624        protection_price: Price,
4625    ) -> Vec<(Price, Quantity)> {
4626        let protection_raw = protection_price.raw;
4627        fills
4628            .into_iter()
4629            .filter(|(fill_price, _)| {
4630                match order.order_side() {
4631                    // BUY: only fill at prices <= protection_price
4632                    OrderSide::Buy => fill_price.raw <= protection_raw,
4633                    // SELL: only fill at prices >= protection_price
4634                    OrderSide::Sell => fill_price.raw >= protection_raw,
4635                    OrderSide::NoOrderSide => false,
4636                }
4637            })
4638            .collect()
4639    }
4640
4641    /// Attempts to fill a limit order against the current order book.
4642    ///
4643    /// Determines fill prices and quantities based on available liquidity,
4644    /// then applies the fills to the order.
4645    ///
4646    /// # Panics
4647    ///
4648    /// Panics if the order has no price (design error).
4649    pub fn fill_limit_order(&mut self, client_order_id: ClientOrderId) {
4650        let mut order = match self
4651            .cache
4652            .borrow()
4653            .order(&client_order_id)
4654            .map(|o| o.clone())
4655        {
4656            Some(order) => order,
4657            None => {
4658                log::error!("Cannot fill limit order: order {client_order_id} not found in cache");
4659                return;
4660            }
4661        };
4662
4663        if order.is_closed() {
4664            self.purge_stale_core_entry(client_order_id);
4665            return;
4666        }
4667
4668        // Convert quote-denominated quantity at fill time for orders that entered
4669        // this path still carrying a quote notional (e.g. trailing-stop-limit with
4670        // a late-assigned price). Idempotent for already-converted orders.
4671        if order.is_quote_quantity()
4672            && !self.instrument.is_inverse()
4673            && !self.convert_quote_to_base_quantity(&mut order)
4674        {
4675            return;
4676        }
4677
4678        match order.price() {
4679            Some(order_price) => {
4680                let cached_filled_qty = self.cached_filled_qty.get(&order.client_order_id());
4681                if let Some(&qty) = cached_filled_qty
4682                    && qty >= order.quantity()
4683                {
4684                    log::debug!(
4685                        "Ignoring fill as already filled pending application of events: {}, {}, {}, {}",
4686                        qty,
4687                        order.quantity(),
4688                        order.filled_qty(),
4689                        order.leaves_qty(),
4690                    );
4691                    return;
4692                }
4693
4694                // Check fill model for MAKER orders at the limit price
4695                if order
4696                    .liquidity_side()
4697                    .is_some_and(|liquidity_side| liquidity_side == LiquiditySide::Maker)
4698                {
4699                    // For trade execution: check if trade price equals order price
4700                    // For quote updates: check if bid/ask equals order price
4701                    let at_limit = if self.last_trade_size.is_some() && self.core.last.is_some() {
4702                        self.core.last.is_some_and(|last| last == order_price)
4703                    } else if order.order_side() == OrderSide::Buy {
4704                        self.core.bid.is_some_and(|bid| bid == order_price)
4705                    } else {
4706                        self.core.ask.is_some_and(|ask| ask == order_price)
4707                    };
4708
4709                    if at_limit {
4710                        let is_limit_filled = match self.fill_model.is_limit_filled() {
4711                            Ok(value) => value,
4712                            Err(e) => {
4713                                log::error!(
4714                                    "Cannot fill limit order {}: fill model failed: {e}",
4715                                    order.client_order_id()
4716                                );
4717                                return;
4718                            }
4719                        };
4720
4721                        if !is_limit_filled {
4722                            return; // Not filled (simulates queue position)
4723                        }
4724                    }
4725                }
4726
4727                let queue_allowed_raw = if self.config.queue_position {
4728                    match self.determine_trade_fill_qty(&order) {
4729                        None | Some(0) => {
4730                            if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc)
4731                            {
4732                                self.cancel_order(&order, None);
4733                            }
4734                            return;
4735                        }
4736                        Some(allowed) => Some(allowed),
4737                    }
4738                } else {
4739                    None
4740                };
4741
4742                let (venue_position_id, position) = self.fill_position_for_order(&order, None);
4743
4744                if self.config.use_reduce_only && order.is_reduce_only() && position.is_none() {
4745                    log::warn!(
4746                        "Canceling REDUCE_ONLY {} as would increase position",
4747                        order.order_type()
4748                    );
4749                    self.cancel_order(&order, None);
4750                    return;
4751                }
4752
4753                let tc_before = self.trade_consumption;
4754                let mut fills = match self.determine_limit_fill_model_price_and_volume(&order) {
4755                    Ok(fills) => fills,
4756                    Err(e) => {
4757                        log::error!(
4758                            "Cannot fill limit order {}: fill model failed: {e}",
4759                            order.client_order_id()
4760                        );
4761                        return;
4762                    }
4763                };
4764
4765                if let Some(allowed_raw) = queue_allowed_raw {
4766                    let size_prec = self.instrument.size_precision();
4767                    let mut remaining = allowed_raw;
4768                    fills = fills
4769                        .into_iter()
4770                        .filter_map(|(price, qty)| {
4771                            if remaining == 0 {
4772                                return None;
4773                            }
4774                            let capped = qty.raw.min(remaining);
4775                            remaining -= capped;
4776                            Some((price, Quantity::from_raw(capped, size_prec)))
4777                        })
4778                        .collect();
4779
4780                    // Consume excess and reconcile trade budget after capping
4781                    let consumed: QuantityRaw = fills.iter().map(|(_, qty)| qty.raw).sum();
4782
4783                    if let Some(excess) = self.queue_excess.get_mut(&order.client_order_id()) {
4784                        *excess = excess.saturating_sub(consumed);
4785                    }
4786                    self.trade_consumption = tc_before + consumed;
4787                }
4788
4789                // Skip apply_fills when consumed-liquidity adjustment produces no fills.
4790                // This occurs for partially filled orders when an unrelated delta arrives
4791                // and no new liquidity is available at the order's price level.
4792                if fills.is_empty() && self.config.liquidity_consumption {
4793                    log::debug!(
4794                        "Skipping fill for {}: no liquidity available after consumption",
4795                        order.client_order_id()
4796                    );
4797
4798                    if matches!(order.time_in_force(), TimeInForce::Fok | TimeInForce::Ioc) {
4799                        self.cancel_order(&order, None);
4800                    }
4801
4802                    return;
4803                }
4804
4805                let liquidity_side = order.liquidity_side().unwrap();
4806                if let Err(e) = self.apply_fills(
4807                    &order,
4808                    &fills,
4809                    liquidity_side,
4810                    venue_position_id,
4811                    position.as_ref(),
4812                    None,
4813                ) {
4814                    log::error!("Cannot fill limit order {}: {e}", order.client_order_id());
4815                }
4816            }
4817            None => panic!("Limit order must have a price"),
4818        }
4819    }
4820
4821    fn fill_position_for_order(
4822        &mut self,
4823        order: &OrderAny,
4824        generate: Option<bool>,
4825    ) -> (Option<PositionId>, Option<Position>) {
4826        if self.oms_type == OmsType::Hedging
4827            && self.config.use_reduce_only
4828            && order.is_reduce_only()
4829        {
4830            let cache = self.cache.as_ref().borrow();
4831
4832            if let Some(position) = cache.position_for_order(&order.client_order_id()) {
4833                let position = position.cloned();
4834                return (Some(position.id), Some(position));
4835            }
4836
4837            if let Some(position) = Self::open_position_reduced_by_order(&cache, order) {
4838                return (Some(position.id), Some(position));
4839            }
4840        }
4841
4842        let venue_position_id = self.ids_generator.get_position_id(order, generate);
4843
4844        let position = {
4845            let cache = self.cache.as_ref().borrow();
4846            venue_position_id
4847                .as_ref()
4848                .and_then(|position_id| cache.position_owned(position_id))
4849        };
4850
4851        (venue_position_id, position)
4852    }
4853
4854    fn position_for_order_in_cache(&self, cache: &Cache, order: &OrderAny) -> Option<Position> {
4855        if let Some(position) = cache.position_for_order(&order.client_order_id()) {
4856            return Some(position.cloned());
4857        }
4858
4859        if self.oms_type == OmsType::Netting {
4860            let position_id = PositionId::new(
4861                format!("{}-{}", order.instrument_id(), order.strategy_id()).as_str(),
4862            );
4863            return cache
4864                .position(&position_id)
4865                .map(|position| position.cloned());
4866        }
4867
4868        if self.oms_type == OmsType::Hedging
4869            && self.config.use_reduce_only
4870            && order.is_reduce_only()
4871        {
4872            return Self::open_position_reduced_by_order(cache, order);
4873        }
4874
4875        None
4876    }
4877
4878    fn open_position_reduced_by_order(cache: &Cache, order: &OrderAny) -> Option<Position> {
4879        cache
4880            .positions_open(
4881                None,
4882                Some(&order.instrument_id()),
4883                Some(&order.strategy_id()),
4884                None,
4885                None,
4886            )
4887            .into_iter()
4888            .find(|position| order.would_reduce_only(position.side, position.quantity))
4889            .map(|position| position.cloned())
4890    }
4891
4892    fn apply_fills(
4893        &mut self,
4894        order: &OrderAny,
4895        fills: &[(Price, Quantity)],
4896        liquidity_side: LiquiditySide,
4897        venue_position_id: Option<PositionId>,
4898        position: Option<&Position>,
4899        protection_price: Option<Price>,
4900    ) -> anyhow::Result<()> {
4901        if order.time_in_force() == TimeInForce::Fok {
4902            let mut total_size = Quantity::zero(order.quantity().precision);
4903
4904            for &(fill_px, fill_qty) in fills {
4905                if self
4906                    .normalize_price_for_current_instrument(fill_px)
4907                    .is_some()
4908                    && let Some(fill_qty) = self.normalize_quantity_for_current_instrument(fill_qty)
4909                {
4910                    total_size = total_size.add(fill_qty);
4911                }
4912            }
4913
4914            if order.leaves_qty() > total_size {
4915                self.cancel_order(order, None);
4916                return Ok(());
4917            }
4918        }
4919
4920        if fills.is_empty() {
4921            if order.status() == OrderStatus::Submitted {
4922                self.generate_order_rejected(
4923                    order,
4924                    format!("No market for {}", order.instrument_id()).into(),
4925                );
4926            } else {
4927                log::error!(
4928                    "Cannot fill order: no fills from book when fills were expected (check size in data)"
4929                );
4930                return Ok(());
4931            }
4932        }
4933
4934        // For netting mode, don't use venue position ID (use None instead)
4935        let venue_position_id = if self.oms_type == OmsType::Netting {
4936            None
4937        } else {
4938            venue_position_id
4939        };
4940
4941        let mut initial_market_to_limit_fill = false;
4942        let mut total_filled = self
4943            .cached_filled_qty
4944            .get(&order.client_order_id())
4945            .copied()
4946            .unwrap_or_else(|| order.filled_qty());
4947        let initial_total_filled = total_filled;
4948        let mut last_fill_px: Option<Price> = None;
4949        let mut reduce_only_remaining_raw = None;
4950        let mut reduce_only_filled_raw = None;
4951
4952        if self.config.use_reduce_only
4953            && order.is_reduce_only()
4954            && let Some(current_position) = position
4955        {
4956            reduce_only_remaining_raw = Some(current_position.quantity.raw);
4957            reduce_only_filled_raw = Some(total_filled.raw);
4958        }
4959
4960        for &(fill_px, fill_qty) in fills {
4961            let Some(mut fill_px) = self.normalize_fill_price(fill_px, order.client_order_id())
4962            else {
4963                continue;
4964            };
4965
4966            let Some(fill_qty) = self.normalize_fill_quantity(fill_qty, order.client_order_id())
4967            else {
4968                continue;
4969            };
4970
4971            if order.filled_qty() == Quantity::zero(order.filled_qty().precision)
4972                && order.order_type() == OrderType::MarketToLimit
4973            {
4974                self.generate_order_updated(order, order.quantity(), Some(fill_px), None, None);
4975                initial_market_to_limit_fill = true;
4976            }
4977
4978            if self.book_type == BookType::L1_MBP && self.fill_model.is_slipped()? {
4979                fill_px = match order.order_side().as_specified() {
4980                    OrderSideSpecified::Buy => fill_px.add(self.instrument.price_increment()),
4981                    OrderSideSpecified::Sell => fill_px.sub(self.instrument.price_increment()),
4982                }
4983            }
4984
4985            let mut effective_fill_qty = fill_qty;
4986
4987            if let Some(remaining_raw) = reduce_only_remaining_raw {
4988                if remaining_raw == 0 {
4989                    return Ok(());
4990                }
4991
4992                if effective_fill_qty.raw > remaining_raw {
4993                    effective_fill_qty =
4994                        Quantity::from_raw(remaining_raw, effective_fill_qty.precision);
4995                }
4996            }
4997
4998            if fill_qty.is_zero() {
4999                if fills.len() == 1 && order.status() == OrderStatus::Submitted {
5000                    self.generate_order_rejected(
5001                        order,
5002                        format!("No market for {}", order.instrument_id()).into(),
5003                    );
5004                }
5005                return Ok(());
5006            }
5007
5008            // Mirror `fill_order`'s leaves cap
5009            let capped_fill_qty = min(
5010                effective_fill_qty,
5011                order.quantity().saturating_sub(total_filled),
5012            );
5013            let reduce_only_exhausts_position = reduce_only_remaining_raw
5014                .is_some_and(|remaining_raw| capped_fill_qty.raw >= remaining_raw);
5015
5016            if reduce_only_exhausts_position {
5017                let reduce_only_target_raw = reduce_only_filled_raw
5018                    .unwrap_or(initial_total_filled.raw)
5019                    .checked_add(capped_fill_qty.raw)
5020                    .expect("Overflow occurred when adding reduce-only target quantity");
5021                let reduce_only_target =
5022                    Quantity::from_raw(reduce_only_target_raw, order.quantity().precision);
5023
5024                if order.quantity() != reduce_only_target {
5025                    self.generate_order_updated(order, reduce_only_target, None, None, None);
5026                }
5027            }
5028
5029            total_filled = total_filled.add(capped_fill_qty);
5030
5031            if let Some(remaining_raw) = reduce_only_remaining_raw.as_mut() {
5032                *remaining_raw = remaining_raw.saturating_sub(capped_fill_qty.raw);
5033            }
5034
5035            if let Some(filled_raw) = reduce_only_filled_raw.as_mut() {
5036                *filled_raw = filled_raw
5037                    .checked_add(capped_fill_qty.raw)
5038                    .expect("Overflow occurred when adding reduce-only filled quantity");
5039            }
5040
5041            self.fill_order(
5042                order,
5043                fill_px,
5044                effective_fill_qty,
5045                liquidity_side,
5046                venue_position_id,
5047                position,
5048            )?;
5049            last_fill_px = Some(fill_px);
5050
5051            if order.order_type() == OrderType::MarketToLimit && initial_market_to_limit_fill {
5052                // Filled initial level
5053                return Ok(());
5054            }
5055
5056            if reduce_only_exhausts_position {
5057                self.purge_cached_filled_qty_if_closed(order.client_order_id());
5058                return Ok(());
5059            }
5060        }
5061
5062        let leaves_remaining = total_filled < order.quantity();
5063        let filled_in_loop = total_filled > initial_total_filled;
5064
5065        if order.time_in_force() == TimeInForce::Ioc && leaves_remaining {
5066            self.cancel_order(order, None);
5067            return Ok(());
5068        }
5069
5070        // `filled_in_loop` covers the just-partially-filled case where the
5071        // local clone's status has not seen the fill events yet.
5072        if leaves_remaining
5073            && (order.is_open() || filled_in_loop)
5074            && self.book_type == BookType::L1_MBP
5075            && matches!(
5076                order.order_type(),
5077                OrderType::Market
5078                    | OrderType::MarketIfTouched
5079                    | OrderType::StopMarket
5080                    | OrderType::TrailingStopMarket
5081            )
5082        {
5083            // Exhausted L1 volume: slip remainder by a single price increment
5084            let Some(last_fill_px) = last_fill_px else {
5085                return Ok(());
5086            };
5087
5088            let side = order.order_side().as_specified();
5089            let slip_fill_px = match side {
5090                OrderSideSpecified::Buy => last_fill_px.add(self.instrument.price_increment()),
5091                OrderSideSpecified::Sell => last_fill_px.sub(self.instrument.price_increment()),
5092            };
5093
5094            if let Some(protection_price) = protection_price {
5095                let exceeds_boundary = match side {
5096                    OrderSideSpecified::Buy => slip_fill_px.raw > protection_price.raw,
5097                    OrderSideSpecified::Sell => slip_fill_px.raw < protection_price.raw,
5098                };
5099
5100                if exceeds_boundary {
5101                    return Ok(());
5102                }
5103            }
5104
5105            let mut leaves_qty = order.quantity().saturating_sub(total_filled);
5106
5107            if let Some(remaining_raw) = reduce_only_remaining_raw {
5108                if remaining_raw == 0 {
5109                    return Ok(());
5110                }
5111
5112                if leaves_qty.raw > remaining_raw {
5113                    leaves_qty = Quantity::from_raw(remaining_raw, leaves_qty.precision);
5114                }
5115
5116                if leaves_qty.raw >= remaining_raw {
5117                    let reduce_only_target_raw = reduce_only_filled_raw
5118                        .unwrap_or(initial_total_filled.raw)
5119                        .checked_add(leaves_qty.raw)
5120                        .expect("Overflow occurred when adding reduce-only target quantity");
5121                    let reduce_only_target =
5122                        Quantity::from_raw(reduce_only_target_raw, order.quantity().precision);
5123
5124                    if order.quantity() != reduce_only_target {
5125                        self.generate_order_updated(order, reduce_only_target, None, None, None);
5126                    }
5127                }
5128            }
5129
5130            if leaves_qty.is_zero() {
5131                return Ok(());
5132            }
5133
5134            self.fill_order(
5135                order,
5136                slip_fill_px,
5137                leaves_qty,
5138                liquidity_side,
5139                venue_position_id,
5140                position,
5141            )?;
5142            self.purge_cached_filled_qty_if_closed(order.client_order_id());
5143        }
5144
5145        Ok(())
5146    }
5147
5148    fn normalize_fill_price(
5149        &self,
5150        fill_px: Price,
5151        client_order_id: ClientOrderId,
5152    ) -> Option<Price> {
5153        let normalized = self.normalize_price_for_current_instrument(fill_px);
5154        if normalized.is_none() {
5155            log::warn!(
5156                "Skipping fill for {client_order_id}: fill price {fill_px} is not compatible \
5157                 with {} price_precision={} price_increment={}",
5158                self.instrument.id(),
5159                self.instrument.price_precision(),
5160                self.instrument.price_increment()
5161            );
5162        }
5163        normalized
5164    }
5165
5166    fn normalize_fill_quantity(
5167        &self,
5168        fill_qty: Quantity,
5169        client_order_id: ClientOrderId,
5170    ) -> Option<Quantity> {
5171        let normalized = self.normalize_quantity_for_current_instrument(fill_qty);
5172        if normalized.is_none() {
5173            log::warn!(
5174                "Skipping fill for {client_order_id}: fill quantity {fill_qty} is not compatible \
5175                 with {} size_precision={}",
5176                self.instrument.id(),
5177                self.instrument.size_precision()
5178            );
5179        }
5180        normalized
5181    }
5182
5183    fn fill_order(
5184        &mut self,
5185        order: &OrderAny,
5186        last_px: Price,
5187        last_qty: Quantity,
5188        liquidity_side: LiquiditySide,
5189        venue_position_id: Option<PositionId>,
5190        _position: Option<&Position>,
5191    ) -> anyhow::Result<()> {
5192        self.check_size_precision(last_qty.precision, "fill quantity")?;
5193
5194        let (last_qty, new_filled_qty) =
5195            if let Some(filled_qty) = self.cached_filled_qty.get(&order.client_order_id()) {
5196                let leaves_qty = order.quantity().saturating_sub(*filled_qty);
5197                let last_qty = min(last_qty, leaves_qty);
5198                (last_qty, *filled_qty + last_qty)
5199            } else {
5200                let last_qty = min(last_qty, order.quantity());
5201                (last_qty, last_qty)
5202            };
5203
5204        if last_qty.is_zero() {
5205            return Ok(());
5206        }
5207
5208        let fee_order;
5209        let commission_order = {
5210            // `order` is a stale pre-fill clone: give fee models the current
5211            // pre-fill `filled_qty` (e.g. `FixedFeeModel` charges once per order).
5212            let mut cloned = order.clone();
5213            write_filled_qty(&mut cloned, new_filled_qty.saturating_sub(last_qty));
5214            if order.liquidity_side() != Some(liquidity_side) {
5215                cloned.set_liquidity_side(liquidity_side);
5216            }
5217            fee_order = cloned;
5218            &fee_order
5219        };
5220
5221        let underlying_px = self.fee_underlying_price()?;
5222        let commission = self.fee_model.get_commission_with_context(
5223            commission_order,
5224            last_qty,
5225            last_px,
5226            &self.instrument,
5227            underlying_px,
5228        )?;
5229
5230        self.cached_filled_qty
5231            .insert(order.client_order_id(), new_filled_qty);
5232
5233        let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
5234        self.generate_order_filled(
5235            order,
5236            venue_order_id,
5237            venue_position_id,
5238            last_qty,
5239            last_px,
5240            self.instrument.quote_currency(),
5241            commission,
5242            liquidity_side,
5243        );
5244
5245        let post_fill_filled_qty = self
5246            .cached_filled_qty
5247            .get(&order.client_order_id())
5248            .copied()
5249            .unwrap_or(order.filled_qty());
5250        let post_fill_leaves_qty = order.quantity().saturating_sub(post_fill_filled_qty);
5251        let fully_filled = post_fill_leaves_qty.is_zero();
5252
5253        if order.is_closed() || fully_filled {
5254            if self.core.order_exists(order.client_order_id()) {
5255                self.delete_core_order(order.client_order_id());
5256            }
5257            // MarketToLimit reads `cached_filled_qty` in its caller to compute leaves;
5258            // its own cleanup happens there after the read.
5259            if order.order_type() != OrderType::MarketToLimit {
5260                self.purge_cached_filled_qty_if_closed(order.client_order_id());
5261            }
5262        }
5263
5264        if !self.config.support_contingent_orders {
5265            return Ok(());
5266        }
5267
5268        if let Some(contingency_type) = order.contingency_type() {
5269            match contingency_type {
5270                ContingencyType::Oto => {
5271                    if let Some(linked_orders_ids) = order.linked_order_ids() {
5272                        for client_order_id in linked_orders_ids {
5273                            let mut child_order = match self.cache.borrow().order(client_order_id) {
5274                                Some(child_order) => child_order.clone(),
5275                                None => anyhow::bail!("Order {client_order_id} not found in cache"),
5276                            };
5277
5278                            if child_order.is_closed() || child_order.is_active_local() {
5279                                continue;
5280                            }
5281
5282                            // Check if we need to index position id
5283                            if let (None, Some(position_id)) =
5284                                (child_order.position_id(), order.position_id())
5285                            {
5286                                self.cache
5287                                    .borrow_mut()
5288                                    .add_position_id(
5289                                        &position_id,
5290                                        &self.venue,
5291                                        client_order_id,
5292                                        &child_order.strategy_id(),
5293                                    )
5294                                    .unwrap();
5295                                log::debug!(
5296                                    "Added position id {position_id} to cache for order {client_order_id}"
5297                                );
5298                            }
5299
5300                            if (!child_order.is_open())
5301                                || (matches!(child_order.status(), OrderStatus::PendingUpdate)
5302                                    && child_order
5303                                        .previous_status()
5304                                        .is_some_and(|s| matches!(s, OrderStatus::Submitted)))
5305                            {
5306                                let account_id = order
5307                                    .account_id()
5308                                    .or_else(|| self.account_ids.get(&order.trader_id()).copied())
5309                                    .ok_or_else(|| {
5310                                        anyhow::anyhow!(
5311                                            "Account ID not found for trader {}",
5312                                            order.trader_id()
5313                                        )
5314                                    })?;
5315                                self.process_order(&mut child_order, account_id);
5316                            }
5317                        }
5318                    } else {
5319                        log::error!(
5320                            "OTO order {} does not have linked orders",
5321                            order.client_order_id()
5322                        );
5323                    }
5324                }
5325                ContingencyType::Oco => {
5326                    if let Some(linked_orders_ids) = order.linked_order_ids() {
5327                        for client_order_id in linked_orders_ids {
5328                            let child_order = match self.cache.borrow().order(client_order_id) {
5329                                Some(child_order) => child_order.clone(),
5330                                None => anyhow::bail!("Order {client_order_id} not found in cache"),
5331                            };
5332
5333                            if child_order.is_closed() || child_order.is_active_local() {
5334                                continue;
5335                            }
5336
5337                            self.cancel_order(&child_order, None);
5338                        }
5339                    } else {
5340                        log::error!(
5341                            "OCO order {} does not have linked orders",
5342                            order.client_order_id()
5343                        );
5344                    }
5345                }
5346                ContingencyType::Ouo => {
5347                    if let Some(linked_orders_ids) = order.linked_order_ids() {
5348                        for client_order_id in linked_orders_ids {
5349                            let child_order = match self.cache.borrow().order(client_order_id) {
5350                                Some(child_order) => child_order.clone(),
5351                                None => anyhow::bail!("Order {client_order_id} not found in cache"),
5352                            };
5353
5354                            if child_order.is_active_local() {
5355                                continue;
5356                            }
5357
5358                            let child_filled_qty = self
5359                                .cached_filled_qty
5360                                .get(&child_order.client_order_id())
5361                                .copied()
5362                                .unwrap_or(child_order.filled_qty());
5363
5364                            if post_fill_leaves_qty.is_zero() && child_order.is_open() {
5365                                self.cancel_order(&child_order, None);
5366                            } else if child_order.is_open()
5367                                && child_filled_qty >= post_fill_leaves_qty
5368                            {
5369                                self.cancel_order(&child_order, Some(false));
5370                            } else if !post_fill_leaves_qty.is_zero()
5371                                && post_fill_leaves_qty != child_order.leaves_qty()
5372                            {
5373                                let price = child_order.price();
5374                                let trigger_price = child_order.trigger_price();
5375                                self.update_order(
5376                                    &child_order,
5377                                    Some(post_fill_leaves_qty),
5378                                    price,
5379                                    trigger_price,
5380                                    Some(false),
5381                                );
5382                            }
5383                        }
5384                    } else {
5385                        log::error!(
5386                            "OUO order {} does not have linked orders",
5387                            order.client_order_id()
5388                        );
5389                    }
5390                }
5391                _ => {}
5392            }
5393        }
5394
5395        Ok(())
5396    }
5397
5398    fn fee_underlying_price(&self) -> CorrectnessResult<Option<Price>> {
5399        if !matches!(
5400            self.instrument,
5401            InstrumentAny::CryptoOption(_) | InstrumentAny::OptionContract(_)
5402        ) {
5403            return Ok(None);
5404        }
5405
5406        let Some(underlying) = self.instrument.underlying() else {
5407            return Ok(None);
5408        };
5409
5410        let underlying_id = InstrumentId::from(format!("{underlying}.{}", self.venue).as_str());
5411        let instrument_id = self.instrument.id();
5412        let cache = self.cache.borrow();
5413        if let Some(price) = cache
5414            .price(&underlying_id, PriceType::Last)
5415            .or_else(|| cache.price(&underlying_id, PriceType::Mark))
5416            .or_else(|| cache.price(&underlying_id, PriceType::Mid))
5417        {
5418            return Ok(Some(price));
5419        }
5420
5421        cache
5422            .option_greeks(&instrument_id)
5423            .and_then(|greeks| greeks.underlying_price)
5424            .map(|price| Price::new_checked(price, FIXED_PRECISION))
5425            .transpose()
5426    }
5427
5428    fn cached_order_is_closed(&self, client_order_id: ClientOrderId) -> bool {
5429        self.cache
5430            .borrow()
5431            .order(&client_order_id)
5432            .is_none_or(|order| order.is_closed())
5433    }
5434
5435    fn purge_cached_filled_qty_if_closed(&mut self, client_order_id: ClientOrderId) {
5436        if self.cached_order_is_closed(client_order_id) {
5437            self.cached_filled_qty.swap_remove(&client_order_id);
5438        }
5439    }
5440
5441    fn purge_closed_cached_filled_qty(&mut self) {
5442        let client_order_ids: Vec<ClientOrderId> = self.cached_filled_qty.keys().copied().collect();
5443
5444        for client_order_id in client_order_ids {
5445            self.purge_cached_filled_qty_if_closed(client_order_id);
5446        }
5447    }
5448
5449    fn update_limit_order(
5450        &mut self,
5451        order: &OrderAny,
5452        quantity: Quantity,
5453        price: Price,
5454    ) -> ModifyOutcome {
5455        if self
5456            .core
5457            .is_limit_matched(order.order_side_specified(), price)
5458        {
5459            if order.is_post_only() {
5460                self.generate_order_modify_rejected(
5461                    order.trader_id(),
5462                    order.strategy_id(),
5463                    order.instrument_id(),
5464                    order.client_order_id(),
5465                    Ustr::from(format!(
5466                        "POST_ONLY {} {} order with new limit px of {} would have been a TAKER: bid={}, ask={}",
5467                        order.order_type(),
5468                        order.order_side(),
5469                        price,
5470                        self.core.bid.map_or_else(|| "None".to_string(), |p| p.to_string()),
5471                        self.core.ask.map_or_else(|| "None".to_string(), |p| p.to_string())
5472                    ).as_str()),
5473                    order.venue_order_id(),
5474                    order.account_id(),
5475                );
5476                return ModifyOutcome::Rejected;
5477            }
5478
5479            self.generate_order_updated(order, quantity, Some(price), None, None);
5480
5481            // Re-read from cache to get the order with events applied
5482            let client_order_id = order.client_order_id();
5483            if let Some(mut order) = self.cache.borrow_mut().order_mut(&client_order_id) {
5484                order.set_liquidity_side(LiquiditySide::Taker);
5485            }
5486            self.fill_limit_order(client_order_id);
5487            return ModifyOutcome::Applied;
5488        }
5489        self.generate_order_updated(order, quantity, Some(price), None, None);
5490        ModifyOutcome::Applied
5491    }
5492
5493    fn update_stop_market_order(
5494        &self,
5495        order: &OrderAny,
5496        quantity: Quantity,
5497        trigger_price: Price,
5498    ) -> ModifyOutcome {
5499        if self.core.is_stop_matched_with_trigger_type(
5500            order.order_side_specified(),
5501            trigger_price,
5502            order.trigger_type().unwrap_or(TriggerType::Default),
5503        ) {
5504            self.generate_order_modify_rejected(
5505                order.trader_id(),
5506                order.strategy_id(),
5507                order.instrument_id(),
5508                order.client_order_id(),
5509                Ustr::from(
5510                    format!(
5511                        "{} {} order new stop px of {} was in the market: bid={}, ask={}",
5512                        order.order_type(),
5513                        order.order_side(),
5514                        trigger_price,
5515                        self.core
5516                            .bid
5517                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
5518                        self.core
5519                            .ask
5520                            .map_or_else(|| "None".to_string(), |p| p.to_string())
5521                    )
5522                    .as_str(),
5523                ),
5524                order.venue_order_id(),
5525                order.account_id(),
5526            );
5527            return ModifyOutcome::Rejected;
5528        }
5529
5530        self.generate_order_updated(order, quantity, None, Some(trigger_price), None);
5531        ModifyOutcome::Applied
5532    }
5533
5534    fn update_stop_limit_order(
5535        &mut self,
5536        order: &OrderAny,
5537        quantity: Quantity,
5538        price: Price,
5539        trigger_price: Price,
5540    ) -> ModifyOutcome {
5541        if order.is_triggered().is_some_and(|t| t) {
5542            // Update limit price
5543            if self
5544                .core
5545                .is_limit_matched(order.order_side_specified(), price)
5546            {
5547                if order.is_post_only() {
5548                    self.generate_order_modify_rejected(
5549                        order.trader_id(),
5550                        order.strategy_id(),
5551                        order.instrument_id(),
5552                        order.client_order_id(),
5553                        Ustr::from(format!(
5554                            "POST_ONLY {} {} order with new limit px of {} would have been a TAKER: bid={}, ask={}",
5555                            order.order_type(),
5556                            order.order_side(),
5557                            price,
5558                            self.core.bid.map_or_else(|| "None".to_string(), |p| p.to_string()),
5559                            self.core.ask.map_or_else(|| "None".to_string(), |p| p.to_string())
5560                        ).as_str()),
5561                        order.venue_order_id(),
5562                        order.account_id(),
5563                    );
5564                    return ModifyOutcome::Rejected;
5565                }
5566                self.generate_order_updated(order, quantity, Some(price), None, None);
5567
5568                // Re-read from cache to get the order with events applied
5569                let client_order_id = order.client_order_id();
5570                if let Some(mut order) = self.cache.borrow_mut().order_mut(&client_order_id) {
5571                    order.set_liquidity_side(LiquiditySide::Taker);
5572                }
5573                self.fill_limit_order(client_order_id);
5574                return ModifyOutcome::Applied;
5575            }
5576        } else {
5577            // Update stop price
5578            if self.core.is_stop_matched_with_trigger_type(
5579                order.order_side_specified(),
5580                trigger_price,
5581                order.trigger_type().unwrap_or(TriggerType::Default),
5582            ) {
5583                self.generate_order_modify_rejected(
5584                    order.trader_id(),
5585                    order.strategy_id(),
5586                    order.instrument_id(),
5587                    order.client_order_id(),
5588                    Ustr::from(
5589                        format!(
5590                            "{} {} order new stop px of {} was in the market: bid={}, ask={}",
5591                            order.order_type(),
5592                            order.order_side(),
5593                            trigger_price,
5594                            self.core
5595                                .bid
5596                                .map_or_else(|| "None".to_string(), |p| p.to_string()),
5597                            self.core
5598                                .ask
5599                                .map_or_else(|| "None".to_string(), |p| p.to_string())
5600                        )
5601                        .as_str(),
5602                    ),
5603                    order.venue_order_id(),
5604                    order.account_id(),
5605                );
5606                return ModifyOutcome::Rejected;
5607            }
5608        }
5609
5610        self.generate_order_updated(order, quantity, Some(price), Some(trigger_price), None);
5611        ModifyOutcome::Applied
5612    }
5613
5614    fn update_market_if_touched_order(
5615        &self,
5616        order: &OrderAny,
5617        quantity: Quantity,
5618        trigger_price: Price,
5619    ) -> ModifyOutcome {
5620        if self.core.is_touch_triggered_with_trigger_type(
5621            order.order_side_specified(),
5622            trigger_price,
5623            order.trigger_type().unwrap_or(TriggerType::Default),
5624        ) {
5625            self.generate_order_modify_rejected(
5626                order.trader_id(),
5627                order.strategy_id(),
5628                order.instrument_id(),
5629                order.client_order_id(),
5630                Ustr::from(
5631                    format!(
5632                        "{} {} order new trigger px of {} was in the market: bid={}, ask={}",
5633                        order.order_type(),
5634                        order.order_side(),
5635                        trigger_price,
5636                        self.core
5637                            .bid
5638                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
5639                        self.core
5640                            .ask
5641                            .map_or_else(|| "None".to_string(), |p| p.to_string())
5642                    )
5643                    .as_str(),
5644                ),
5645                order.venue_order_id(),
5646                order.account_id(),
5647            );
5648            // Cannot update order
5649            return ModifyOutcome::Rejected;
5650        }
5651
5652        self.generate_order_updated(order, quantity, None, Some(trigger_price), None);
5653        ModifyOutcome::Applied
5654    }
5655
5656    fn update_limit_if_touched_order(
5657        &mut self,
5658        order: &OrderAny,
5659        quantity: Quantity,
5660        price: Price,
5661        trigger_price: Price,
5662    ) -> ModifyOutcome {
5663        if order.is_triggered().is_some_and(|t| t) {
5664            // Update limit price
5665            if self
5666                .core
5667                .is_limit_matched(order.order_side_specified(), price)
5668            {
5669                if order.is_post_only() {
5670                    self.generate_order_modify_rejected(
5671                        order.trader_id(),
5672                        order.strategy_id(),
5673                        order.instrument_id(),
5674                        order.client_order_id(),
5675                        Ustr::from(format!(
5676                            "POST_ONLY {} {} order with new limit px of {} would have been a TAKER: bid={}, ask={}",
5677                            order.order_type(),
5678                            order.order_side(),
5679                            price,
5680                            self.core.bid.map_or_else(|| "None".to_string(), |p| p.to_string()),
5681                            self.core.ask.map_or_else(|| "None".to_string(), |p| p.to_string())
5682                        ).as_str()),
5683                        order.venue_order_id(),
5684                        order.account_id(),
5685                    );
5686                    // Cannot update order
5687                    return ModifyOutcome::Rejected;
5688                }
5689                self.generate_order_updated(order, quantity, Some(price), None, None);
5690
5691                // Re-read from cache to get the order with events applied
5692                let client_order_id = order.client_order_id();
5693                if let Some(mut order) = self.cache.borrow_mut().order_mut(&client_order_id) {
5694                    order.set_liquidity_side(LiquiditySide::Taker);
5695                }
5696                self.fill_limit_order(client_order_id);
5697                return ModifyOutcome::Applied;
5698            }
5699        } else {
5700            // Update trigger price
5701            if self.core.is_touch_triggered_with_trigger_type(
5702                order.order_side_specified(),
5703                trigger_price,
5704                order.trigger_type().unwrap_or(TriggerType::Default),
5705            ) {
5706                self.generate_order_modify_rejected(
5707                    order.trader_id(),
5708                    order.strategy_id(),
5709                    order.instrument_id(),
5710                    order.client_order_id(),
5711                    Ustr::from(
5712                        format!(
5713                            "{} {} order new trigger px of {} was in the market: bid={}, ask={}",
5714                            order.order_type(),
5715                            order.order_side(),
5716                            trigger_price,
5717                            self.core
5718                                .bid
5719                                .map_or_else(|| "None".to_string(), |p| p.to_string()),
5720                            self.core
5721                                .ask
5722                                .map_or_else(|| "None".to_string(), |p| p.to_string())
5723                        )
5724                        .as_str(),
5725                    ),
5726                    order.venue_order_id(),
5727                    order.account_id(),
5728                );
5729                return ModifyOutcome::Rejected;
5730            }
5731        }
5732
5733        self.generate_order_updated(order, quantity, Some(price), Some(trigger_price), None);
5734        ModifyOutcome::Applied
5735    }
5736
5737    fn update_trailing_stop_order(&self, order: &OrderAny) {
5738        let (new_trigger_price, new_price) = match trailing_stop_calculate(
5739            self.instrument.price_increment(),
5740            order.trigger_price(),
5741            order,
5742            self.core.bid,
5743            self.core.ask,
5744            self.core.last,
5745        ) {
5746            Ok(prices) => prices,
5747            Err(e) => {
5748                // Missing market data yet: await the next update to compute the trigger.
5749                log::debug!("Cannot calculate trailing-stop update: {e}");
5750                return;
5751            }
5752        };
5753
5754        if new_trigger_price.is_none() && new_price.is_none() {
5755            return;
5756        }
5757
5758        self.generate_order_updated(order, order.quantity(), new_price, new_trigger_price, None);
5759    }
5760
5761    fn accept_order(&mut self, order: &mut OrderAny) {
5762        if order.is_closed() {
5763            // Temporary guard to prevent invalid processing
5764            return;
5765        }
5766
5767        if order.status() != OrderStatus::Accepted {
5768            let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
5769            let event = self.create_order_accepted(order, venue_order_id);
5770            // Apply locally so `cancel_order` sees `Accepted`,
5771            // dispatch on apply failure so `Released` still registers with the core.
5772            if let Err(e) = order.apply(event.clone()) {
5773                log::warn!(
5774                    "Skipping local apply of accepted event for {}: {e}",
5775                    order.client_order_id(),
5776                );
5777            }
5778            self.dispatch_order_event(event);
5779
5780            // Activate before emitting `OrderUpdated` so `match_info` below
5781            // carries the activation flag.
5782            if matches!(
5783                order.order_type(),
5784                OrderType::TrailingStopLimit | OrderType::TrailingStopMarket
5785            ) && order.trigger_price().is_none()
5786                && self.maybe_activate_trailing_stop(
5787                    order,
5788                    self.core.bid,
5789                    self.core.ask,
5790                    self.core.last,
5791                )
5792            {
5793                self.update_trailing_stop_order(order);
5794            }
5795        }
5796
5797        let match_info = Self::matching_core_entry(order);
5798        self.track_post_match_order(order);
5799        self.core.add_order(match_info);
5800    }
5801
5802    fn track_post_match_order(&mut self, order: &OrderAny) {
5803        self.post_match_order_ids.insert(order.client_order_id());
5804    }
5805
5806    fn delete_core_order(&mut self, client_order_id: ClientOrderId) {
5807        self.post_match_order_ids.swap_remove(&client_order_id);
5808        let _ = self.core.delete_order(client_order_id);
5809    }
5810
5811    fn requires_post_match_maintenance(order: &OrderAny) -> bool {
5812        order.expire_time().is_some()
5813            || matches!(
5814                order.order_type(),
5815                OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
5816            )
5817    }
5818
5819    fn matching_core_entry(order: &OrderAny) -> RestingOrder {
5820        let triggered_limit_style = matches!(
5821            order.order_type(),
5822            OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit
5823        ) && order.is_triggered().is_some_and(|triggered| triggered);
5824
5825        RestingOrder::new_with_trigger_type(
5826            order.client_order_id(),
5827            order.order_side().as_specified(),
5828            order.order_type(),
5829            order.trigger_type().unwrap_or(TriggerType::Default),
5830            if triggered_limit_style {
5831                None
5832            } else {
5833                order.trigger_price()
5834            },
5835            order.price(),
5836            match order {
5837                OrderAny::TrailingStopMarket(o) => o.is_activated,
5838                OrderAny::TrailingStopLimit(o) => o.is_activated,
5839                _ => true,
5840            },
5841        )
5842    }
5843
5844    fn expire_order(&mut self, order: &OrderAny) {
5845        if self.config.support_contingent_orders
5846            && order
5847                .contingency_type()
5848                .is_some_and(|c| c != ContingencyType::NoContingency)
5849        {
5850            self.cancel_contingent_orders(order);
5851        }
5852
5853        self.generate_order_expired(order);
5854    }
5855
5856    fn cancel_order(&mut self, order: &OrderAny, cancel_contingencies: Option<bool>) {
5857        let cancel_contingencies = cancel_contingencies.unwrap_or(true);
5858
5859        if order.is_active_local()
5860            && !matches!(
5861                (order.status(), order.order_type(), order.time_in_force()),
5862                (
5863                    OrderStatus::Initialized | OrderStatus::Released,
5864                    OrderType::Market,
5865                    TimeInForce::Ioc | TimeInForce::Fok
5866                )
5867            )
5868        {
5869            log::error!(
5870                "Cannot cancel an order with {} from the matching engine",
5871                order.status()
5872            );
5873            return;
5874        }
5875
5876        // Check if order exists in OrderMatching core, and delete it if it does
5877        if self.core.order_exists(order.client_order_id()) {
5878            self.delete_core_order(order.client_order_id());
5879        }
5880        self.cached_filled_qty.swap_remove(&order.client_order_id());
5881
5882        let venue_order_id = self.ids_generator.get_venue_order_id(order).unwrap();
5883        self.generate_order_canceled(order, venue_order_id);
5884
5885        if self.config.support_contingent_orders
5886            && order.contingency_type().is_some()
5887            && order.contingency_type().unwrap() != ContingencyType::NoContingency
5888            && cancel_contingencies
5889        {
5890            self.cancel_contingent_orders(order);
5891        }
5892    }
5893
5894    fn update_order(
5895        &mut self,
5896        order: &OrderAny,
5897        quantity: Option<Quantity>,
5898        price: Option<Price>,
5899        trigger_price: Option<Price>,
5900        update_contingencies: Option<bool>,
5901    ) -> bool {
5902        let update_contingencies = update_contingencies.unwrap_or(true);
5903        let quantity = quantity.unwrap_or(order.quantity());
5904
5905        let price_prec = self.instrument.price_precision();
5906        let size_prec = self.instrument.size_precision();
5907        let instrument_id = self.instrument.id();
5908
5909        if quantity.precision != size_prec {
5910            self.generate_order_modify_rejected(
5911                order.trader_id(),
5912                order.strategy_id(),
5913                order.instrument_id(),
5914                order.client_order_id(),
5915                Ustr::from(&format!(
5916                    "Invalid update quantity precision {}, expected {size_prec} for {instrument_id}",
5917                    quantity.precision
5918                )),
5919                order.venue_order_id(),
5920                order.account_id(),
5921            );
5922            return false;
5923        }
5924
5925        if let Some(px) = price
5926            && px.precision != price_prec
5927        {
5928            self.generate_order_modify_rejected(
5929                order.trader_id(),
5930                order.strategy_id(),
5931                order.instrument_id(),
5932                order.client_order_id(),
5933                Ustr::from(&format!(
5934                    "Invalid update price precision {}, expected {price_prec} for {instrument_id}",
5935                    px.precision
5936                )),
5937                order.venue_order_id(),
5938                order.account_id(),
5939            );
5940            return false;
5941        }
5942
5943        if let Some(tp) = trigger_price
5944            && tp.precision != price_prec
5945        {
5946            self.generate_order_modify_rejected(
5947                order.trader_id(),
5948                order.strategy_id(),
5949                order.instrument_id(),
5950                order.client_order_id(),
5951                Ustr::from(&format!(
5952                    "Invalid update trigger_price precision {}, expected {price_prec} for {instrument_id}",
5953                    tp.precision
5954                )),
5955                order.venue_order_id(),
5956                order.account_id(),
5957            );
5958            return false;
5959        }
5960
5961        // Use cached_filled_qty since PassiveOrderAny in core is not updated with fills
5962        let filled_qty = self
5963            .cached_filled_qty
5964            .get(&order.client_order_id())
5965            .copied()
5966            .unwrap_or(order.filled_qty());
5967        if quantity < filled_qty {
5968            self.generate_order_modify_rejected(
5969                order.trader_id(),
5970                order.strategy_id(),
5971                order.instrument_id(),
5972                order.client_order_id(),
5973                Ustr::from(&format!(
5974                    "Cannot reduce order quantity {quantity} below filled quantity {filled_qty}",
5975                )),
5976                order.venue_order_id(),
5977                order.account_id(),
5978            );
5979            return false;
5980        }
5981
5982        let outcome = match order {
5983            OrderAny::Limit(_) | OrderAny::MarketToLimit(_) => {
5984                let price = price.unwrap_or(order.price().unwrap());
5985                self.update_limit_order(order, quantity, price)
5986            }
5987            OrderAny::StopMarket(_) => {
5988                let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
5989                self.update_stop_market_order(order, quantity, trigger_price)
5990            }
5991            OrderAny::StopLimit(_) => {
5992                let price = price.unwrap_or(order.price().unwrap());
5993                let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
5994                self.update_stop_limit_order(order, quantity, price, trigger_price)
5995            }
5996            OrderAny::MarketIfTouched(_) => {
5997                let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
5998                self.update_market_if_touched_order(order, quantity, trigger_price)
5999            }
6000            OrderAny::LimitIfTouched(_) => {
6001                let price = price.unwrap_or(order.price().unwrap());
6002                let trigger_price = trigger_price.unwrap_or(order.trigger_price().unwrap());
6003                self.update_limit_if_touched_order(order, quantity, price, trigger_price)
6004            }
6005            OrderAny::TrailingStopMarket(_) => {
6006                if let Some(trigger_price) = trigger_price.or(order.trigger_price()) {
6007                    self.update_market_if_touched_order(order, quantity, trigger_price)
6008                } else {
6009                    self.generate_order_updated(order, quantity, None, trigger_price, None);
6010                    ModifyOutcome::Applied
6011                }
6012            }
6013            OrderAny::TrailingStopLimit(_) => {
6014                match (
6015                    price.or(order.price()),
6016                    trigger_price.or(order.trigger_price()),
6017                ) {
6018                    (Some(price), Some(trigger_price)) => {
6019                        self.update_limit_if_touched_order(order, quantity, price, trigger_price)
6020                    }
6021                    _ => {
6022                        self.generate_order_updated(order, quantity, price, trigger_price, None);
6023                        ModifyOutcome::Applied
6024                    }
6025                }
6026            }
6027            _ => {
6028                panic!(
6029                    "Unsupported order type {} for update_order",
6030                    order.order_type()
6031                );
6032            }
6033        };
6034
6035        if outcome == ModifyOutcome::Rejected {
6036            return false;
6037        }
6038
6039        // If order now has zero leaves after update, cancel it
6040        let new_leaves_qty = quantity.saturating_sub(filled_qty);
6041        if new_leaves_qty.is_zero() {
6042            if self.config.support_contingent_orders
6043                && order
6044                    .contingency_type()
6045                    .is_some_and(|c| c != ContingencyType::NoContingency)
6046                && update_contingencies
6047            {
6048                self.update_contingent_order(order, quantity);
6049            }
6050            // Pass false since we already handled contingents above
6051            self.cancel_order(order, Some(false));
6052            return true;
6053        }
6054
6055        if self.config.support_contingent_orders
6056            && order
6057                .contingency_type()
6058                .is_some_and(|c| c != ContingencyType::NoContingency)
6059            && update_contingencies
6060        {
6061            self.update_contingent_order(order, quantity);
6062        }
6063
6064        true
6065    }
6066
6067    /// Triggers a stop order, converting it to an active market or limit order.
6068    pub fn trigger_stop_order(&mut self, client_order_id: ClientOrderId) {
6069        let order = match self
6070            .cache
6071            .borrow()
6072            .order(&client_order_id)
6073            .map(|o| o.clone())
6074        {
6075            Some(order) => order,
6076            None => {
6077                log::error!(
6078                    "Cannot trigger stop order: order {client_order_id} not found in cache"
6079                );
6080                return;
6081            }
6082        };
6083
6084        if order.is_closed() {
6085            log::debug!("Cannot trigger stop order: {client_order_id} already closed");
6086            return;
6087        }
6088
6089        match order.order_type() {
6090            OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit => {
6091                self.trigger_limit_style_stop_order(client_order_id, order);
6092            }
6093            OrderType::StopMarket | OrderType::MarketIfTouched | OrderType::TrailingStopMarket => {
6094                self.fill_market_order(client_order_id);
6095            }
6096            _ => {
6097                log::error!(
6098                    "Cannot trigger stop order: invalid order type {}",
6099                    order.order_type()
6100                );
6101            }
6102        }
6103    }
6104
6105    fn trigger_limit_style_stop_order(&mut self, client_order_id: ClientOrderId, order: OrderAny) {
6106        if order.is_triggered().is_some_and(|triggered| triggered) {
6107            let liquidity_side = match (order.price(), order.trigger_price()) {
6108                (Some(price), Some(trigger_price)) => Self::determine_triggered_limit_liquidity(
6109                    order.order_side(),
6110                    price,
6111                    trigger_price,
6112                ),
6113                _ => LiquiditySide::Maker,
6114            };
6115
6116            if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id)
6117                && !matches!(
6118                    cached_order.liquidity_side(),
6119                    Some(LiquiditySide::Maker | LiquiditySide::Taker)
6120                )
6121            {
6122                cached_order.set_liquidity_side(liquidity_side);
6123            }
6124            self.fill_limit_order(client_order_id);
6125            return;
6126        }
6127
6128        let event = self.create_order_triggered(&order);
6129        let order = match self.cache.borrow_mut().update_order(&event) {
6130            Ok(order) => order,
6131            Err(e) => {
6132                log::debug!(
6133                    "Failed to apply triggered event for {} before fill: {e}",
6134                    order.client_order_id(),
6135                );
6136                order
6137            }
6138        };
6139        self.dispatch_order_event(event);
6140
6141        let trigger_price = order
6142            .trigger_price()
6143            .expect("Limit-style stop order must have a trigger price");
6144        let price = order
6145            .price()
6146            .expect("Limit-style stop order must have a price");
6147
6148        let maker_inside = match order.order_side() {
6149            OrderSide::Buy => self
6150                .core
6151                .ask
6152                .is_some_and(|ask| trigger_price > price && price > ask),
6153            OrderSide::Sell => self
6154                .core
6155                .bid
6156                .is_some_and(|bid| trigger_price < price && price < bid),
6157            OrderSide::NoOrderSide => false,
6158        };
6159
6160        if maker_inside {
6161            if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6162                cached_order.set_liquidity_side(LiquiditySide::Maker);
6163            }
6164            self.resync_core_entry(client_order_id);
6165            self.fill_limit_order(client_order_id);
6166            return;
6167        }
6168
6169        if self
6170            .core
6171            .is_limit_matched(order.order_side_specified(), price)
6172        {
6173            if order.is_post_only() {
6174                self.delete_core_order(client_order_id);
6175                self.cached_filled_qty.swap_remove(&client_order_id);
6176                let event = self.create_order_rejected(
6177                    &order,
6178                    format!(
6179                        "POST_ONLY {} {} order limit px of {} would have been a TAKER: bid={}, ask={}",
6180                        order.order_type(),
6181                        order.order_side(),
6182                        price,
6183                        self.core
6184                            .bid
6185                            .map_or_else(|| "None".to_string(), |p| p.to_string()),
6186                        self.core
6187                            .ask
6188                            .map_or_else(|| "None".to_string(), |p| p.to_string())
6189                    )
6190                    .into(),
6191                );
6192
6193                if let Err(e) = self.cache.borrow_mut().update_order(&event) {
6194                    log::debug!(
6195                        "Failed to apply rejected event for {} after post-only trigger: {e}",
6196                        order.client_order_id(),
6197                    );
6198                }
6199                self.dispatch_order_event(event);
6200                return;
6201            }
6202
6203            if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6204                cached_order.set_liquidity_side(LiquiditySide::Taker);
6205            }
6206            self.resync_core_entry(client_order_id);
6207            self.fill_limit_order(client_order_id);
6208            return;
6209        }
6210
6211        if let Some(mut cached_order) = self.cache.borrow_mut().order_mut(&client_order_id) {
6212            cached_order.set_liquidity_side(Self::determine_triggered_limit_liquidity(
6213                order.order_side(),
6214                price,
6215                trigger_price,
6216            ));
6217        }
6218        self.resync_core_entry(client_order_id);
6219    }
6220
6221    fn determine_triggered_limit_liquidity(
6222        side: OrderSide,
6223        price: Price,
6224        trigger_price: Price,
6225    ) -> LiquiditySide {
6226        if (side == OrderSide::Buy && trigger_price > price)
6227            || (side == OrderSide::Sell && trigger_price < price)
6228        {
6229            LiquiditySide::Maker
6230        } else {
6231            LiquiditySide::Taker
6232        }
6233    }
6234
6235    fn update_contingent_order(&mut self, order: &OrderAny, parent_quantity: Quantity) {
6236        log::debug!(
6237            "Updating contingent orders from {}",
6238            order.client_order_id()
6239        );
6240
6241        if let Some(linked_order_ids) = order.linked_order_ids() {
6242            let parent_filled_qty = self
6243                .cached_filled_qty
6244                .get(&order.client_order_id())
6245                .copied()
6246                .unwrap_or(order.filled_qty());
6247            let parent_leaves_qty = parent_quantity.saturating_sub(parent_filled_qty);
6248
6249            for client_order_id in linked_order_ids {
6250                let child_order = match self.cache.borrow().order(client_order_id) {
6251                    Some(order) => order.clone(),
6252                    None => panic!("Order {client_order_id} not found in cache."),
6253                };
6254
6255                if child_order.is_active_local() {
6256                    continue;
6257                }
6258
6259                let child_filled_qty = self
6260                    .cached_filled_qty
6261                    .get(&child_order.client_order_id())
6262                    .copied()
6263                    .unwrap_or(child_order.filled_qty());
6264
6265                if parent_leaves_qty.is_zero() {
6266                    self.cancel_order(&child_order, Some(false));
6267                } else if child_filled_qty >= parent_leaves_qty {
6268                    // Child already filled beyond parent's remaining qty, cancel it
6269                    self.cancel_order(&child_order, Some(false));
6270                } else {
6271                    let child_leaves_qty = child_order.quantity().saturating_sub(child_filled_qty);
6272                    if child_leaves_qty != parent_leaves_qty {
6273                        let price = child_order.price();
6274                        let trigger_price = child_order.trigger_price();
6275                        self.update_order(
6276                            &child_order,
6277                            Some(parent_leaves_qty),
6278                            price,
6279                            trigger_price,
6280                            Some(false),
6281                        );
6282                    }
6283                }
6284            }
6285        }
6286    }
6287
6288    fn cancel_contingent_orders(&mut self, order: &OrderAny) {
6289        if let Some(linked_order_ids) = order.linked_order_ids() {
6290            for client_order_id in linked_order_ids {
6291                let contingent_order = match self.cache.borrow().order(client_order_id) {
6292                    Some(order) => order.clone(),
6293                    None => panic!("Cannot find contingent order for {client_order_id}"),
6294                };
6295
6296                if contingent_order.is_active_local() {
6297                    // order is not on the exchange yet
6298                    continue;
6299                }
6300
6301                if !contingent_order.is_closed() {
6302                    self.cancel_order(&contingent_order, Some(false));
6303                }
6304            }
6305        }
6306    }
6307
6308    fn generate_order_submitted(&self, order: &OrderAny, account_id: AccountId) {
6309        let ts_now = self.clock.borrow().timestamp_ns();
6310        let event = OrderEventAny::Submitted(OrderSubmitted::new(
6311            order.trader_id(),
6312            order.strategy_id(),
6313            order.instrument_id(),
6314            order.client_order_id(),
6315            account_id,
6316            UUID4::new(),
6317            ts_now,
6318            ts_now,
6319        ));
6320        self.dispatch_order_event(event);
6321    }
6322
6323    fn create_order_rejected(&self, order: &OrderAny, reason: Ustr) -> OrderEventAny {
6324        let ts_now = self.clock.borrow().timestamp_ns();
6325        let account_id = order
6326            .account_id()
6327            .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
6328
6329        let due_post_only = reason.as_str().starts_with("POST_ONLY");
6330
6331        OrderEventAny::Rejected(OrderRejected::new(
6332            order.trader_id(),
6333            order.strategy_id(),
6334            order.instrument_id(),
6335            order.client_order_id(),
6336            account_id,
6337            reason,
6338            UUID4::new(),
6339            ts_now,
6340            ts_now,
6341            false,
6342            due_post_only,
6343        ))
6344    }
6345
6346    fn generate_order_rejected(&self, order: &OrderAny, reason: Ustr) {
6347        let event = self.create_order_rejected(order, reason);
6348        self.dispatch_order_event(event);
6349    }
6350
6351    fn publish_order_initialized(&self, order: &OrderAny) {
6352        let event = OrderEventAny::Initialized(order.init_event().clone());
6353        msgbus::publish_order_event(
6354            format!("events.order.{}", order.strategy_id()).into(),
6355            &event,
6356        );
6357    }
6358
6359    fn create_order_accepted(
6360        &self,
6361        order: &OrderAny,
6362        venue_order_id: VenueOrderId,
6363    ) -> OrderEventAny {
6364        let ts_now = self.clock.borrow().timestamp_ns();
6365        let account_id = order
6366            .account_id()
6367            .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
6368        OrderEventAny::Accepted(OrderAccepted::new(
6369            order.trader_id(),
6370            order.strategy_id(),
6371            order.instrument_id(),
6372            order.client_order_id(),
6373            venue_order_id,
6374            account_id,
6375            UUID4::new(),
6376            ts_now,
6377            ts_now,
6378            false,
6379        ))
6380    }
6381
6382    fn generate_order_accepted(&self, order: &OrderAny, venue_order_id: VenueOrderId) {
6383        let event = self.create_order_accepted(order, venue_order_id);
6384        self.dispatch_order_event(event);
6385    }
6386
6387    #[expect(clippy::too_many_arguments)]
6388    fn generate_order_modify_rejected(
6389        &self,
6390        trader_id: TraderId,
6391        strategy_id: StrategyId,
6392        instrument_id: InstrumentId,
6393        client_order_id: ClientOrderId,
6394        reason: Ustr,
6395        venue_order_id: Option<VenueOrderId>,
6396        account_id: Option<AccountId>,
6397    ) {
6398        let ts_now = self.clock.borrow().timestamp_ns();
6399        let event = OrderEventAny::ModifyRejected(OrderModifyRejected::new(
6400            trader_id,
6401            strategy_id,
6402            instrument_id,
6403            client_order_id,
6404            reason,
6405            UUID4::new(),
6406            ts_now,
6407            ts_now,
6408            false,
6409            venue_order_id,
6410            account_id,
6411        ));
6412        self.dispatch_order_event(event);
6413    }
6414
6415    #[expect(clippy::too_many_arguments)]
6416    fn generate_order_cancel_rejected(
6417        &self,
6418        trader_id: TraderId,
6419        strategy_id: StrategyId,
6420        account_id: AccountId,
6421        instrument_id: InstrumentId,
6422        client_order_id: ClientOrderId,
6423        venue_order_id: Option<VenueOrderId>,
6424        reason: Ustr,
6425    ) {
6426        let ts_now = self.clock.borrow().timestamp_ns();
6427        let event = OrderEventAny::CancelRejected(OrderCancelRejected::new(
6428            trader_id,
6429            strategy_id,
6430            instrument_id,
6431            client_order_id,
6432            reason,
6433            UUID4::new(),
6434            ts_now,
6435            ts_now,
6436            false,
6437            venue_order_id,
6438            Some(account_id),
6439        ));
6440        self.dispatch_order_event(event);
6441    }
6442
6443    fn generate_order_updated(
6444        &self,
6445        order: &OrderAny,
6446        quantity: Quantity,
6447        price: Option<Price>,
6448        trigger_price: Option<Price>,
6449        protection_price: Option<Price>,
6450    ) {
6451        let ts_now = self.clock.borrow().timestamp_ns();
6452        let event = OrderEventAny::Updated(OrderUpdated::new(
6453            order.trader_id(),
6454            order.strategy_id(),
6455            order.instrument_id(),
6456            order.client_order_id(),
6457            quantity,
6458            UUID4::new(),
6459            ts_now,
6460            ts_now,
6461            false,
6462            order.venue_order_id(),
6463            order.account_id(),
6464            price,
6465            trigger_price,
6466            protection_price,
6467            order.is_quote_quantity(),
6468        ));
6469
6470        self.dispatch_order_event(event);
6471    }
6472
6473    fn generate_order_canceled(&self, order: &OrderAny, venue_order_id: VenueOrderId) {
6474        let ts_now = self.clock.borrow().timestamp_ns();
6475        let event = OrderEventAny::Canceled(OrderCanceled::new(
6476            order.trader_id(),
6477            order.strategy_id(),
6478            order.instrument_id(),
6479            order.client_order_id(),
6480            UUID4::new(),
6481            ts_now,
6482            ts_now,
6483            false,
6484            Some(venue_order_id),
6485            order.account_id(),
6486        ));
6487        self.dispatch_order_event(event);
6488    }
6489
6490    fn create_order_triggered(&self, order: &OrderAny) -> OrderEventAny {
6491        let ts_now = self.clock.borrow().timestamp_ns();
6492        OrderEventAny::Triggered(OrderTriggered::new(
6493            order.trader_id(),
6494            order.strategy_id(),
6495            order.instrument_id(),
6496            order.client_order_id(),
6497            UUID4::new(),
6498            ts_now,
6499            ts_now,
6500            false,
6501            order.venue_order_id(),
6502            order.account_id(),
6503        ))
6504    }
6505
6506    fn generate_order_expired(&self, order: &OrderAny) {
6507        let ts_now = self.clock.borrow().timestamp_ns();
6508        let event = OrderEventAny::Expired(OrderExpired::new(
6509            order.trader_id(),
6510            order.strategy_id(),
6511            order.instrument_id(),
6512            order.client_order_id(),
6513            UUID4::new(),
6514            ts_now,
6515            ts_now,
6516            false,
6517            order.venue_order_id(),
6518            order.account_id(),
6519        ));
6520        self.dispatch_order_event(event);
6521    }
6522
6523    #[expect(clippy::too_many_arguments)]
6524    fn generate_order_filled(
6525        &mut self,
6526        order: &OrderAny,
6527        venue_order_id: VenueOrderId,
6528        venue_position_id: Option<PositionId>,
6529        last_qty: Quantity,
6530        last_px: Price,
6531        quote_currency: Currency,
6532        commission: Money,
6533        liquidity_side: LiquiditySide,
6534    ) {
6535        debug_assert!(
6536            last_qty <= order.quantity(),
6537            "Fill quantity {last_qty} exceeds order quantity {order_qty} for {client_order_id}",
6538            order_qty = order.quantity(),
6539            client_order_id = order.client_order_id()
6540        );
6541
6542        let ts_now = self.clock.borrow().timestamp_ns();
6543        let account_id = order
6544            .account_id()
6545            .unwrap_or(self.account_ids.get(&order.trader_id()).unwrap().to_owned());
6546        let event = OrderEventAny::Filled(OrderFilled::new(
6547            order.trader_id(),
6548            order.strategy_id(),
6549            order.instrument_id(),
6550            order.client_order_id(),
6551            venue_order_id,
6552            account_id,
6553            self.ids_generator.generate_trade_id(ts_now),
6554            order.order_side(),
6555            order.order_type(),
6556            last_qty,
6557            last_px,
6558            quote_currency,
6559            liquidity_side,
6560            UUID4::new(),
6561            ts_now,
6562            ts_now,
6563            false,
6564            venue_position_id,
6565            Some(commission),
6566            None,
6567        ));
6568
6569        self.dispatch_order_event(event);
6570    }
6571}
6572
6573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6574enum ModifyOutcome {
6575    Applied,
6576    Rejected,
6577}
6578
6579#[derive(Debug)]
6580enum PostMatchOrderAction {
6581    RemoveClosed,
6582    Expire(OrderAny),
6583    UpdateTrailing(OrderAny),
6584    NoMaintenance,
6585}
6586
6587fn post_match_order_action<F>(
6588    order: &OrderAny,
6589    support_gtd_orders: bool,
6590    timestamp_ns: UnixNanos,
6591    clone_order: F,
6592) -> PostMatchOrderAction
6593where
6594    F: FnOnce(&OrderAny) -> OrderAny,
6595{
6596    if order.is_closed() {
6597        PostMatchOrderAction::RemoveClosed
6598    } else if support_gtd_orders
6599        && order
6600            .expire_time()
6601            .is_some_and(|expire_ns| timestamp_ns >= expire_ns)
6602    {
6603        PostMatchOrderAction::Expire(clone_order(order))
6604    } else if matches!(
6605        order.order_type(),
6606        OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
6607    ) {
6608        PostMatchOrderAction::UpdateTrailing(clone_order(order))
6609    } else {
6610        PostMatchOrderAction::NoMaintenance
6611    }
6612}
6613
6614/// Writes `filled_qty` directly onto an order clone's core state.
6615///
6616/// Used to present fee models with the current pre-fill quantity when the
6617/// order passed to the fill path is a stale clone (see `fill_order`).
6618fn write_filled_qty(order: &mut OrderAny, filled_qty: Quantity) {
6619    match order {
6620        OrderAny::Limit(o) => o.filled_qty = filled_qty,
6621        OrderAny::LimitIfTouched(o) => o.filled_qty = filled_qty,
6622        OrderAny::Market(o) => o.filled_qty = filled_qty,
6623        OrderAny::MarketIfTouched(o) => o.filled_qty = filled_qty,
6624        OrderAny::MarketToLimit(o) => o.filled_qty = filled_qty,
6625        OrderAny::StopLimit(o) => o.filled_qty = filled_qty,
6626        OrderAny::StopMarket(o) => o.filled_qty = filled_qty,
6627        OrderAny::TrailingStopLimit(o) => o.filled_qty = filled_qty,
6628        OrderAny::TrailingStopMarket(o) => o.filled_qty = filled_qty,
6629    }
6630}
6631
6632#[derive(Debug, Clone, Copy)]
6633struct BarTickSizes {
6634    open: Quantity,
6635    high: Quantity,
6636    low: Quantity,
6637    close: Quantity,
6638}
6639
6640impl BarTickSizes {
6641    fn from_volume(volume: Quantity, size_increment: Quantity) -> Self {
6642        let precision_diff = FIXED_PRECISION.saturating_sub(volume.precision);
6643        let scale = QuantityRaw::pow(10, u32::from(precision_diff));
6644        let units = volume.raw / scale;
6645        let increment_units = (size_increment.raw / scale).max(1);
6646        let rounded_units = (units / increment_units) * increment_units;
6647        let increments = rounded_units / increment_units;
6648        let zero = Quantity::zero(volume.precision);
6649        let size =
6650            |increments| Quantity::from_raw(increments * increment_units * scale, volume.precision);
6651
6652        match increments {
6653            0 => Self {
6654                open: zero,
6655                high: zero,
6656                low: zero,
6657                close: zero,
6658            },
6659            // One increment cannot cover both high and low without exceeding the bar volume.
6660            1 => Self {
6661                open: zero,
6662                high: zero,
6663                low: zero,
6664                close: size(1),
6665            },
6666            2 => Self {
6667                open: zero,
6668                high: size(1),
6669                low: size(1),
6670                close: zero,
6671            },
6672            3 => {
6673                let path_size = size(1);
6674
6675                Self {
6676                    open: path_size,
6677                    high: path_size,
6678                    low: path_size,
6679                    close: zero,
6680                }
6681            }
6682            _ => {
6683                let path_increments = increments / 4;
6684                let close_increments = increments - (path_increments * 3);
6685                let path_size = size(path_increments);
6686
6687                Self {
6688                    open: path_size,
6689                    high: path_size,
6690                    low: path_size,
6691                    close: size(close_increments),
6692                }
6693            }
6694        }
6695    }
6696}
6697
6698#[cfg(test)]
6699mod tests {
6700    use std::{
6701        cell::{Cell, RefCell},
6702        collections::{HashMap, HashSet},
6703        rc::Rc,
6704    };
6705
6706    use nautilus_common::{cache::Cache, clock::TestClock};
6707    use nautilus_core::{UnixNanos, correctness::CorrectnessError};
6708    use nautilus_model::{
6709        data::{
6710            DEPTH10_LEN, OrderBookDelta, OrderBookDepth10, QuoteTick, TradeTick,
6711            option_chain::OptionGreeks,
6712            order::{BookOrder, OrderId},
6713        },
6714        enums::{
6715            AccountType, AggressorSide, BookAction, BookType, LiquiditySide, OmsType, OrderSide,
6716            OrderType, RecordFlag, TimeInForce, TrailingOffsetType, TriggerType,
6717        },
6718        events::OrderEventAny,
6719        identifiers::{AccountId, ClientOrderId, TradeId, VenueOrderId},
6720        instruments::{
6721            Instrument, InstrumentAny,
6722            stubs::{crypto_option_btc_deribit, crypto_perpetual_ethusdt, futures_contract_es},
6723        },
6724        orderbook::OrderBook,
6725        orders::{Order, OrderAny, OrderTestBuilder, stubs::TestOrderEventStubs},
6726        types::{Money, Price, Quantity, fixed::FIXED_PRECISION, quantity::QuantityRaw},
6727    };
6728    use proptest::prelude::*;
6729    use rstest::rstest;
6730    use rust_decimal::Decimal;
6731
6732    use super::{BarTickSizes, OrderMatchingEngine, PostMatchOrderAction, post_match_order_action};
6733    use crate::{
6734        matching_engine::config::OrderMatchingEngineConfig,
6735        models::{
6736            fee::{FeeModel, FeeModelAny, FeeModelHandle},
6737            fill::{FillModel, FillModelHandle},
6738        },
6739    };
6740
6741    fn assert_valid_bar_tick_sizes(volume: Quantity, size_increment: Quantity) {
6742        let sizes = BarTickSizes::from_volume(volume, size_increment);
6743        let total_raw = sizes.open.raw + sizes.high.raw + sizes.low.raw + sizes.close.raw;
6744        assert!(total_raw <= volume.raw);
6745
6746        for quantity in [sizes.open, sizes.high, sizes.low, sizes.close] {
6747            assert_eq!(quantity.precision, volume.precision);
6748            assert!(
6749                OrderMatchingEngine::quantity_matches_precision(quantity, volume.precision),
6750                "bar tick quantity {quantity} not aligned to precision {}",
6751                volume.precision,
6752            );
6753            assert!(
6754                size_increment.raw == 0 || quantity.raw.is_multiple_of(size_increment.raw),
6755                "bar tick quantity {quantity} not aligned to increment {size_increment}",
6756            );
6757        }
6758
6759        if size_increment.raw > 0 {
6760            assert!(
6761                volume.raw - total_raw < size_increment.raw,
6762                "bar tick split left {} raw units from volume {volume} and increment {size_increment}",
6763                volume.raw - total_raw,
6764            );
6765        }
6766    }
6767
6768    #[rstest]
6769    fn test_post_match_order_action_does_not_clone_no_maintenance_order() {
6770        let order = post_match_limit_order();
6771        let clone_count = Cell::new(0);
6772
6773        let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
6774            clone_count.set(clone_count.get() + 1);
6775            order.clone()
6776        });
6777
6778        assert!(matches!(action, PostMatchOrderAction::NoMaintenance));
6779        assert_eq!(clone_count.get(), 0);
6780    }
6781
6782    #[rstest]
6783    fn test_post_match_order_action_does_not_clone_closed_order() {
6784        let order = post_match_closed_limit_order();
6785        let clone_count = Cell::new(0);
6786
6787        let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
6788            clone_count.set(clone_count.get() + 1);
6789            order.clone()
6790        });
6791
6792        assert!(matches!(action, PostMatchOrderAction::RemoveClosed));
6793        assert_eq!(clone_count.get(), 0);
6794    }
6795
6796    #[rstest]
6797    fn test_post_match_order_action_clones_expired_gtd_order_once() {
6798        let order = post_match_gtd_limit_order();
6799        let clone_count = Cell::new(0);
6800
6801        let action = post_match_order_action(&order, true, UnixNanos::from(10_u64), |order| {
6802            clone_count.set(clone_count.get() + 1);
6803            order.clone()
6804        });
6805
6806        let PostMatchOrderAction::Expire(cloned) = action else {
6807            panic!("Expected expired action, was {action:?}");
6808        };
6809        assert_eq!(cloned.client_order_id(), order.client_order_id());
6810        assert_eq!(clone_count.get(), 1);
6811    }
6812
6813    #[rstest]
6814    fn test_post_match_order_action_clones_trailing_order_once() {
6815        let order = post_match_trailing_stop_order();
6816        let clone_count = Cell::new(0);
6817
6818        let action = post_match_order_action(&order, true, UnixNanos::from(1_u64), |order| {
6819            clone_count.set(clone_count.get() + 1);
6820            order.clone()
6821        });
6822
6823        let PostMatchOrderAction::UpdateTrailing(cloned) = action else {
6824            panic!("Expected trailing update action, was {action:?}");
6825        };
6826        assert_eq!(cloned.client_order_id(), order.client_order_id());
6827        assert_eq!(clone_count.get(), 1);
6828    }
6829
6830    fn post_match_limit_order() -> OrderAny {
6831        OrderTestBuilder::new(OrderType::Limit)
6832            .instrument_id(crypto_perpetual_ethusdt().id())
6833            .side(OrderSide::Buy)
6834            .price(Price::from("1500.00"))
6835            .quantity(Quantity::from("1.000"))
6836            .client_order_id(ClientOrderId::from("POST-MATCH-LIMIT"))
6837            .submit(true)
6838            .build()
6839    }
6840
6841    fn post_match_closed_limit_order() -> OrderAny {
6842        let account_id = AccountId::from("SIM-001");
6843        let venue_order_id = VenueOrderId::from("V-001");
6844        let mut order = post_match_limit_order();
6845        order
6846            .apply(TestOrderEventStubs::accepted(
6847                &order,
6848                account_id,
6849                venue_order_id,
6850            ))
6851            .unwrap();
6852        order
6853            .apply(TestOrderEventStubs::canceled(
6854                &order,
6855                account_id,
6856                Some(venue_order_id),
6857            ))
6858            .unwrap();
6859        order
6860    }
6861
6862    fn post_match_gtd_limit_order() -> OrderAny {
6863        OrderTestBuilder::new(OrderType::Limit)
6864            .instrument_id(crypto_perpetual_ethusdt().id())
6865            .side(OrderSide::Buy)
6866            .price(Price::from("1500.00"))
6867            .quantity(Quantity::from("1.000"))
6868            .time_in_force(TimeInForce::Gtd)
6869            .expire_time(UnixNanos::from(10_u64))
6870            .client_order_id(ClientOrderId::from("POST-MATCH-GTD"))
6871            .submit(true)
6872            .build()
6873    }
6874
6875    fn post_match_trailing_stop_order() -> OrderAny {
6876        OrderTestBuilder::new(OrderType::TrailingStopMarket)
6877            .instrument_id(crypto_perpetual_ethusdt().id())
6878            .side(OrderSide::Buy)
6879            .quantity(Quantity::from("1.000"))
6880            .trigger_price(Price::from("1510.00"))
6881            .trigger_type(TriggerType::BidAsk)
6882            .trailing_offset(Decimal::new(5, 0))
6883            .trailing_offset_type(TrailingOffsetType::Price)
6884            .client_order_id(ClientOrderId::from("POST-MATCH-TRAIL"))
6885            .submit(true)
6886            .build()
6887    }
6888
6889    #[rstest]
6890    fn test_fill_order_calculates_commission_from_fill_liquidity_side() {
6891        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
6892        let cache = Rc::new(RefCell::new(Cache::default()));
6893        let clock = Rc::new(RefCell::new(TestClock::new()));
6894        let mut engine = OrderMatchingEngine::new(
6895            instrument.clone(),
6896            1,
6897            FillModelHandle::default(),
6898            FeeModelAny::default().into(),
6899            BookType::L1_MBP,
6900            OmsType::Netting,
6901            AccountType::Margin,
6902            clock,
6903            cache,
6904            Default::default(),
6905        );
6906        let events = Rc::new(RefCell::new(Vec::new()));
6907        let events_handler = Rc::clone(&events);
6908        engine.set_event_handler(Rc::new(move |event| {
6909            events_handler.borrow_mut().push(event);
6910        }));
6911
6912        let mut order = OrderTestBuilder::new(OrderType::Market)
6913            .instrument_id(instrument.id())
6914            .side(OrderSide::Buy)
6915            .quantity(Quantity::from("1.000"))
6916            .submit(true)
6917            .build();
6918        order.set_liquidity_side(LiquiditySide::Maker);
6919        engine
6920            .account_ids
6921            .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
6922
6923        engine
6924            .fill_order(
6925                &order,
6926                Price::from("1500.00"),
6927                Quantity::from("1.000"),
6928                LiquiditySide::Taker,
6929                None,
6930                None,
6931            )
6932            .unwrap();
6933
6934        let events = events.borrow();
6935        assert_eq!(events.len(), 1);
6936        let fill = match &events[0] {
6937            OrderEventAny::Filled(fill) => fill,
6938            event => panic!("Expected OrderFilled, was {event:?}"),
6939        };
6940        let commission = fill.commission.expect("expected commission");
6941        let expected_commission =
6942            fill.last_qty.as_decimal() * fill.last_px.as_decimal() * instrument.taker_fee();
6943
6944        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
6945        assert_eq!(commission.currency, instrument.quote_currency());
6946        assert_eq!(commission.as_decimal(), expected_commission);
6947    }
6948
6949    #[rstest]
6950    fn test_custom_fee_model_handle_is_called_by_fill_order() {
6951        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
6952        let cache = Rc::new(RefCell::new(Cache::default()));
6953        let clock = Rc::new(RefCell::new(TestClock::new()));
6954        let calls = Rc::new(Cell::new(0));
6955        let expected_commission = Money::from("1.23 USDT");
6956        let fee_model = FeeModelHandle::new(RecordingFeeModel {
6957            calls: Rc::clone(&calls),
6958            commission: expected_commission,
6959        });
6960        let cloned_fee_model = fee_model.clone();
6961        drop(fee_model);
6962        let mut engine = OrderMatchingEngine::new(
6963            instrument.clone(),
6964            1,
6965            FillModelHandle::default(),
6966            cloned_fee_model,
6967            BookType::L1_MBP,
6968            OmsType::Netting,
6969            AccountType::Margin,
6970            clock,
6971            cache,
6972            Default::default(),
6973        );
6974        let events = Rc::new(RefCell::new(Vec::new()));
6975        let events_handler = Rc::clone(&events);
6976        engine.set_event_handler(Rc::new(move |event| {
6977            events_handler.borrow_mut().push(event);
6978        }));
6979
6980        let order = OrderTestBuilder::new(OrderType::Market)
6981            .instrument_id(instrument.id())
6982            .side(OrderSide::Buy)
6983            .quantity(Quantity::from("1.000"))
6984            .submit(true)
6985            .build();
6986        engine
6987            .account_ids
6988            .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
6989
6990        engine
6991            .fill_order(
6992                &order,
6993                Price::from("1500.00"),
6994                Quantity::from("1.000"),
6995                LiquiditySide::Taker,
6996                None,
6997                None,
6998            )
6999            .unwrap();
7000
7001        let events = events.borrow();
7002        assert_eq!(events.len(), 1);
7003        let fill = match &events[0] {
7004            OrderEventAny::Filled(fill) => fill,
7005            event => panic!("Expected OrderFilled, was {event:?}"),
7006        };
7007
7008        assert_eq!(calls.get(), 1);
7009        assert_eq!(fill.commission, Some(expected_commission));
7010    }
7011
7012    #[rstest]
7013    fn test_fill_order_does_not_cache_filled_qty_when_fee_model_fails() {
7014        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7015        let cache = Rc::new(RefCell::new(Cache::default()));
7016        let clock = Rc::new(RefCell::new(TestClock::new()));
7017        let mut engine = OrderMatchingEngine::new(
7018            instrument.clone(),
7019            1,
7020            FillModelHandle::default(),
7021            FeeModelHandle::new(FailingFeeModel),
7022            BookType::L1_MBP,
7023            OmsType::Netting,
7024            AccountType::Margin,
7025            clock,
7026            cache,
7027            Default::default(),
7028        );
7029        let events = Rc::new(RefCell::new(Vec::new()));
7030        let events_handler = Rc::clone(&events);
7031        engine.set_event_handler(Rc::new(move |event| {
7032            events_handler.borrow_mut().push(event);
7033        }));
7034
7035        let order = OrderTestBuilder::new(OrderType::Market)
7036            .instrument_id(instrument.id())
7037            .side(OrderSide::Buy)
7038            .quantity(Quantity::from("1.000"))
7039            .submit(true)
7040            .build();
7041        engine
7042            .account_ids
7043            .insert(order.trader_id(), AccountId::from("ACCOUNT-001"));
7044
7045        let result = engine.fill_order(
7046            &order,
7047            Price::from("1500.00"),
7048            Quantity::from("1.000"),
7049            LiquiditySide::Taker,
7050            None,
7051            None,
7052        );
7053
7054        assert!(result.is_err());
7055        assert_eq!(engine.cached_filled_qty_len(), 0);
7056        assert!(events.borrow().is_empty());
7057    }
7058
7059    fn collision_engine() -> (OrderMatchingEngine, Rc<RefCell<Cache>>, VenueOrderId) {
7060        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7061        let cache = Rc::new(RefCell::new(Cache::default()));
7062        let venue_order_id = VenueOrderId::from(format!("{}-1-1", instrument.id().venue));
7063        cache
7064            .borrow_mut()
7065            .add_venue_order_id(&ClientOrderId::from("O-OWNER"), &venue_order_id, false)
7066            .unwrap();
7067        let engine = OrderMatchingEngine::new(
7068            instrument,
7069            1,
7070            FillModelHandle::default(),
7071            FeeModelAny::default().into(),
7072            BookType::L1_MBP,
7073            OmsType::Netting,
7074            AccountType::Margin,
7075            Rc::new(RefCell::new(TestClock::new())),
7076            Rc::clone(&cache),
7077            Default::default(),
7078        );
7079
7080        (engine, cache, venue_order_id)
7081    }
7082
7083    #[rstest]
7084    #[case(OrderType::Market)]
7085    #[case(OrderType::MarketToLimit)]
7086    fn test_market_collision_probes_and_fills_with_default_ack_config(
7087        #[case] order_type: OrderType,
7088    ) {
7089        let (mut engine, cache, venue_order_id) = collision_engine();
7090        assert!(!engine.config.use_market_order_acks);
7091        let quote = QuoteTick::new(
7092            engine.instrument.id(),
7093            Price::from("1499.00"),
7094            Price::from("1500.00"),
7095            Quantity::from("10.000"),
7096            Quantity::from("10.000"),
7097            UnixNanos::default(),
7098            UnixNanos::default(),
7099        );
7100        engine.process_quote_tick(&quote);
7101        let events = Rc::new(RefCell::new(Vec::new()));
7102        let events_handler = Rc::clone(&events);
7103        engine.set_event_handler(Rc::new(move |event| {
7104            events_handler.borrow_mut().push(event);
7105        }));
7106        let mut order = OrderTestBuilder::new(order_type)
7107            .instrument_id(engine.instrument.id())
7108            .client_order_id(ClientOrderId::from("O-CLAIMANT"))
7109            .side(OrderSide::Buy)
7110            .quantity(Quantity::from("1.000"))
7111            .submit(true)
7112            .build();
7113
7114        engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
7115
7116        assert!(
7117            !events
7118                .borrow()
7119                .iter()
7120                .any(|event| matches!(event, OrderEventAny::Rejected(_)))
7121        );
7122        assert!(
7123            events
7124                .borrow()
7125                .iter()
7126                .any(|event| matches!(event, OrderEventAny::Filled(_)))
7127        );
7128        assert!(cache.borrow().order_exists(&order.client_order_id()));
7129        assert_eq!(
7130            cache.borrow().client_order_id(&venue_order_id),
7131            Some(&ClientOrderId::from("O-OWNER"))
7132        );
7133        assert_eq!(
7134            cache.borrow().venue_order_id(&order.client_order_id()),
7135            Some(&VenueOrderId::from(format!("{}-1-2", engine.venue)))
7136        );
7137    }
7138
7139    struct RecordingFeeModel {
7140        calls: Rc<Cell<u32>>,
7141        commission: Money,
7142    }
7143
7144    impl FeeModel for RecordingFeeModel {
7145        fn get_commission(
7146            &self,
7147            _order: &OrderAny,
7148            _fill_quantity: Quantity,
7149            _fill_px: Price,
7150            _instrument: &InstrumentAny,
7151        ) -> anyhow::Result<Money> {
7152            self.calls.set(self.calls.get() + 1);
7153            Ok(self.commission)
7154        }
7155    }
7156
7157    struct FailingFeeModel;
7158
7159    impl FeeModel for FailingFeeModel {
7160        fn get_commission(
7161            &self,
7162            _order: &OrderAny,
7163            _fill_quantity: Quantity,
7164            _fill_px: Price,
7165            _instrument: &InstrumentAny,
7166        ) -> anyhow::Result<Money> {
7167            Err(anyhow::anyhow!("fee model failed"))
7168        }
7169    }
7170
7171    #[rstest]
7172    fn test_custom_fill_model_handle_is_called_by_market_fill() {
7173        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7174        let cache = Rc::new(RefCell::new(Cache::default()));
7175        let clock = Rc::new(RefCell::new(TestClock::new()));
7176        let calls = Rc::new(Cell::new(0));
7177        let fill_model = FillModelHandle::new(RecordingFillModel {
7178            calls: Rc::clone(&calls),
7179        });
7180        let mut engine = OrderMatchingEngine::new(
7181            instrument.clone(),
7182            1,
7183            fill_model,
7184            FeeModelAny::default().into(),
7185            BookType::L1_MBP,
7186            OmsType::Netting,
7187            AccountType::Margin,
7188            clock,
7189            cache,
7190            Default::default(),
7191        );
7192        let quote = QuoteTick::new(
7193            instrument.id(),
7194            Price::from("1500.00"),
7195            Price::from("1501.00"),
7196            Quantity::from("10.000"),
7197            Quantity::from("10.000"),
7198            UnixNanos::default(),
7199            UnixNanos::default(),
7200        );
7201        engine.process_quote_tick(&quote);
7202
7203        let mut order = OrderTestBuilder::new(OrderType::Market)
7204            .instrument_id(instrument.id())
7205            .side(OrderSide::Buy)
7206            .quantity(Quantity::from("1.000"))
7207            .submit(true)
7208            .build();
7209        engine.process_order(&mut order, AccountId::from("ACCOUNT-001"));
7210
7211        assert_eq!(calls.get(), 1);
7212    }
7213
7214    #[rstest]
7215    fn test_l1_depth10_skips_padding_for_last_quote_tracking() {
7216        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7217        let cache = Rc::new(RefCell::new(Cache::default()));
7218        let clock = Rc::new(RefCell::new(TestClock::new()));
7219        let mut engine = OrderMatchingEngine::new(
7220            instrument.clone(),
7221            1,
7222            FillModelHandle::default(),
7223            FeeModelAny::default().into(),
7224            BookType::L1_MBP,
7225            OmsType::Netting,
7226            AccountType::Margin,
7227            clock,
7228            cache,
7229            Default::default(),
7230        );
7231        let mut bids = [BookOrder::default(); DEPTH10_LEN];
7232        let mut asks = [BookOrder::default(); DEPTH10_LEN];
7233        bids[1] = BookOrder::new(
7234            OrderSide::Buy,
7235            Price::from("1499.00"),
7236            Quantity::from("1.000"),
7237            1,
7238        );
7239        asks[0] = BookOrder::new(
7240            OrderSide::Sell,
7241            Price::from("1500.00"),
7242            Quantity::from("1.000"),
7243            2,
7244        );
7245
7246        let depth = OrderBookDepth10::new(
7247            instrument.id(),
7248            bids,
7249            asks,
7250            [0; DEPTH10_LEN],
7251            [0; DEPTH10_LEN],
7252            0,
7253            0,
7254            UnixNanos::from(1_u64),
7255            UnixNanos::from(1_u64),
7256        );
7257        engine.process_order_book_depth10(&depth).unwrap();
7258
7259        assert_eq!(engine.last_quote_bid, Some(Price::from("1499.00")));
7260        assert_eq!(engine.last_quote_ask, Some(Price::from("1500.00")));
7261
7262        let depth_without_bid = OrderBookDepth10::new(
7263            instrument.id(),
7264            [BookOrder::default(); DEPTH10_LEN],
7265            asks,
7266            [0; DEPTH10_LEN],
7267            [0; DEPTH10_LEN],
7268            0,
7269            1,
7270            UnixNanos::from(2_u64),
7271            UnixNanos::from(2_u64),
7272        );
7273        engine
7274            .process_order_book_depth10(&depth_without_bid)
7275            .unwrap();
7276
7277        assert_eq!(engine.last_quote_bid, None);
7278        assert_eq!(engine.last_quote_ask, Some(Price::from("1500.00")));
7279    }
7280
7281    struct RecordingFillModel {
7282        calls: Rc<Cell<u32>>,
7283    }
7284
7285    impl FillModel for RecordingFillModel {
7286        fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
7287            Ok(true)
7288        }
7289
7290        fn is_slipped(&mut self) -> anyhow::Result<bool> {
7291            Ok(false)
7292        }
7293
7294        fn get_orderbook_for_fill_simulation(
7295            &mut self,
7296            _instrument: &InstrumentAny,
7297            _order: &OrderAny,
7298            _best_bid: Price,
7299            _best_ask: Price,
7300        ) -> anyhow::Result<Option<OrderBook>> {
7301            self.calls.set(self.calls.get() + 1);
7302            Ok(None)
7303        }
7304    }
7305
7306    #[rstest]
7307    fn test_fee_underlying_price_uses_valid_cached_greeks_price() {
7308        let instrument = InstrumentAny::CryptoOption(crypto_option_btc_deribit(
7309            3,
7310            1,
7311            Price::from("0.001"),
7312            Quantity::from("0.1"),
7313        ));
7314        let cache = Rc::new(RefCell::new(Cache::default()));
7315        cache.borrow_mut().add_option_greeks(OptionGreeks {
7316            instrument_id: instrument.id(),
7317            underlying_price: Some(50_000.0),
7318            ..Default::default()
7319        });
7320        let clock = Rc::new(RefCell::new(TestClock::new()));
7321        let engine = OrderMatchingEngine::new(
7322            instrument,
7323            1,
7324            FillModelHandle::default(),
7325            FeeModelAny::default().into(),
7326            BookType::L1_MBP,
7327            OmsType::Netting,
7328            AccountType::Margin,
7329            clock,
7330            cache,
7331            Default::default(),
7332        );
7333
7334        let price = engine
7335            .fee_underlying_price()
7336            .unwrap()
7337            .expect("expected underlying price");
7338
7339        assert_eq!(price.precision, FIXED_PRECISION);
7340        assert_eq!(price.as_decimal(), Decimal::from(50_000));
7341    }
7342
7343    #[rstest]
7344    fn test_fee_underlying_price_rejects_invalid_cached_greeks_price() {
7345        let instrument = InstrumentAny::CryptoOption(crypto_option_btc_deribit(
7346            3,
7347            1,
7348            Price::from("0.001"),
7349            Quantity::from("0.1"),
7350        ));
7351        let cache = Rc::new(RefCell::new(Cache::default()));
7352        cache.borrow_mut().add_option_greeks(OptionGreeks {
7353            instrument_id: instrument.id(),
7354            underlying_price: Some(f64::NAN),
7355            ..Default::default()
7356        });
7357        let clock = Rc::new(RefCell::new(TestClock::new()));
7358        let engine = OrderMatchingEngine::new(
7359            instrument,
7360            1,
7361            FillModelHandle::default(),
7362            FeeModelAny::default().into(),
7363            BookType::L1_MBP,
7364            OmsType::Netting,
7365            AccountType::Margin,
7366            clock,
7367            cache,
7368            Default::default(),
7369        );
7370
7371        let error = engine.fee_underlying_price().unwrap_err();
7372
7373        assert_eq!(
7374            error,
7375            CorrectnessError::InvalidValue {
7376                param: "value".to_string(),
7377                value: "NaN".to_string(),
7378                type_name: "f64",
7379            }
7380        );
7381    }
7382
7383    #[rstest]
7384    fn test_bar_tick_sizes_divisible() {
7385        // precision=3, units=100_000: exactly divisible by 4, no rounding.
7386        let volume = Quantity::from("100.000");
7387        let increment = Quantity::from("0.001");
7388        let sizes = BarTickSizes::from_volume(volume, increment);
7389        assert_eq!(sizes.open, Quantity::from("25.000"));
7390        assert_eq!(sizes.high, Quantity::from("25.000"));
7391        assert_eq!(sizes.low, Quantity::from("25.000"));
7392        assert_eq!(sizes.close, Quantity::from("25.000"));
7393        assert_valid_bar_tick_sizes(volume, increment);
7394    }
7395
7396    #[rstest]
7397    fn test_bar_tick_sizes_indivisible_with_remainder() {
7398        // precision=2, units=5: quarter_units=1, remainder=1; close carries 2 units.
7399        let volume = Quantity::from("0.05");
7400        let increment = Quantity::from("0.01");
7401        let sizes = BarTickSizes::from_volume(volume, increment);
7402        assert_eq!(sizes.open, Quantity::from("0.01"));
7403        assert_eq!(sizes.high, Quantity::from("0.01"));
7404        assert_eq!(sizes.low, Quantity::from("0.01"));
7405        assert_eq!(sizes.close, Quantity::from("0.02"));
7406        assert_valid_bar_tick_sizes(volume, increment);
7407        assert_eq!(
7408            sizes.open.raw + sizes.high.raw + sizes.low.raw + sizes.close.raw,
7409            volume.raw
7410        );
7411    }
7412
7413    #[rstest]
7414    #[case("1", "0", "0", "0", "1")]
7415    #[case("2", "0", "1", "1", "0")]
7416    #[case("3", "1", "1", "1", "0")]
7417    fn test_bar_tick_sizes_units_less_than_four_preserves_volume(
7418        #[case] volume: &str,
7419        #[case] open_size: &str,
7420        #[case] high_size: &str,
7421        #[case] low_size: &str,
7422        #[case] close_size: &str,
7423    ) {
7424        let volume = Quantity::from(volume);
7425        let increment = Quantity::from("1");
7426        let sizes = BarTickSizes::from_volume(volume, increment);
7427
7428        assert_eq!(sizes.open, Quantity::from(open_size));
7429        assert_eq!(sizes.high, Quantity::from(high_size));
7430        assert_eq!(sizes.low, Quantity::from(low_size));
7431        assert_eq!(sizes.close, Quantity::from(close_size));
7432        assert_valid_bar_tick_sizes(volume, increment);
7433        assert_eq!(
7434            sizes.open.raw + sizes.high.raw + sizes.low.raw + sizes.close.raw,
7435            volume.raw
7436        );
7437    }
7438
7439    #[rstest]
7440    fn test_bar_tick_sizes_zero_volume_remains_zero() {
7441        let volume = Quantity::zero(3);
7442        let increment = Quantity::from("0.001");
7443        let sizes = BarTickSizes::from_volume(volume, increment);
7444        assert_eq!(sizes.open, Quantity::zero(3));
7445        assert_eq!(sizes.high, Quantity::zero(3));
7446        assert_eq!(sizes.low, Quantity::zero(3));
7447        assert_eq!(sizes.close, Quantity::zero(3));
7448        assert_valid_bar_tick_sizes(volume, increment);
7449    }
7450
7451    #[rstest]
7452    fn test_bar_tick_sizes_rounds_down_to_size_increment() {
7453        let volume = Quantity::from("1.07");
7454        let increment = Quantity::from("0.10");
7455        let sizes = BarTickSizes::from_volume(volume, increment);
7456        assert_eq!(sizes.open, Quantity::from("0.20"));
7457        assert_eq!(sizes.high, Quantity::from("0.20"));
7458        assert_eq!(sizes.low, Quantity::from("0.20"));
7459        assert_eq!(sizes.close, Quantity::from("0.40"));
7460        assert_valid_bar_tick_sizes(volume, increment);
7461    }
7462
7463    #[rstest]
7464    fn test_bar_tick_sizes_at_fixed_precision() {
7465        // When volume.precision == FIXED_PRECISION the scale is 1 and the formula
7466        // degenerates to a plain raw-space quartering.
7467        let units: QuantityRaw = 17;
7468        let volume = Quantity::from_raw(units, FIXED_PRECISION);
7469        let increment = Quantity::from_raw(1, FIXED_PRECISION);
7470        let sizes = BarTickSizes::from_volume(volume, increment);
7471        assert_eq!(sizes.open.raw, 4);
7472        assert_eq!(sizes.high.raw, 4);
7473        assert_eq!(sizes.low.raw, 4);
7474        assert_eq!(sizes.close.raw, 5);
7475        assert_valid_bar_tick_sizes(volume, increment);
7476    }
7477
7478    fn get_l3_queue_engine(instrument: InstrumentAny) -> (OrderMatchingEngine, Rc<RefCell<Cache>>) {
7479        let clock = Rc::new(RefCell::new(TestClock::new()));
7480        let cache = Rc::new(RefCell::new(Cache::default()));
7481        let config = OrderMatchingEngineConfig {
7482            trade_execution: true,
7483            queue_position: true,
7484            ..Default::default()
7485        };
7486
7487        let mut engine = OrderMatchingEngine::new(
7488            instrument,
7489            1,
7490            FillModelHandle::default(),
7491            FeeModelAny::default().into(),
7492            BookType::L3_MBO,
7493            OmsType::Netting,
7494            AccountType::Margin,
7495            clock,
7496            Rc::clone(&cache),
7497            config,
7498        );
7499
7500        let handler_cache = Rc::clone(&cache);
7501        engine.set_event_handler(Rc::new(move |event: OrderEventAny| {
7502            if let Ok(mut cache) = handler_cache.try_borrow_mut() {
7503                let _ = cache.update_order(&event);
7504            }
7505        }));
7506
7507        (engine, cache)
7508    }
7509
7510    fn assert_l3_queue_synced(engine: &OrderMatchingEngine) {
7511        for (client_order_id, orders_ahead) in &engine.queue_ahead_orders {
7512            let set_sum: QuantityRaw = orders_ahead.values().sum();
7513            let counter = engine
7514                .queue_ahead_total
7515                .get(client_order_id)
7516                .map_or(0, |&(_, ahead_raw)| ahead_raw);
7517            assert_eq!(
7518                set_sum, counter,
7519                "tracked orders out of sync with quantity-ahead counter for {client_order_id}",
7520            );
7521        }
7522    }
7523
7524    #[derive(Debug, Clone, Copy)]
7525    enum QueueEvent {
7526        Add { id: OrderId, size: u64 },
7527        Update { id: OrderId, size: u64 },
7528        MoveAway { id: OrderId },
7529        Delete { id: OrderId },
7530        Trade { size: u64, aggressor: u8 },
7531        AggregateCap { size: u64 },
7532        AggregateDelete,
7533        RestOrder,
7534    }
7535
7536    fn granular_queue_event() -> impl Strategy<Value = QueueEvent> {
7537        prop_oneof![
7538            3 => (1u64..=6, 1u64..=9).prop_map(|(id, size)| QueueEvent::Add { id, size }),
7539            3 => (1u64..=6, 1u64..=9).prop_map(|(id, size)| QueueEvent::Update { id, size }),
7540            1 => (1u64..=6).prop_map(|id| QueueEvent::MoveAway { id }),
7541            2 => (1u64..=6).prop_map(|id| QueueEvent::Delete { id }),
7542            2 => Just(QueueEvent::RestOrder),
7543        ]
7544    }
7545
7546    fn any_queue_event() -> impl Strategy<Value = QueueEvent> {
7547        prop_oneof![
7548            5 => granular_queue_event(),
7549            3 => (1u64..=9, 0u8..3).prop_map(|(size, aggressor)| QueueEvent::Trade {
7550                size,
7551                aggressor,
7552            }),
7553            1 => (1u64..=9).prop_map(|size| QueueEvent::AggregateCap { size }),
7554            1 => Just(QueueEvent::AggregateDelete),
7555        ]
7556    }
7557
7558    // Drives generated events through an L3 queue_position engine; the
7559    // shadow id maps sanitize the feed to what real MBO feeds guarantee
7560    struct L3QueueSim {
7561        engine: OrderMatchingEngine,
7562        account_id: AccountId,
7563        live_main: HashMap<OrderId, u64>,
7564        live_away: HashSet<OrderId>,
7565        rest_snapshots: HashMap<ClientOrderId, HashSet<OrderId>>,
7566        rested: usize,
7567        sequence: u64,
7568    }
7569
7570    impl L3QueueSim {
7571        const MAIN_PRICE: &'static str = "100.00";
7572        const AWAY_PRICE: &'static str = "101.00";
7573
7574        fn new() -> Self {
7575            let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7576            let (engine, _cache) = get_l3_queue_engine(instrument);
7577
7578            Self {
7579                engine,
7580                account_id: AccountId::from("SIM-001"),
7581                live_main: HashMap::new(),
7582                live_away: HashSet::new(),
7583                rest_snapshots: HashMap::new(),
7584                rested: 0,
7585                sequence: 0,
7586            }
7587        }
7588
7589        fn quantity(size: u64) -> Quantity {
7590            Quantity::from(format!("{size}.000").as_str())
7591        }
7592
7593        fn process_delta(
7594            &mut self,
7595            action: BookAction,
7596            price: &str,
7597            size: u64,
7598            order_id: OrderId,
7599            flags: u8,
7600        ) {
7601            self.sequence += 1;
7602            let delta = OrderBookDelta::new(
7603                self.engine.instrument.id(),
7604                action,
7605                BookOrder::new(
7606                    OrderSide::Sell,
7607                    Price::from(price),
7608                    Self::quantity(size),
7609                    order_id,
7610                ),
7611                flags,
7612                self.sequence,
7613                UnixNanos::from(self.sequence),
7614                UnixNanos::from(self.sequence),
7615            );
7616            self.engine.process_order_book_delta(&delta).unwrap();
7617        }
7618
7619        fn apply(&mut self, event: QueueEvent) {
7620            match event {
7621                QueueEvent::Add { id, size } => {
7622                    if self.live_main.contains_key(&id) || self.live_away.contains(&id) {
7623                        return;
7624                    }
7625                    self.process_delta(BookAction::Add, Self::MAIN_PRICE, size, id, 0);
7626                    self.live_main.insert(id, size);
7627                }
7628                QueueEvent::Update { id, size } => {
7629                    if !self.live_main.contains_key(&id) {
7630                        return;
7631                    }
7632                    self.process_delta(BookAction::Update, Self::MAIN_PRICE, size, id, 0);
7633                    self.live_main.insert(id, size);
7634                }
7635                QueueEvent::MoveAway { id } => {
7636                    let Some(size) = self.live_main.remove(&id) else {
7637                        return;
7638                    };
7639                    self.process_delta(BookAction::Update, Self::AWAY_PRICE, size, id, 0);
7640                    self.live_away.insert(id);
7641                }
7642                QueueEvent::Delete { id } => {
7643                    if let Some(size) = self.live_main.remove(&id) {
7644                        self.process_delta(BookAction::Delete, Self::MAIN_PRICE, size, id, 0);
7645                    } else if self.live_away.remove(&id) {
7646                        self.process_delta(BookAction::Delete, Self::AWAY_PRICE, 1, id, 0);
7647                    } else {
7648                        // Unknown id exercises the ignore path
7649                        self.process_delta(BookAction::Delete, Self::MAIN_PRICE, 1, id, 0);
7650                    }
7651
7652                    // A later Add reusing this id is a new order, not the
7653                    // snapshot-time one (real feeds never reuse ids)
7654                    for snapshot_ids in self.rest_snapshots.values_mut() {
7655                        snapshot_ids.remove(&id);
7656                    }
7657                }
7658                QueueEvent::Trade { size, aggressor } => {
7659                    self.sequence += 1;
7660                    let aggressor_side = match aggressor {
7661                        0 => AggressorSide::Buyer,
7662                        1 => AggressorSide::Seller,
7663                        _ => AggressorSide::NoAggressor,
7664                    };
7665                    let trade = TradeTick::new(
7666                        self.engine.instrument.id(),
7667                        Price::from(Self::MAIN_PRICE),
7668                        Self::quantity(size),
7669                        aggressor_side,
7670                        TradeId::new(format!("T-{}", self.sequence).as_str()),
7671                        UnixNanos::from(self.sequence),
7672                        UnixNanos::from(self.sequence),
7673                    );
7674                    self.engine.process_trade_tick(&trade);
7675                }
7676                QueueEvent::AggregateCap { size } => {
7677                    self.process_delta(
7678                        BookAction::Update,
7679                        Self::MAIN_PRICE,
7680                        size,
7681                        0,
7682                        RecordFlag::F_MBP as u8,
7683                    );
7684                }
7685                QueueEvent::AggregateDelete => {
7686                    self.process_delta(
7687                        BookAction::Delete,
7688                        Self::MAIN_PRICE,
7689                        1,
7690                        0,
7691                        RecordFlag::F_MBP as u8,
7692                    );
7693                }
7694                QueueEvent::RestOrder => {
7695                    if self.rested >= 3 {
7696                        return;
7697                    }
7698                    self.rested += 1;
7699                    let mut order = OrderTestBuilder::new(OrderType::Limit)
7700                        .instrument_id(self.engine.instrument.id())
7701                        .side(OrderSide::Sell)
7702                        .price(Price::from(Self::MAIN_PRICE))
7703                        .quantity(Self::quantity(5))
7704                        .client_order_id(ClientOrderId::from(
7705                            format!("O-PROP-{}", self.rested).as_str(),
7706                        ))
7707                        .submit(true)
7708                        .build();
7709                    self.engine.process_order(&mut order, self.account_id);
7710
7711                    assert!(
7712                        self.engine
7713                            .queue_ahead_orders
7714                            .contains_key(&order.client_order_id()),
7715                        "L3 snapshot must track the resting order",
7716                    );
7717
7718                    self.rest_snapshots.insert(
7719                        order.client_order_id(),
7720                        self.live_main.keys().copied().collect(),
7721                    );
7722                }
7723            }
7724        }
7725
7726        // Without trades or aggregate rows, tracked orders must mirror the book
7727        // exactly, and equal the rest-time snapshot ids still at the level
7728        fn assert_tracked_orders_match_book(&self) {
7729            let level: HashMap<OrderId, QuantityRaw> = self
7730                .engine
7731                .book
7732                .get_orders_at_level(Price::from(Self::MAIN_PRICE), OrderSide::Buy)
7733                .iter()
7734                .map(|order| (order.order_id, order.size.raw))
7735                .collect();
7736
7737            for (client_order_id, orders_ahead) in &self.engine.queue_ahead_orders {
7738                for (order_id, size_raw) in orders_ahead {
7739                    let book_size = level.get(order_id).copied().unwrap_or_else(|| {
7740                        panic!("tracked order {order_id} for {client_order_id} not in book level")
7741                    });
7742                    assert_eq!(
7743                        book_size, *size_raw,
7744                        "tracked size diverged from book for order {order_id}",
7745                    );
7746                }
7747
7748                let tracked: HashSet<OrderId> = orders_ahead.keys().copied().collect();
7749                let expected: HashSet<OrderId> = self.rest_snapshots[client_order_id]
7750                    .iter()
7751                    .filter(|id| self.live_main.contains_key(id))
7752                    .copied()
7753                    .collect();
7754                assert_eq!(
7755                    tracked, expected,
7756                    "tracked set incomplete or stale for {client_order_id}",
7757                );
7758            }
7759        }
7760    }
7761
7762    #[rstest]
7763    fn prop_test_l3_queue_tracking_stays_synced_with_counter() {
7764        proptest!(|(events in prop::collection::vec(any_queue_event(), 1..=80))| {
7765            let mut sim = L3QueueSim::new();
7766            for event in events {
7767                sim.apply(event);
7768                assert_l3_queue_synced(&sim.engine);
7769            }
7770        });
7771    }
7772
7773    #[rstest]
7774    fn prop_test_l3_queue_tracking_mirrors_book_without_trades() {
7775        proptest!(|(events in prop::collection::vec(granular_queue_event(), 1..=80))| {
7776            let mut sim = L3QueueSim::new();
7777            for event in events {
7778                sim.apply(event);
7779                assert_l3_queue_synced(&sim.engine);
7780                sim.assert_tracked_orders_match_book();
7781            }
7782        });
7783    }
7784
7785    // Replays real GLBX MBO flow (records 9150..10650 of
7786    // tests/test_data/databento/esh4-glbx-mdp3-20231225.mbo.dbn.zst as JSON),
7787    // joining the touch periodically; the mid-stream start also exercises
7788    // unseen-id ignore paths
7789    #[rstest]
7790    fn test_l3_queue_position_replay_databento_mbo_stays_synced() {
7791        let json = include_str!("../../test_data/databento_esh4_mbo.json");
7792        let records: Vec<serde_json::Value> = serde_json::from_str(json).unwrap();
7793        assert!(records.len() > 1000);
7794
7795        let instrument = InstrumentAny::FuturesContract(futures_contract_es(None, None));
7796        let instrument_id = instrument.id();
7797        let (mut engine, cache) = get_l3_queue_engine(instrument);
7798        let account_id = AccountId::from("SIM-001");
7799
7800        let mut rested = 0usize;
7801        let mut trades = 0usize;
7802
7803        for (index, record) in records.iter().enumerate() {
7804            match record.get("type").and_then(serde_json::Value::as_str) {
7805                Some("OrderBookDelta") => {
7806                    let mut delta: OrderBookDelta = serde_json::from_value(record.clone()).unwrap();
7807                    delta.instrument_id = instrument_id;
7808                    engine.process_order_book_delta(&delta).unwrap();
7809                }
7810                Some("TradeTick") => {
7811                    let mut trade: TradeTick = serde_json::from_value(record.clone()).unwrap();
7812                    trade.instrument_id = instrument_id;
7813                    engine.process_trade_tick(&trade);
7814                    trades += 1;
7815                }
7816                other => panic!("unexpected record type {other:?}"),
7817            }
7818
7819            if index % 150 == 100 {
7820                let (side, price) = if rested.is_multiple_of(2) {
7821                    (OrderSide::Sell, engine.book.best_ask_price())
7822                } else {
7823                    (OrderSide::Buy, engine.book.best_bid_price())
7824                };
7825
7826                if let Some(price) = price {
7827                    rested += 1;
7828                    let mut order = OrderTestBuilder::new(OrderType::Limit)
7829                        .instrument_id(instrument_id)
7830                        .side(side)
7831                        .price(price)
7832                        .quantity(Quantity::from("1"))
7833                        .client_order_id(ClientOrderId::from(format!("O-MBO-{rested}").as_str()))
7834                        .submit(true)
7835                        .build();
7836                    engine.process_order(&mut order, account_id);
7837
7838                    // A crossed mid-stream book can fill a joined order on
7839                    // arrival; only open orders are tracked
7840                    let is_open = cache
7841                        .borrow()
7842                        .order(&order.client_order_id())
7843                        .is_some_and(|order| order.is_open());
7844                    if is_open {
7845                        assert!(
7846                            engine
7847                                .queue_ahead_orders
7848                                .contains_key(&order.client_order_id()),
7849                            "L3 snapshot must track the resting order",
7850                        );
7851                    }
7852                }
7853            }
7854
7855            assert_l3_queue_synced(&engine);
7856        }
7857
7858        assert!(rested >= 5, "replay must exercise resting orders");
7859        assert!(trades >= 50, "replay must exercise trade interleavings");
7860    }
7861}