Skip to main content

nautilus_execution/matching_engine/
mod.rs

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