Skip to main content

nautilus_execution/matching_core/
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//! A common order matching core for the `OrderMatchingEngine` and other components.
17//!
18//! # Book layout
19//!
20//! Each side has two separate books, mirroring real-venue architecture:
21//! - **Limit book**: `BTreeMap<Price, OrderBucket>` keyed by limit price.
22//!   Holds plain `LIMIT` orders.
23//! - **Stop book**: `BTreeMap<Price, OrderBucket>` keyed by trigger price.
24//!   Holds `STOP_*`, `*_IF_TOUCHED`, and `TRAILING_STOP_*` orders that need
25//!   trigger checking before matching.
26//!
27//! Plus a per-side pending `SmallVec` for orders without a key (e.g.
28//! `MARKET_TO_LIMIT` before conversion).
29//!
30//! # Ordering invariant
31//!
32//! Orders are matched in **price-time priority**, with limits processed
33//! before stops on each side:
34//! - **Bid limits**: best (highest) price first via `iter().rev()`.
35//! - **Ask limits**: best (lowest) price first via `iter()`.
36//! - **Bid stops**: closest trigger first via `iter()` (lowest trigger crosses
37//!   first as ask climbs through resting buy stops).
38//! - **Ask stops**: closest trigger first via `iter().rev()` (highest trigger
39//!   crosses first as bid drops through resting sell stops).
40//!
41//! Within a price level orders are stored in a `SmallVec` in insertion order,
42//! preserving time priority (FIFO at the same price). No active sorting
43//! happens; the `BTreeMap`'s tree shape gives price ordering for free.
44//!
45//! # Modify semantics
46//!
47//! The core does not expose an in-place modify API. Any change to a resting
48//! order must call [`OrderMatchingCore::delete_order`] followed by
49//! [`OrderMatchingCore::add_order`], which lands the order at the back of
50//! its (new or unchanged) price level. This matches real-venue behavior for
51//! price-changing modifies but loses queue position on quantity-only
52//! modifies. An in-place quantity-update API could be added later if the
53//! engine wants to preserve queue position on those.
54//!
55//! # Known limitation: limits-then-stops emission
56//!
57//! On each side, [`OrderMatchingCore::iterate_bids`] and
58//! [`OrderMatchingCore::iterate_asks`] emit all matchable limits before any
59//! triggered stops. In real venues stops trigger as the price crosses them
60//! and only then aggress against the limit book, so a snapshot iteration that
61//! sees both kinds matchable simultaneously cannot perfectly reconstruct the
62//! temporal order. The matching engine drives the snapshot, so a future
63//! engine change that feeds the previous bid/ask to the core could replay
64//! the price path and emit triggers/fills in cross-time order. Until then,
65//! callers that depend on price-path ordering (e.g. multi-level gap
66//! scenarios with both matchable limits and matchable stops on the same
67//! side) should treat that interleaving as undefined.
68//!
69//! # Duplicate inserts
70//!
71//! `add_order` does not deduplicate. Adding the same `client_order_id` twice
72//! without an intervening `delete_order` puts two `RestingOrder` entries in
73//! the vec and they will both match. Callers must ensure each
74//! `client_order_id` appears at most once across both sides.
75//!
76//! # Performance
77//!
78//! Per-level buckets are `SmallVec`s with [`INLINE_ORDERS_PER_LEVEL`] inline
79//! slots so the common case (1-3 orders per price) avoids heap allocation
80//! per bucket. Above that threshold the bucket spills to the heap. Adds and
81//! deletes are O(log L) for the `BTreeMap` lookup plus O(B) for the bucket
82//! scan/shift, where L is the number of distinct price levels per book and
83//! B is orders at that level: both small in practice.
84//!
85//! An `AHashMap` index from `ClientOrderId` to `(side, BookKind, Price)`
86//! makes [`OrderMatchingCore::get_order`], [`OrderMatchingCore::order_exists`],
87//! and the lookup portion of [`OrderMatchingCore::delete_order`] hash-fast;
88//! the follow-on bucket scan is O(B). The map is used purely for point
89//! queries (never iterated), so its randomized seed does not affect
90//! determinism.
91
92use std::collections::BTreeMap;
93
94use ahash::AHashMap;
95use nautilus_model::{
96    enums::{OrderSideSpecified, OrderType, TriggerType},
97    identifiers::{ClientOrderId, InstrumentId},
98    orders::{Order, OrderError, PassiveOrderAny, StopOrderAny},
99    types::Price,
100};
101use smallvec::SmallVec;
102
103/// Inline capacity for orders at a single price level. Sized to cover the
104/// typical 1-3 orders per level; above this the per-bucket `SmallVec` spills
105/// to the heap.
106pub const INLINE_ORDERS_PER_LEVEL: usize = 4;
107
108type OrderBucket = SmallVec<[RestingOrder; INLINE_ORDERS_PER_LEVEL]>;
109
110/// Identifies which per-side book a [`RestingOrder`] lives in.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112enum BookKind {
113    /// Plain `LIMIT` order in the limit book, keyed by limit price.
114    Limit,
115    /// Stop-style order in the stop book, keyed by trigger price. Includes
116    /// `STOP_*`, `*_IF_TOUCHED`, and `TRAILING_STOP_*` order types.
117    Stop,
118}
119
120/// An action returned by [`OrderMatchingCore::iterate`] when an order matches.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum MatchAction {
123    FillLimit(ClientOrderId),
124    TriggerStop(ClientOrderId),
125}
126
127/// Lightweight order information for matching/trigger checking.
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub struct RestingOrder {
130    pub client_order_id: ClientOrderId,
131    pub order_side: OrderSideSpecified,
132    pub order_type: OrderType,
133    pub trigger_type: TriggerType,
134    pub trigger_price: Option<Price>,
135    pub limit_price: Option<Price>,
136    pub is_activated: bool,
137}
138
139impl RestingOrder {
140    /// Creates a new [`RestingOrder`] instance.
141    ///
142    /// `MARKET_TO_LIMIT` orders may legitimately be constructed with both
143    /// `trigger_price` and `limit_price` set to `None` until they convert to
144    /// a limit at execution time; [`OrderMatchingCore::match_order`] returns
145    /// `None` for such orders. This is a known coverage gap and not a bug
146    /// in the constructor.
147    #[must_use]
148    pub const fn new(
149        client_order_id: ClientOrderId,
150        order_side: OrderSideSpecified,
151        order_type: OrderType,
152        trigger_price: Option<Price>,
153        limit_price: Option<Price>,
154        is_activated: bool,
155    ) -> Self {
156        Self::new_with_trigger_type(
157            client_order_id,
158            order_side,
159            order_type,
160            TriggerType::Default,
161            trigger_price,
162            limit_price,
163            is_activated,
164        )
165    }
166
167    #[must_use]
168    pub(crate) const fn new_with_trigger_type(
169        client_order_id: ClientOrderId,
170        order_side: OrderSideSpecified,
171        order_type: OrderType,
172        trigger_type: TriggerType,
173        trigger_price: Option<Price>,
174        limit_price: Option<Price>,
175        is_activated: bool,
176    ) -> Self {
177        Self {
178            client_order_id,
179            order_side,
180            order_type,
181            trigger_type,
182            trigger_price,
183            limit_price,
184            is_activated,
185        }
186    }
187
188    /// Returns true if this is a stop order type that needs trigger checking.
189    #[must_use]
190    pub const fn is_stop(&self) -> bool {
191        self.trigger_price.is_some()
192    }
193
194    /// Returns true if this is a limit order type that needs fill checking.
195    #[must_use]
196    pub const fn is_limit(&self) -> bool {
197        self.limit_price.is_some() && self.trigger_price.is_none()
198    }
199}
200
201impl From<&PassiveOrderAny> for RestingOrder {
202    fn from(order: &PassiveOrderAny) -> Self {
203        match order {
204            PassiveOrderAny::Limit(limit) => Self {
205                client_order_id: limit.client_order_id(),
206                order_side: limit.order_side_specified(),
207                order_type: limit.order_type(),
208                trigger_type: TriggerType::NoTrigger,
209                trigger_price: None,
210                limit_price: Some(limit.limit_px()),
211                is_activated: true,
212            },
213            PassiveOrderAny::Stop(stop) => {
214                let limit_price = match stop {
215                    StopOrderAny::LimitIfTouched(o) => Some(o.price),
216                    StopOrderAny::StopLimit(o) => Some(o.price),
217                    StopOrderAny::TrailingStopLimit(o) => o.price,
218                    StopOrderAny::MarketIfTouched(_)
219                    | StopOrderAny::StopMarket(_)
220                    | StopOrderAny::TrailingStopMarket(_) => None,
221                };
222                let is_activated = match stop {
223                    StopOrderAny::TrailingStopMarket(o) => o.is_activated,
224                    StopOrderAny::TrailingStopLimit(o) => o.is_activated,
225                    _ => true,
226                };
227                Self {
228                    client_order_id: stop.client_order_id(),
229                    order_side: stop.order_side_specified(),
230                    order_type: stop.order_type(),
231                    trigger_type: stop.trigger_type().unwrap_or(TriggerType::Default),
232                    trigger_price: stop.stop_px(),
233                    limit_price,
234                    is_activated,
235                }
236            }
237        }
238    }
239}
240
241/// A generic order matching core. See module docs for ordering, modify,
242/// duplicate, and performance contracts.
243#[derive(Clone, Debug)]
244pub struct OrderMatchingCore {
245    /// The instrument ID for the matching core.
246    pub instrument_id: InstrumentId,
247    /// The price increment for the matching core.
248    pub price_increment: Price,
249    /// The current bid price for the matching core.
250    pub bid: Option<Price>,
251    /// The current ask price for the matching core.
252    pub ask: Option<Price>,
253    /// The last price for the matching core.
254    pub last: Option<Price>,
255    fill_limit_inside_spread: bool,
256    bid_limits: BTreeMap<Price, OrderBucket>,
257    ask_limits: BTreeMap<Price, OrderBucket>,
258    bid_stops: BTreeMap<Price, OrderBucket>,
259    ask_stops: BTreeMap<Price, OrderBucket>,
260    pending_bid: SmallVec<[RestingOrder; 2]>,
261    pending_ask: SmallVec<[RestingOrder; 2]>,
262    order_index: AHashMap<ClientOrderId, (OrderSideSpecified, Option<(BookKind, Price)>)>,
263}
264
265impl OrderMatchingCore {
266    /// Creates a new [`OrderMatchingCore`] for the given instrument.
267    #[must_use]
268    pub fn new(instrument_id: InstrumentId, price_increment: Price) -> Self {
269        Self {
270            instrument_id,
271            price_increment,
272            bid: None,
273            ask: None,
274            last: None,
275            fill_limit_inside_spread: false,
276            bid_limits: BTreeMap::new(),
277            ask_limits: BTreeMap::new(),
278            bid_stops: BTreeMap::new(),
279            ask_stops: BTreeMap::new(),
280            pending_bid: SmallVec::new(),
281            pending_ask: SmallVec::new(),
282            order_index: AHashMap::new(),
283        }
284    }
285
286    /// Returns the price precision of the instrument's tick size.
287    #[must_use]
288    pub const fn price_precision(&self) -> u8 {
289        self.price_increment.precision
290    }
291
292    /// Returns the order with the given `client_order_id`, searching both sides.
293    #[must_use]
294    pub fn get_order(&self, client_order_id: ClientOrderId) -> Option<&RestingOrder> {
295        let (side, location) = self.order_index.get(&client_order_id).copied()?;
296        if let Some((kind, price)) = location {
297            self.book_for(side, kind)
298                .get(&price)?
299                .iter()
300                .find(|o| o.client_order_id == client_order_id)
301        } else {
302            self.pending_for(side)
303                .iter()
304                .find(|o| o.client_order_id == client_order_id)
305        }
306    }
307
308    /// Iterates the bid-side orders in price-time priority without
309    /// allocating: limits best (highest) first, then stops nearest-trigger
310    /// (lowest) first, then pending unkeyed orders. Borrowed view; for an
311    /// owned snapshot use [`Self::get_orders_bid`].
312    pub fn iter_bid_orders(&self) -> impl Iterator<Item = &RestingOrder> {
313        self.bid_limits
314            .values()
315            .rev()
316            .flat_map(|b| b.iter())
317            .chain(self.bid_stops.values().flat_map(|b| b.iter()))
318            .chain(self.pending_bid.iter())
319    }
320
321    /// Iterates the ask-side orders in price-time priority without
322    /// allocating: limits best (lowest) first, then stops nearest-trigger
323    /// (highest) first, then pending unkeyed orders. Borrowed view; for an
324    /// owned snapshot use [`Self::get_orders_ask`].
325    pub fn iter_ask_orders(&self) -> impl Iterator<Item = &RestingOrder> {
326        self.ask_limits
327            .values()
328            .flat_map(|b| b.iter())
329            .chain(self.ask_stops.values().rev().flat_map(|b| b.iter()))
330            .chain(self.pending_ask.iter())
331    }
332
333    /// Iterates all orders without allocating, bids (best first) then asks
334    /// (best first). Borrowed view; for an owned snapshot use
335    /// [`Self::get_orders`].
336    pub fn iter_orders(&self) -> impl Iterator<Item = &RestingOrder> {
337        self.iter_bid_orders().chain(self.iter_ask_orders())
338    }
339
340    /// Returns the bid-side orders in price-time priority: limits best
341    /// (highest) first, then stops nearest-trigger (lowest) first, then
342    /// pending unkeyed orders. Allocates an owned snapshot; for borrowed
343    /// iteration use [`Self::iter_bid_orders`].
344    #[must_use]
345    pub fn get_orders_bid(&self) -> Vec<RestingOrder> {
346        self.iter_bid_orders().copied().collect()
347    }
348
349    /// Returns the ask-side orders in price-time priority: limits best
350    /// (lowest) first, then stops nearest-trigger (highest) first, then
351    /// pending unkeyed orders. Allocates an owned snapshot; for borrowed
352    /// iteration use [`Self::iter_ask_orders`].
353    #[must_use]
354    pub fn get_orders_ask(&self) -> Vec<RestingOrder> {
355        self.iter_ask_orders().copied().collect()
356    }
357
358    /// Returns the per-side book for the given `(side, kind)`.
359    fn book_for(&self, side: OrderSideSpecified, kind: BookKind) -> &BTreeMap<Price, OrderBucket> {
360        match (side, kind) {
361            (OrderSideSpecified::Buy, BookKind::Limit) => &self.bid_limits,
362            (OrderSideSpecified::Buy, BookKind::Stop) => &self.bid_stops,
363            (OrderSideSpecified::Sell, BookKind::Limit) => &self.ask_limits,
364            (OrderSideSpecified::Sell, BookKind::Stop) => &self.ask_stops,
365        }
366    }
367
368    /// Returns the per-side pending bucket.
369    fn pending_for(&self, side: OrderSideSpecified) -> &[RestingOrder] {
370        match side {
371            OrderSideSpecified::Buy => &self.pending_bid,
372            OrderSideSpecified::Sell => &self.pending_ask,
373        }
374    }
375
376    /// Returns all orders, bids (best first) then asks (best first).
377    /// Allocates an owned snapshot; for borrowed iteration use
378    /// [`Self::iter_orders`].
379    #[must_use]
380    pub fn get_orders(&self) -> Vec<RestingOrder> {
381        self.iter_orders().copied().collect()
382    }
383
384    /// Returns whether an order with `client_order_id` is present on either side.
385    #[must_use]
386    pub fn order_exists(&self, client_order_id: ClientOrderId) -> bool {
387        self.order_index.contains_key(&client_order_id)
388    }
389
390    /// Sets the last traded price.
391    pub const fn set_last_raw(&mut self, last: Price) {
392        self.last = Some(last);
393    }
394
395    /// Sets the best bid price.
396    pub const fn set_bid_raw(&mut self, bid: Price) {
397        self.bid = Some(bid);
398    }
399
400    /// Sets the best ask price.
401    pub const fn set_ask_raw(&mut self, ask: Price) {
402        self.ask = Some(ask);
403    }
404
405    /// Updates the price increment (tick size) for the matching core.
406    pub const fn update_price_increment(&mut self, price_increment: Price) {
407        self.price_increment = price_increment;
408    }
409
410    /// Clears all orders and resets bid/ask/last to uninitialized.
411    pub fn reset(&mut self) {
412        self.bid = None;
413        self.ask = None;
414        self.last = None;
415        self.bid_limits.clear();
416        self.ask_limits.clear();
417        self.bid_stops.clear();
418        self.ask_stops.clear();
419        self.pending_bid.clear();
420        self.pending_ask.clear();
421        self.order_index.clear();
422    }
423
424    /// Returns the (book kind, key) for an order, or `None` if the order has
425    /// neither limit nor trigger price (e.g. `MARKET_TO_LIMIT` pre-conversion).
426    fn locate(order: &RestingOrder) -> Option<(BookKind, Price)> {
427        if order.is_stop() {
428            // is_stop() == trigger_price.is_some()
429            Some((BookKind::Stop, order.trigger_price.unwrap()))
430        } else {
431            order.limit_price.map(|p| (BookKind::Limit, p))
432        }
433    }
434
435    /// Adds an order to the matching core.
436    ///
437    /// # Invariant
438    ///
439    /// Each `client_order_id` must appear at most once across all books.
440    /// To re-add an order under the same ID (e.g. a price-changing modify),
441    /// call [`Self::delete_order`] first. Inserting duplicates puts two entries
442    /// in the bucket and the order will match twice.
443    ///
444    /// Routing:
445    /// - `is_stop()` orders go to the side's stop book, keyed by trigger price.
446    /// - Pure `LIMIT` orders go to the side's limit book, keyed by limit price.
447    /// - Orders with neither price (e.g. `MARKET_TO_LIMIT` before conversion)
448    ///   go to the per-side pending bucket. They remain visible to `get_order`
449    ///   / `order_exists` but `iterate_*` skips them.
450    ///
451    /// # Panics
452    ///
453    /// Panics in debug builds if the invariant is violated.
454    pub fn add_order(&mut self, order: RestingOrder) {
455        debug_assert!(
456            !self.order_exists(order.client_order_id),
457            "duplicate add_order for {}; caller must delete before re-adding",
458            order.client_order_id,
459        );
460
461        let side = order.order_side;
462        let client_order_id = order.client_order_id;
463        let location = Self::locate(&order);
464
465        if let Some((kind, price)) = location {
466            let book = match (side, kind) {
467                (OrderSideSpecified::Buy, BookKind::Limit) => &mut self.bid_limits,
468                (OrderSideSpecified::Buy, BookKind::Stop) => &mut self.bid_stops,
469                (OrderSideSpecified::Sell, BookKind::Limit) => &mut self.ask_limits,
470                (OrderSideSpecified::Sell, BookKind::Stop) => &mut self.ask_stops,
471            };
472            book.entry(price).or_default().push(order);
473        } else {
474            match side {
475                OrderSideSpecified::Buy => self.pending_bid.push(order),
476                OrderSideSpecified::Sell => self.pending_ask.push(order),
477            }
478        }
479        self.order_index.insert(client_order_id, (side, location));
480    }
481
482    /// Deletes an order from the matching core by client order ID.
483    ///
484    /// # Errors
485    ///
486    /// Returns an [`OrderError::NotFound`] if the order is not present.
487    ///
488    /// # Panics
489    ///
490    /// Panics if the index points at a bucket that is missing or no longer
491    /// contains the expected order, indicating internal index corruption.
492    pub fn delete_order(&mut self, client_order_id: ClientOrderId) -> Result<(), OrderError> {
493        let Some((side, location)) = self.order_index.remove(&client_order_id) else {
494            return Err(OrderError::NotFound(client_order_id));
495        };
496
497        if let Some((kind, price)) = location {
498            let book = match (side, kind) {
499                (OrderSideSpecified::Buy, BookKind::Limit) => &mut self.bid_limits,
500                (OrderSideSpecified::Buy, BookKind::Stop) => &mut self.bid_stops,
501                (OrderSideSpecified::Sell, BookKind::Limit) => &mut self.ask_limits,
502                (OrderSideSpecified::Sell, BookKind::Stop) => &mut self.ask_stops,
503            };
504            let bucket = book
505                .get_mut(&price)
506                .expect("order_index points to existing bucket");
507            let pos = bucket
508                .iter()
509                .position(|o| o.client_order_id == client_order_id)
510                .expect("order_index points to existing slot");
511            bucket.remove(pos);
512            if bucket.is_empty() {
513                book.remove(&price);
514            }
515        } else {
516            let pending = match side {
517                OrderSideSpecified::Buy => &mut self.pending_bid,
518                OrderSideSpecified::Sell => &mut self.pending_ask,
519            };
520            let pos = pending
521                .iter()
522                .position(|o| o.client_order_id == client_order_id)
523                .expect("order_index points to existing pending slot");
524            pending.remove(pos);
525        }
526        Ok(())
527    }
528
529    /// Matches all bid then ask orders against the current market and returns
530    /// the resulting actions in price-time priority.
531    pub fn iterate(&self) -> Vec<MatchAction> {
532        let mut actions = self.iterate_bids();
533        actions.extend(self.iterate_asks());
534        actions
535    }
536
537    /// Matches bid-side orders: limits best (highest) first, then stops
538    /// nearest-trigger (lowest) first. FIFO within each price level.
539    pub fn iterate_bids(&self) -> Vec<MatchAction> {
540        self.bid_limits
541            .iter()
542            .rev()
543            .flat_map(|(_, b)| b.iter())
544            .chain(self.bid_stops.values().flat_map(|b| b.iter()))
545            .filter_map(|order| self.match_order(order))
546            .collect()
547    }
548
549    /// Matches ask-side orders: limits best (lowest) first, then stops
550    /// nearest-trigger (highest) first. FIFO within each price level.
551    pub fn iterate_asks(&self) -> Vec<MatchAction> {
552        self.ask_limits
553            .values()
554            .flat_map(|b| b.iter())
555            .chain(self.ask_stops.iter().rev().flat_map(|(_, b)| b.iter()))
556            .filter_map(|order| self.match_order(order))
557            .collect()
558    }
559
560    /// Returns a [`MatchAction`] if the order matches the current market,
561    /// or `None` if it does not (or has neither trigger nor limit price).
562    pub fn match_order(&self, order: &RestingOrder) -> Option<MatchAction> {
563        if order.is_stop() {
564            self.match_stop_order(order)
565        } else if order.is_limit() {
566            self.match_limit_order(order)
567        } else {
568            None
569        }
570    }
571
572    fn match_limit_order(&self, order: &RestingOrder) -> Option<MatchAction> {
573        if let Some(limit_price) = order.limit_price
574            && self.is_limit_fillable(order.order_side, limit_price)
575        {
576            Some(MatchAction::FillLimit(order.client_order_id))
577        } else {
578            None
579        }
580    }
581
582    fn match_stop_order(&self, order: &RestingOrder) -> Option<MatchAction> {
583        if !order.is_activated {
584            return None;
585        }
586
587        let trigger_price = order.trigger_price?;
588        let is_triggered = match order.order_type {
589            OrderType::MarketIfTouched | OrderType::LimitIfTouched => self
590                .is_touch_triggered_with_trigger_type(
591                    order.order_side,
592                    trigger_price,
593                    order.trigger_type,
594                ),
595            _ => self.is_stop_matched_with_trigger_type(
596                order.order_side,
597                trigger_price,
598                order.trigger_type,
599            ),
600        };
601
602        if is_triggered {
603            Some(MatchAction::TriggerStop(order.client_order_id))
604        } else {
605            None
606        }
607    }
608
609    /// Returns whether a limit order at `price` would cross the opposite side
610    /// (BUY: `ask <= price`, SELL: `bid >= price`).
611    #[must_use]
612    pub fn is_limit_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
613        match side {
614            OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= price),
615            OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= price),
616        }
617    }
618
619    /// Returns whether a stop trigger at `price` has been reached
620    /// (BUY: `ask >= price`, SELL: `bid <= price`).
621    #[must_use]
622    pub fn is_stop_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
623        self.is_stop_matched_with_trigger_type(side, price, TriggerType::BidAsk)
624    }
625
626    #[must_use]
627    pub(crate) fn is_stop_matched_with_trigger_type(
628        &self,
629        side: OrderSideSpecified,
630        price: Price,
631        trigger_type: TriggerType,
632    ) -> bool {
633        self.market_price_for_trigger(side, trigger_type)
634            .is_some_and(|market_price| match side {
635                OrderSideSpecified::Buy => market_price >= price,
636                OrderSideSpecified::Sell => market_price <= price,
637            })
638    }
639
640    /// Returns whether a touch trigger at `trigger_price` has been reached
641    /// (BUY: `ask <= trigger_price`, SELL: `bid >= trigger_price`).
642    #[must_use]
643    pub fn is_touch_triggered(&self, side: OrderSideSpecified, trigger_price: Price) -> bool {
644        self.is_touch_triggered_with_trigger_type(side, trigger_price, TriggerType::BidAsk)
645    }
646
647    #[must_use]
648    pub(crate) fn is_touch_triggered_with_trigger_type(
649        &self,
650        side: OrderSideSpecified,
651        trigger_price: Price,
652        trigger_type: TriggerType,
653    ) -> bool {
654        self.market_price_for_trigger(side, trigger_type)
655            .is_some_and(|market_price| match side {
656                OrderSideSpecified::Buy => market_price <= trigger_price,
657                OrderSideSpecified::Sell => market_price >= trigger_price,
658            })
659    }
660
661    fn market_price_for_trigger(
662        &self,
663        side: OrderSideSpecified,
664        trigger_type: TriggerType,
665    ) -> Option<Price> {
666        let quote_price = match side {
667            OrderSideSpecified::Buy => self.ask,
668            OrderSideSpecified::Sell => self.bid,
669        };
670
671        match trigger_type {
672            TriggerType::LastPrice => self.last,
673            TriggerType::LastOrBidAsk => self.last.or(quote_price),
674            _ => quote_price,
675        }
676    }
677
678    /// Toggles whether limit orders fill at-or-inside the spread (vs only on cross).
679    pub fn set_fill_limit_inside_spread(&mut self, value: bool) {
680        self.fill_limit_inside_spread = value;
681    }
682
683    /// Returns whether a limit order is fillable at the given price.
684    ///
685    /// Checks `is_limit_matched` first (crosses the spread). When
686    /// `fill_limit_inside_spread` is set, also checks at-or-inside spread
687    /// (BUY >= bid, SELL <= ask), requiring both sides initialized.
688    #[must_use]
689    pub fn is_limit_fillable(&self, side: OrderSideSpecified, price: Price) -> bool {
690        if self.is_limit_matched(side, price) {
691            return true;
692        }
693
694        if !self.fill_limit_inside_spread {
695            return false;
696        }
697
698        // Require both quotes present since fill simulation needs best bid and ask
699        if let (Some(bid), Some(ask)) = (self.bid, self.ask) {
700            match side {
701                OrderSideSpecified::Buy => price >= bid,
702                OrderSideSpecified::Sell => price <= ask,
703            }
704        } else {
705            false
706        }
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use nautilus_model::{
713        enums::{OrderSide, OrderType, TrailingOffsetType, TriggerType},
714        events::{OrderEventAny, OrderInitialized, order::spec::OrderInitializedSpec},
715        orders::{Order, OrderAny, builder::OrderTestBuilder},
716        types::Quantity,
717    };
718    use rstest::rstest;
719    use rust_decimal::Decimal;
720
721    use super::*;
722
723    fn create_matching_core(
724        instrument_id: InstrumentId,
725        price_increment: Price,
726    ) -> OrderMatchingCore {
727        OrderMatchingCore::new(instrument_id, price_increment)
728    }
729
730    #[rstest]
731    fn test_add_order_bid_side() {
732        let instrument_id = InstrumentId::from("AAPL.XNAS");
733        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
734
735        let order = OrderTestBuilder::new(OrderType::Limit)
736            .instrument_id(instrument_id)
737            .side(OrderSide::Buy)
738            .price(Price::from("100.00"))
739            .quantity(Quantity::from("100"))
740            .build();
741
742        let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
743        matching_core.add_order(match_info);
744
745        assert!(matching_core.get_orders_bid().contains(&match_info));
746        assert!(!matching_core.get_orders_ask().contains(&match_info));
747        assert_eq!(matching_core.get_orders_bid().len(), 1);
748        assert!(matching_core.get_orders_ask().is_empty());
749        assert!(matching_core.order_exists(match_info.client_order_id));
750    }
751
752    #[rstest]
753    fn test_add_order_ask_side() {
754        let instrument_id = InstrumentId::from("AAPL.XNAS");
755        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
756
757        let order = OrderTestBuilder::new(OrderType::Limit)
758            .instrument_id(instrument_id)
759            .side(OrderSide::Sell)
760            .price(Price::from("100.00"))
761            .quantity(Quantity::from("100"))
762            .build();
763
764        let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
765        matching_core.add_order(match_info);
766
767        assert!(matching_core.get_orders_ask().contains(&match_info));
768        assert!(!matching_core.get_orders_bid().contains(&match_info));
769        assert_eq!(matching_core.get_orders_ask().len(), 1);
770        assert!(matching_core.get_orders_bid().is_empty());
771        assert!(matching_core.order_exists(match_info.client_order_id));
772    }
773
774    #[rstest]
775    fn test_reset() {
776        let instrument_id = InstrumentId::from("AAPL.XNAS");
777        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
778
779        let order = OrderTestBuilder::new(OrderType::Limit)
780            .instrument_id(instrument_id)
781            .side(OrderSide::Sell)
782            .price(Price::from("100.00"))
783            .quantity(Quantity::from("100"))
784            .build();
785
786        let client_order_id = order.client_order_id();
787        let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
788        matching_core.add_order(match_info);
789        matching_core.set_bid_raw(Price::from("100.00"));
790        matching_core.set_ask_raw(Price::from("100.00"));
791        matching_core.set_last_raw(Price::from("100.00"));
792
793        matching_core.reset();
794
795        assert!(matching_core.bid.is_none());
796        assert!(matching_core.ask.is_none());
797        assert!(matching_core.last.is_none());
798        assert!(matching_core.get_orders_bid().is_empty());
799        assert!(matching_core.get_orders_ask().is_empty());
800        assert!(!matching_core.order_exists(client_order_id));
801    }
802
803    #[rstest]
804    fn test_delete_order_when_not_exists() {
805        let instrument_id = InstrumentId::from("AAPL.XNAS");
806        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
807
808        let order = OrderTestBuilder::new(OrderType::Limit)
809            .instrument_id(instrument_id)
810            .side(OrderSide::Buy)
811            .price(Price::from("100.00"))
812            .quantity(Quantity::from("100"))
813            .build();
814
815        let result = matching_core.delete_order(order.client_order_id());
816        assert!(result.is_err());
817    }
818
819    #[rstest]
820    #[case(OrderSide::Buy)]
821    #[case(OrderSide::Sell)]
822    fn test_delete_order_when_exists(#[case] order_side: OrderSide) {
823        let instrument_id = InstrumentId::from("AAPL.XNAS");
824        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
825
826        let order = OrderTestBuilder::new(OrderType::Limit)
827            .instrument_id(instrument_id)
828            .side(order_side)
829            .price(Price::from("100.00"))
830            .quantity(Quantity::from("100"))
831            .build();
832
833        let client_order_id = order.client_order_id();
834        let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
835        matching_core.add_order(match_info);
836        matching_core.delete_order(client_order_id).unwrap();
837
838        assert!(matching_core.get_orders_ask().is_empty());
839        assert!(matching_core.get_orders_bid().is_empty());
840    }
841
842    #[rstest]
843    #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
844    #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
845    #[case(
846        Some(Price::from("100.00")),
847        Some(Price::from("101.00")),
848        Price::from("100.00"),  // <-- Price below ask
849        OrderSide::Buy,
850        false
851    )]
852    #[case(
853        Some(Price::from("100.00")),
854        Some(Price::from("101.00")),
855        Price::from("101.00"),  // <-- Price at ask
856        OrderSide::Buy,
857        true
858    )]
859    #[case(
860        Some(Price::from("100.00")),
861        Some(Price::from("101.00")),
862        Price::from("102.00"),  // <-- Price above ask (marketable)
863        OrderSide::Buy,
864        true
865    )]
866    #[case(
867        Some(Price::from("100.00")),
868        Some(Price::from("101.00")),
869        Price::from("101.00"), // <-- Price above bid
870        OrderSide::Sell,
871        false
872    )]
873    #[case(
874        Some(Price::from("100.00")),
875        Some(Price::from("101.00")),
876        Price::from("100.00"),  // <-- Price at bid
877        OrderSide::Sell,
878        true
879    )]
880    #[case(
881        Some(Price::from("100.00")),
882        Some(Price::from("101.00")),
883        Price::from("99.00"),  // <-- Price below bid (marketable)
884        OrderSide::Sell,
885        true
886    )]
887    fn test_is_limit_matched(
888        #[case] bid: Option<Price>,
889        #[case] ask: Option<Price>,
890        #[case] price: Price,
891        #[case] order_side: OrderSide,
892        #[case] expected: bool,
893    ) {
894        let instrument_id = InstrumentId::from("AAPL.XNAS");
895        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
896        matching_core.bid = bid;
897        matching_core.ask = ask;
898
899        let order = OrderTestBuilder::new(OrderType::Limit)
900            .instrument_id(instrument_id)
901            .side(order_side)
902            .price(price)
903            .quantity(Quantity::from("100"))
904            .build();
905
906        let result =
907            matching_core.is_limit_matched(order.order_side_specified(), order.price().unwrap());
908        assert_eq!(result, expected);
909    }
910
911    #[rstest]
912    #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
913    #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
914    #[case(
915        Some(Price::from("100.00")),
916        Some(Price::from("101.00")),
917        Price::from("102.00"),  // <-- Trigger above ask
918        OrderSide::Buy,
919        false
920    )]
921    #[case(
922        Some(Price::from("100.00")),
923        Some(Price::from("101.00")),
924        Price::from("101.00"),  // <-- Trigger at ask
925        OrderSide::Buy,
926        true
927    )]
928    #[case(
929        Some(Price::from("100.00")),
930        Some(Price::from("101.00")),
931        Price::from("100.00"),  // <-- Trigger below ask
932        OrderSide::Buy,
933        true
934    )]
935    #[case(
936        Some(Price::from("100.00")),
937        Some(Price::from("101.00")),
938        Price::from("99.00"),  // Trigger below bid
939        OrderSide::Sell,
940        false
941    )]
942    #[case(
943        Some(Price::from("100.00")),
944        Some(Price::from("101.00")),
945        Price::from("100.00"),  // <-- Trigger at bid
946        OrderSide::Sell,
947        true
948    )]
949    #[case(
950        Some(Price::from("100.00")),
951        Some(Price::from("101.00")),
952        Price::from("101.00"),  // <-- Trigger above bid
953        OrderSide::Sell,
954        true
955    )]
956    fn test_is_stop_matched(
957        #[case] bid: Option<Price>,
958        #[case] ask: Option<Price>,
959        #[case] trigger_price: Price,
960        #[case] order_side: OrderSide,
961        #[case] expected: bool,
962    ) {
963        let instrument_id = InstrumentId::from("AAPL.XNAS");
964        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
965        matching_core.bid = bid;
966        matching_core.ask = ask;
967
968        let order = OrderTestBuilder::new(OrderType::StopMarket)
969            .instrument_id(instrument_id)
970            .side(order_side)
971            .trigger_price(trigger_price)
972            .quantity(Quantity::from("100"))
973            .build();
974
975        let result = matching_core
976            .is_stop_matched(order.order_side_specified(), order.trigger_price().unwrap());
977        assert_eq!(result, expected);
978    }
979
980    #[rstest]
981    #[case::last_price_below_trigger(Some(Price::from("99.00")), TriggerType::LastPrice, false)]
982    #[case::last_price_at_trigger(Some(Price::from("100.00")), TriggerType::LastPrice, true)]
983    #[case::last_price_unavailable(None, TriggerType::LastPrice, false)]
984    #[case::last_or_bid_ask_prefers_last(
985        Some(Price::from("99.00")),
986        TriggerType::LastOrBidAsk,
987        false
988    )]
989    #[case::last_or_bid_ask_falls_back_to_quote(None, TriggerType::LastOrBidAsk, true)]
990    #[case::bid_ask_uses_quote(Some(Price::from("99.00")), TriggerType::BidAsk, true)]
991    fn test_is_stop_matched_uses_trigger_type(
992        #[case] last: Option<Price>,
993        #[case] trigger_type: TriggerType,
994        #[case] expected: bool,
995    ) {
996        let instrument_id = InstrumentId::from("AAPL.XNAS");
997        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
998        matching_core.ask = Some(Price::from("101.00"));
999        matching_core.last = last;
1000
1001        let result = matching_core.is_stop_matched_with_trigger_type(
1002            OrderSideSpecified::Buy,
1003            Price::from("100.00"),
1004            trigger_type,
1005        );
1006
1007        assert_eq!(result, expected);
1008    }
1009
1010    #[rstest]
1011    fn test_iterate_returns_empty_when_no_orders() {
1012        let instrument_id = InstrumentId::from("AAPL.XNAS");
1013        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1014        matching_core.set_bid_raw(Price::from("100.00"));
1015        matching_core.set_ask_raw(Price::from("101.00"));
1016
1017        let actions = matching_core.iterate();
1018
1019        assert!(actions.is_empty());
1020    }
1021
1022    #[rstest]
1023    fn test_iterate_returns_empty_when_no_market_data() {
1024        let instrument_id = InstrumentId::from("AAPL.XNAS");
1025        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1026
1027        let order = OrderTestBuilder::new(OrderType::Limit)
1028            .instrument_id(instrument_id)
1029            .side(OrderSide::Buy)
1030            .price(Price::from("100.00"))
1031            .quantity(Quantity::from("100"))
1032            .build();
1033        let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1034        matching_core.add_order(match_info);
1035
1036        let actions = matching_core.iterate();
1037
1038        assert!(actions.is_empty());
1039    }
1040
1041    #[rstest]
1042    fn test_iterate_returns_fill_limit_for_matched_buy() {
1043        let instrument_id = InstrumentId::from("AAPL.XNAS");
1044        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1045        matching_core.set_ask_raw(Price::from("100.00"));
1046
1047        let order = OrderTestBuilder::new(OrderType::Limit)
1048            .instrument_id(instrument_id)
1049            .side(OrderSide::Buy)
1050            .price(Price::from("100.00"))
1051            .quantity(Quantity::from("100"))
1052            .build();
1053        let client_order_id = order.client_order_id();
1054        let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1055        matching_core.add_order(match_info);
1056
1057        let actions = matching_core.iterate();
1058
1059        assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
1060    }
1061
1062    #[rstest]
1063    fn test_iterate_returns_fill_limit_for_matched_sell() {
1064        let instrument_id = InstrumentId::from("AAPL.XNAS");
1065        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1066        matching_core.set_bid_raw(Price::from("100.00"));
1067
1068        let order = OrderTestBuilder::new(OrderType::Limit)
1069            .instrument_id(instrument_id)
1070            .side(OrderSide::Sell)
1071            .price(Price::from("100.00"))
1072            .quantity(Quantity::from("100"))
1073            .build();
1074        let client_order_id = order.client_order_id();
1075        let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1076        matching_core.add_order(match_info);
1077
1078        let actions = matching_core.iterate();
1079
1080        assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
1081    }
1082
1083    #[rstest]
1084    fn test_iterate_returns_no_fill_for_unmatched_limit() {
1085        let instrument_id = InstrumentId::from("AAPL.XNAS");
1086        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1087        matching_core.set_ask_raw(Price::from("101.00"));
1088
1089        // Buy limit at 100 with ask at 101 - not matched
1090        let order = OrderTestBuilder::new(OrderType::Limit)
1091            .instrument_id(instrument_id)
1092            .side(OrderSide::Buy)
1093            .price(Price::from("100.00"))
1094            .quantity(Quantity::from("100"))
1095            .build();
1096        let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1097        matching_core.add_order(match_info);
1098
1099        let actions = matching_core.iterate();
1100
1101        assert!(actions.is_empty());
1102    }
1103
1104    #[rstest]
1105    fn test_iterate_returns_trigger_stop_for_matched_buy() {
1106        let instrument_id = InstrumentId::from("AAPL.XNAS");
1107        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1108        matching_core.set_ask_raw(Price::from("101.00"));
1109
1110        let order = OrderTestBuilder::new(OrderType::StopMarket)
1111            .instrument_id(instrument_id)
1112            .side(OrderSide::Buy)
1113            .trigger_price(Price::from("101.00"))
1114            .trigger_type(TriggerType::Default)
1115            .quantity(Quantity::from("100"))
1116            .build();
1117        let client_order_id = order.client_order_id();
1118        let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1119        matching_core.add_order(match_info);
1120
1121        let actions = matching_core.iterate();
1122
1123        assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
1124    }
1125
1126    #[rstest]
1127    fn test_iterate_returns_trigger_stop_for_matched_sell() {
1128        let instrument_id = InstrumentId::from("AAPL.XNAS");
1129        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1130        matching_core.set_bid_raw(Price::from("99.00"));
1131
1132        let order = OrderTestBuilder::new(OrderType::StopMarket)
1133            .instrument_id(instrument_id)
1134            .side(OrderSide::Sell)
1135            .trigger_price(Price::from("99.00"))
1136            .quantity(Quantity::from("100"))
1137            .build();
1138        let client_order_id = order.client_order_id();
1139        let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1140        matching_core.add_order(match_info);
1141
1142        let actions = matching_core.iterate();
1143
1144        assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
1145    }
1146
1147    #[rstest]
1148    fn test_iterate_skips_unactivated_stop_order() {
1149        let instrument_id = InstrumentId::from("AAPL.XNAS");
1150        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1151        matching_core.set_ask_raw(Price::from("110.00"));
1152
1153        // Manually create an unactivated stop (simulates trailing stop)
1154        let match_info = RestingOrder::new(
1155            ClientOrderId::from("O-001"),
1156            OrderSideSpecified::Buy,
1157            OrderType::TrailingStopMarket,
1158            Some(Price::from("105.00")),
1159            None,
1160            false, // not activated
1161        );
1162        matching_core.add_order(match_info);
1163
1164        let actions = matching_core.iterate();
1165
1166        assert!(actions.is_empty());
1167    }
1168
1169    #[rstest]
1170    fn test_iterate_triggers_activated_stop_order() {
1171        let instrument_id = InstrumentId::from("AAPL.XNAS");
1172        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1173        matching_core.set_ask_raw(Price::from("110.00"));
1174
1175        let client_order_id = ClientOrderId::from("O-001");
1176        let match_info = RestingOrder::new(
1177            client_order_id,
1178            OrderSideSpecified::Buy,
1179            OrderType::TrailingStopMarket,
1180            Some(Price::from("105.00")),
1181            None,
1182            true, // activated
1183        );
1184        matching_core.add_order(match_info);
1185
1186        let actions = matching_core.iterate();
1187
1188        assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
1189    }
1190
1191    #[rstest]
1192    fn test_iterate_returns_mixed_actions_for_limits_and_stops() {
1193        let instrument_id = InstrumentId::from("AAPL.XNAS");
1194        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1195        matching_core.set_bid_raw(Price::from("99.00"));
1196        matching_core.set_ask_raw(Price::from("101.00"));
1197
1198        // Buy limit at 101 - matches (ask <= price)
1199        let buy_limit = OrderTestBuilder::new(OrderType::Limit)
1200            .instrument_id(instrument_id)
1201            .side(OrderSide::Buy)
1202            .price(Price::from("101.00"))
1203            .quantity(Quantity::from("100"))
1204            .client_order_id(ClientOrderId::from("O-BUY-LIMIT"))
1205            .build();
1206        let buy_limit_id = buy_limit.client_order_id();
1207        matching_core.add_order(RestingOrder::from(
1208            &PassiveOrderAny::try_from(buy_limit).unwrap(),
1209        ));
1210
1211        // Sell stop at 99 - matches (bid <= trigger)
1212        let sell_stop = OrderTestBuilder::new(OrderType::StopMarket)
1213            .instrument_id(instrument_id)
1214            .side(OrderSide::Sell)
1215            .trigger_price(Price::from("99.00"))
1216            .quantity(Quantity::from("50"))
1217            .client_order_id(ClientOrderId::from("O-SELL-STOP"))
1218            .build();
1219        let sell_stop_id = sell_stop.client_order_id();
1220        matching_core.add_order(RestingOrder::from(
1221            &PassiveOrderAny::try_from(sell_stop).unwrap(),
1222        ));
1223
1224        let actions = matching_core.iterate();
1225
1226        // Bids processed first, then asks
1227        assert_eq!(actions.len(), 2);
1228        assert_eq!(actions[0], MatchAction::FillLimit(buy_limit_id));
1229        assert_eq!(actions[1], MatchAction::TriggerStop(sell_stop_id));
1230    }
1231
1232    #[rstest]
1233    fn test_is_limit_fillable_delegates_to_is_limit_matched_by_default() {
1234        let instrument_id = InstrumentId::from("AAPL.XNAS");
1235        let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1236        core.set_bid_raw(Price::from("100.00"));
1237        core.set_ask_raw(Price::from("101.00"));
1238
1239        assert!(core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("101.00")));
1240        assert!(!core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
1241        assert!(core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("100.00")));
1242        assert!(!core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("101.00")));
1243    }
1244
1245    #[rstest]
1246    fn test_is_limit_fillable_inside_spread_buy_at_bid() {
1247        let instrument_id = InstrumentId::from("AAPL.XNAS");
1248        let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1249        core.set_bid_raw(Price::from("100.00"));
1250        core.set_ask_raw(Price::from("101.00"));
1251        core.set_fill_limit_inside_spread(true);
1252
1253        assert!(core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
1254        assert!(core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.50")));
1255        assert!(!core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("99.00")));
1256    }
1257
1258    #[rstest]
1259    fn test_is_limit_fillable_inside_spread_sell_at_ask() {
1260        let instrument_id = InstrumentId::from("AAPL.XNAS");
1261        let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1262        core.set_bid_raw(Price::from("100.00"));
1263        core.set_ask_raw(Price::from("101.00"));
1264        core.set_fill_limit_inside_spread(true);
1265
1266        assert!(core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("101.00")));
1267        assert!(core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("100.50")));
1268        assert!(!core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("102.00")));
1269    }
1270
1271    #[rstest]
1272    fn test_is_limit_fillable_inside_spread_requires_both_quotes_present() {
1273        let instrument_id = InstrumentId::from("AAPL.XNAS");
1274        let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1275        core.set_fill_limit_inside_spread(true);
1276
1277        core.set_bid_raw(Price::from("100.00"));
1278        assert!(!core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
1279
1280        let mut core2 = create_matching_core(instrument_id, Price::from("0.01"));
1281        core2.set_fill_limit_inside_spread(true);
1282        core2.set_ask_raw(Price::from("101.00"));
1283        assert!(!core2.is_limit_fillable(OrderSideSpecified::Sell, Price::from("101.00")));
1284
1285        // Ask cleared after both were set
1286        let mut core3 = create_matching_core(instrument_id, Price::from("0.01"));
1287        core3.set_fill_limit_inside_spread(true);
1288        core3.set_bid_raw(Price::from("100.00"));
1289        core3.set_ask_raw(Price::from("101.00"));
1290        core3.ask = None;
1291        assert!(!core3.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
1292    }
1293
1294    #[rstest]
1295    fn test_iterate_fills_limit_inside_spread_when_enabled() {
1296        let instrument_id = InstrumentId::from("AAPL.XNAS");
1297        let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1298        core.set_bid_raw(Price::from("100.00"));
1299        core.set_ask_raw(Price::from("101.00"));
1300        core.set_fill_limit_inside_spread(true);
1301
1302        let order = OrderTestBuilder::new(OrderType::Limit)
1303            .instrument_id(instrument_id)
1304            .side(OrderSide::Buy)
1305            .price(Price::from("100.00"))
1306            .quantity(Quantity::from("100"))
1307            .build();
1308        let client_order_id = order.client_order_id();
1309        let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1310        core.add_order(match_info);
1311
1312        let actions = core.iterate();
1313        assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
1314    }
1315
1316    #[rstest]
1317    #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
1318    #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
1319    #[case(
1320        Some(Price::from("100.00")),
1321        Some(Price::from("101.00")),
1322        Price::from("102.00"),  // <-- Ask below trigger
1323        OrderSide::Buy,
1324        true
1325    )]
1326    #[case(
1327        Some(Price::from("100.00")),
1328        Some(Price::from("101.00")),
1329        Price::from("101.00"),  // <-- Ask at trigger
1330        OrderSide::Buy,
1331        true
1332    )]
1333    #[case(
1334        Some(Price::from("100.00")),
1335        Some(Price::from("101.00")),
1336        Price::from("100.00"),  // <-- Ask above trigger
1337        OrderSide::Buy,
1338        false
1339    )]
1340    #[case(
1341        Some(Price::from("100.00")),
1342        Some(Price::from("101.00")),
1343        Price::from("99.00"),  // <-- Bid above trigger
1344        OrderSide::Sell,
1345        true
1346    )]
1347    #[case(
1348        Some(Price::from("100.00")),
1349        Some(Price::from("101.00")),
1350        Price::from("100.00"),  // <-- Bid at trigger
1351        OrderSide::Sell,
1352        true
1353    )]
1354    #[case(
1355        Some(Price::from("100.00")),
1356        Some(Price::from("101.00")),
1357        Price::from("101.00"),  // <-- Bid below trigger
1358        OrderSide::Sell,
1359        false
1360    )]
1361    fn test_is_touch_triggered(
1362        #[case] bid: Option<Price>,
1363        #[case] ask: Option<Price>,
1364        #[case] trigger_price: Price,
1365        #[case] order_side: OrderSide,
1366        #[case] expected: bool,
1367    ) {
1368        let instrument_id = InstrumentId::from("AAPL.XNAS");
1369        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1370        matching_core.bid = bid;
1371        matching_core.ask = ask;
1372
1373        let result = matching_core.is_touch_triggered(order_side.as_specified(), trigger_price);
1374        assert_eq!(result, expected);
1375    }
1376
1377    #[rstest]
1378    fn test_update_price_increment_updates_increment_and_precision() {
1379        let instrument_id = InstrumentId::from("AAPL.XNAS");
1380        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1381
1382        assert_eq!(matching_core.price_increment, Price::from("0.01"));
1383        assert_eq!(matching_core.price_precision(), 2);
1384
1385        matching_core.update_price_increment(Price::from("0.001"));
1386
1387        assert_eq!(matching_core.price_increment, Price::from("0.001"));
1388        assert_eq!(matching_core.price_precision(), 3);
1389    }
1390
1391    fn order_from_init(spec: OrderInitialized) -> OrderAny {
1392        OrderAny::from_events(vec![OrderEventAny::Initialized(spec)]).unwrap()
1393    }
1394
1395    #[rstest]
1396    fn test_get_order_finds_orders_on_either_side() {
1397        let instrument_id = InstrumentId::from("AAPL.XNAS");
1398        let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1399
1400        let buy = order_from_init(
1401            OrderInitializedSpec::builder()
1402                .instrument_id(instrument_id)
1403                .client_order_id(ClientOrderId::from("O-BUY"))
1404                .order_side(OrderSide::Buy)
1405                .order_type(OrderType::Limit)
1406                .quantity(Quantity::from("10"))
1407                .price(Price::from("100.00"))
1408                .build(),
1409        );
1410        let buy_id = buy.client_order_id();
1411        core.add_order(RestingOrder::from(&PassiveOrderAny::try_from(buy).unwrap()));
1412
1413        let sell = order_from_init(
1414            OrderInitializedSpec::builder()
1415                .instrument_id(instrument_id)
1416                .client_order_id(ClientOrderId::from("O-SELL"))
1417                .order_side(OrderSide::Sell)
1418                .order_type(OrderType::Limit)
1419                .quantity(Quantity::from("10"))
1420                .price(Price::from("101.00"))
1421                .build(),
1422        );
1423        let sell_id = sell.client_order_id();
1424        core.add_order(RestingOrder::from(
1425            &PassiveOrderAny::try_from(sell).unwrap(),
1426        ));
1427
1428        assert_eq!(
1429            core.get_order(buy_id).map(|o| o.client_order_id),
1430            Some(buy_id)
1431        );
1432        assert_eq!(
1433            core.get_order(sell_id).map(|o| o.client_order_id),
1434            Some(sell_id)
1435        );
1436        assert!(core.get_order(ClientOrderId::from("O-MISSING")).is_none());
1437    }
1438
1439    #[rstest]
1440    fn test_match_order_returns_none_when_neither_price_set() {
1441        // MARKET_TO_LIMIT and any caller-built `RestingOrder::new` with both
1442        // prices `None` must no-op rather than dispatch to a match function.
1443        let instrument_id = InstrumentId::from("AAPL.XNAS");
1444        let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1445        core.set_bid_raw(Price::from("100.00"));
1446        core.set_ask_raw(Price::from("101.00"));
1447
1448        let info = RestingOrder::new(
1449            ClientOrderId::from("O-NEITHER"),
1450            OrderSideSpecified::Buy,
1451            OrderType::MarketToLimit,
1452            None,
1453            None,
1454            true,
1455        );
1456        assert!(core.match_order(&info).is_none());
1457    }
1458
1459    #[rstest]
1460    fn test_from_passive_order_extracts_limit_price_for_stop_limit() {
1461        let order = order_from_init(
1462            OrderInitializedSpec::builder()
1463                .order_type(OrderType::StopLimit)
1464                .order_side(OrderSide::Buy)
1465                .quantity(Quantity::from("10"))
1466                .price(Price::from("101.00"))
1467                .trigger_price(Price::from("100.00"))
1468                .trigger_type(TriggerType::Default)
1469                .build(),
1470        );
1471
1472        let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1473
1474        assert_eq!(info.trigger_price, Some(Price::from("100.00")));
1475        assert_eq!(info.limit_price, Some(Price::from("101.00")));
1476        assert!(info.is_activated);
1477    }
1478
1479    #[rstest]
1480    fn test_from_passive_order_extracts_limit_price_for_limit_if_touched() {
1481        let order = order_from_init(
1482            OrderInitializedSpec::builder()
1483                .order_type(OrderType::LimitIfTouched)
1484                .order_side(OrderSide::Sell)
1485                .quantity(Quantity::from("10"))
1486                .price(Price::from("99.00"))
1487                .trigger_price(Price::from("100.00"))
1488                .trigger_type(TriggerType::LastPrice)
1489                .build(),
1490        );
1491
1492        let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1493
1494        assert_eq!(info.trigger_price, Some(Price::from("100.00")));
1495        assert_eq!(info.limit_price, Some(Price::from("99.00")));
1496        assert_eq!(info.trigger_type, TriggerType::LastPrice);
1497        assert!(info.is_activated);
1498    }
1499
1500    #[rstest]
1501    fn test_from_passive_order_extracts_is_activated_for_trailing_stop_market() {
1502        let order = order_from_init(
1503            OrderInitializedSpec::builder()
1504                .order_type(OrderType::TrailingStopMarket)
1505                .order_side(OrderSide::Buy)
1506                .quantity(Quantity::from("10"))
1507                .trigger_price(Price::from("101.00"))
1508                .trigger_type(TriggerType::Default)
1509                .trailing_offset(Decimal::from(1))
1510                .trailing_offset_type(TrailingOffsetType::Price)
1511                .build(),
1512        );
1513
1514        let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1515
1516        assert_eq!(info.trigger_price, Some(Price::from("101.00")));
1517        assert_eq!(info.limit_price, None);
1518        // TrailingStopMarket starts unactivated until the trigger has been seen.
1519        assert!(!info.is_activated);
1520    }
1521
1522    #[rstest]
1523    fn test_from_passive_order_extracts_limit_and_is_activated_for_trailing_stop_limit() {
1524        let order = order_from_init(
1525            OrderInitializedSpec::builder()
1526                .order_type(OrderType::TrailingStopLimit)
1527                .order_side(OrderSide::Sell)
1528                .quantity(Quantity::from("10"))
1529                .price(Price::from("99.00"))
1530                .trigger_price(Price::from("100.00"))
1531                .trigger_type(TriggerType::Default)
1532                .limit_offset(Decimal::from(1))
1533                .trailing_offset(Decimal::from(1))
1534                .trailing_offset_type(TrailingOffsetType::Price)
1535                .build(),
1536        );
1537
1538        let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1539
1540        assert_eq!(info.trigger_price, Some(Price::from("100.00")));
1541        assert_eq!(info.limit_price, Some(Price::from("99.00")));
1542        assert!(!info.is_activated);
1543    }
1544
1545    // -- Book layout & iteration ordering ---------------------------------
1546
1547    fn limit_order(side: OrderSide, price: &str, id: &str) -> RestingOrder {
1548        let order = order_from_init(
1549            OrderInitializedSpec::builder()
1550                .client_order_id(ClientOrderId::from(id))
1551                .order_type(OrderType::Limit)
1552                .order_side(side)
1553                .quantity(Quantity::from("10"))
1554                .price(Price::from(price))
1555                .build(),
1556        );
1557        RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap())
1558    }
1559
1560    fn stop_order(side: OrderSide, trigger: &str, id: &str) -> RestingOrder {
1561        let order = order_from_init(
1562            OrderInitializedSpec::builder()
1563                .client_order_id(ClientOrderId::from(id))
1564                .order_type(OrderType::StopMarket)
1565                .order_side(side)
1566                .quantity(Quantity::from("10"))
1567                .trigger_price(Price::from(trigger))
1568                .trigger_type(TriggerType::Default)
1569                .build(),
1570        );
1571        RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap())
1572    }
1573
1574    fn stop_limit_order(side: OrderSide, trigger: &str, limit: &str, id: &str) -> RestingOrder {
1575        let order = order_from_init(
1576            OrderInitializedSpec::builder()
1577                .client_order_id(ClientOrderId::from(id))
1578                .order_type(OrderType::StopLimit)
1579                .order_side(side)
1580                .quantity(Quantity::from("10"))
1581                .price(Price::from(limit))
1582                .trigger_price(Price::from(trigger))
1583                .trigger_type(TriggerType::Default)
1584                .build(),
1585        );
1586        RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap())
1587    }
1588
1589    #[rstest]
1590    fn test_iterate_bids_returns_limits_in_descending_price_order() {
1591        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1592        core.set_ask_raw(Price::from("99.00"));
1593
1594        // Add intentionally out-of-price-order to verify the BTreeMap re-sorts.
1595        core.add_order(limit_order(OrderSide::Buy, "100.00", "O-MID"));
1596        core.add_order(limit_order(OrderSide::Buy, "100.50", "O-HIGH"));
1597        core.add_order(limit_order(OrderSide::Buy, "99.50", "O-LOW"));
1598
1599        let actions = core.iterate_bids();
1600        assert_eq!(
1601            actions,
1602            vec![
1603                MatchAction::FillLimit(ClientOrderId::from("O-HIGH")),
1604                MatchAction::FillLimit(ClientOrderId::from("O-MID")),
1605                MatchAction::FillLimit(ClientOrderId::from("O-LOW")),
1606            ],
1607        );
1608    }
1609
1610    #[rstest]
1611    fn test_iterate_asks_returns_limits_in_ascending_price_order() {
1612        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1613        core.set_bid_raw(Price::from("101.00"));
1614
1615        core.add_order(limit_order(OrderSide::Sell, "100.50", "O-MID"));
1616        core.add_order(limit_order(OrderSide::Sell, "100.00", "O-LOW"));
1617        core.add_order(limit_order(OrderSide::Sell, "100.75", "O-HIGH"));
1618
1619        let actions = core.iterate_asks();
1620        assert_eq!(
1621            actions,
1622            vec![
1623                MatchAction::FillLimit(ClientOrderId::from("O-LOW")),
1624                MatchAction::FillLimit(ClientOrderId::from("O-MID")),
1625                MatchAction::FillLimit(ClientOrderId::from("O-HIGH")),
1626            ],
1627        );
1628    }
1629
1630    #[rstest]
1631    fn test_iterate_limits_preserves_fifo_within_same_price() {
1632        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1633        core.set_ask_raw(Price::from("99.00"));
1634
1635        for id in ["O-1", "O-2", "O-3", "O-4"] {
1636            core.add_order(limit_order(OrderSide::Buy, "100.00", id));
1637        }
1638
1639        let actions = core.iterate_bids();
1640        assert_eq!(
1641            actions,
1642            vec![
1643                MatchAction::FillLimit(ClientOrderId::from("O-1")),
1644                MatchAction::FillLimit(ClientOrderId::from("O-2")),
1645                MatchAction::FillLimit(ClientOrderId::from("O-3")),
1646                MatchAction::FillLimit(ClientOrderId::from("O-4")),
1647            ],
1648        );
1649    }
1650
1651    #[rstest]
1652    fn test_buy_stops_trigger_in_ascending_price_order_when_ask_crosses_multiple() {
1653        // Codex regression: ask climbs from 100 to 106. BUY stops at 101 and
1654        // 105 should both trigger, but the 101 stop must fire first because
1655        // the ask crossed it before reaching 105.
1656        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1657        core.set_ask_raw(Price::from("106.00"));
1658
1659        core.add_order(stop_order(OrderSide::Buy, "105.00", "O-FAR"));
1660        core.add_order(stop_order(OrderSide::Buy, "101.00", "O-NEAR"));
1661
1662        let actions = core.iterate_bids();
1663        assert_eq!(
1664            actions,
1665            vec![
1666                MatchAction::TriggerStop(ClientOrderId::from("O-NEAR")),
1667                MatchAction::TriggerStop(ClientOrderId::from("O-FAR")),
1668            ],
1669        );
1670    }
1671
1672    #[rstest]
1673    fn test_sell_stops_trigger_in_descending_price_order_when_bid_crosses_multiple() {
1674        // Symmetric to the BUY case: bid drops from 100 to 94. SELL stops at
1675        // 99 and 95 should both trigger, but 99 must fire first because the
1676        // bid crossed it before reaching 95.
1677        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1678        core.set_bid_raw(Price::from("94.00"));
1679
1680        core.add_order(stop_order(OrderSide::Sell, "95.00", "O-FAR"));
1681        core.add_order(stop_order(OrderSide::Sell, "99.00", "O-NEAR"));
1682
1683        let actions = core.iterate_asks();
1684        assert_eq!(
1685            actions,
1686            vec![
1687                MatchAction::TriggerStop(ClientOrderId::from("O-NEAR")),
1688                MatchAction::TriggerStop(ClientOrderId::from("O-FAR")),
1689            ],
1690        );
1691    }
1692
1693    #[rstest]
1694    fn test_iterate_stops_preserves_fifo_within_same_trigger() {
1695        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1696        core.set_ask_raw(Price::from("106.00"));
1697
1698        for id in ["O-S1", "O-S2", "O-S3"] {
1699            core.add_order(stop_order(OrderSide::Buy, "101.00", id));
1700        }
1701
1702        let actions = core.iterate_bids();
1703        assert_eq!(
1704            actions,
1705            vec![
1706                MatchAction::TriggerStop(ClientOrderId::from("O-S1")),
1707                MatchAction::TriggerStop(ClientOrderId::from("O-S2")),
1708                MatchAction::TriggerStop(ClientOrderId::from("O-S3")),
1709            ],
1710        );
1711    }
1712
1713    #[rstest]
1714    fn test_iterate_bids_processes_limits_before_stops() {
1715        // Both must match: ask=106 fills BUY limit at 110 (106 <= 110) AND
1716        // triggers BUY stop at 101 (106 >= 101). Limits emit before stops.
1717        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1718        core.set_ask_raw(Price::from("106.00"));
1719
1720        core.add_order(limit_order(OrderSide::Buy, "110.00", "O-LMT"));
1721        core.add_order(stop_order(OrderSide::Buy, "101.00", "O-STP"));
1722
1723        let actions = core.iterate_bids();
1724        assert_eq!(
1725            actions,
1726            vec![
1727                MatchAction::FillLimit(ClientOrderId::from("O-LMT")),
1728                MatchAction::TriggerStop(ClientOrderId::from("O-STP")),
1729            ],
1730        );
1731    }
1732
1733    #[rstest]
1734    fn test_iterate_asks_processes_limits_before_stops() {
1735        // Both must match: bid=94 fills SELL limit at 90 (94 >= 90) AND
1736        // triggers SELL stop at 99 (94 <= 99). Limits emit before stops.
1737        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1738        core.set_bid_raw(Price::from("94.00"));
1739
1740        core.add_order(limit_order(OrderSide::Sell, "90.00", "O-LMT"));
1741        core.add_order(stop_order(OrderSide::Sell, "99.00", "O-STP"));
1742
1743        let actions = core.iterate_asks();
1744        assert_eq!(
1745            actions,
1746            vec![
1747                MatchAction::FillLimit(ClientOrderId::from("O-LMT")),
1748                MatchAction::TriggerStop(ClientOrderId::from("O-STP")),
1749            ],
1750        );
1751    }
1752
1753    #[rstest]
1754    fn test_stop_limit_routed_to_stop_book_keyed_by_trigger() {
1755        // STOP_LIMIT has both prices set. is_stop() is true (because
1756        // trigger_price.is_some()), so it must live in the stop book and be
1757        // keyed by trigger_price for trigger-priority iteration.
1758        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1759        core.set_ask_raw(Price::from("106.00"));
1760
1761        // Two STOP_LIMIT BUYs at different triggers; the closer trigger
1762        // (101) must fire first regardless of limit prices.
1763        core.add_order(stop_limit_order(
1764            OrderSide::Buy,
1765            "105.00",
1766            "110.00",
1767            "O-FAR",
1768        ));
1769        core.add_order(stop_limit_order(
1770            OrderSide::Buy,
1771            "101.00",
1772            "110.00",
1773            "O-NEAR",
1774        ));
1775
1776        let actions = core.iterate_bids();
1777        assert_eq!(
1778            actions,
1779            vec![
1780                MatchAction::TriggerStop(ClientOrderId::from("O-NEAR")),
1781                MatchAction::TriggerStop(ClientOrderId::from("O-FAR")),
1782            ],
1783        );
1784    }
1785
1786    #[rstest]
1787    fn test_iterate_full_walk_combines_bids_then_asks_each_with_limits_then_stops() {
1788        // Both sides matchable simultaneously requires limits priced beyond
1789        // the touch and stops nearer to the touch.
1790        // Bid: ask=106 -> BUY limits at 110/107 fill (106 <= each), BUY stops
1791        // at 101/105 trigger (106 >= each).
1792        // Ask: bid=94 -> SELL limits at 90/93 fill (94 >= each), SELL stops
1793        // at 95/99 trigger (94 <= each).
1794        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1795        core.set_bid_raw(Price::from("94.00"));
1796        core.set_ask_raw(Price::from("106.00"));
1797
1798        core.add_order(limit_order(OrderSide::Buy, "110.00", "O-B-LMT-HIGH"));
1799        core.add_order(limit_order(OrderSide::Buy, "107.00", "O-B-LMT-LOW"));
1800        core.add_order(stop_order(OrderSide::Buy, "105.00", "O-B-STP-FAR"));
1801        core.add_order(stop_order(OrderSide::Buy, "101.00", "O-B-STP-NEAR"));
1802
1803        core.add_order(limit_order(OrderSide::Sell, "90.00", "O-A-LMT-LOW"));
1804        core.add_order(limit_order(OrderSide::Sell, "93.00", "O-A-LMT-HIGH"));
1805        core.add_order(stop_order(OrderSide::Sell, "95.00", "O-A-STP-FAR"));
1806        core.add_order(stop_order(OrderSide::Sell, "99.00", "O-A-STP-NEAR"));
1807
1808        let actions = core.iterate();
1809        assert_eq!(
1810            actions,
1811            vec![
1812                // bids: limits high-to-low, then stops near-to-far
1813                MatchAction::FillLimit(ClientOrderId::from("O-B-LMT-HIGH")),
1814                MatchAction::FillLimit(ClientOrderId::from("O-B-LMT-LOW")),
1815                MatchAction::TriggerStop(ClientOrderId::from("O-B-STP-NEAR")),
1816                MatchAction::TriggerStop(ClientOrderId::from("O-B-STP-FAR")),
1817                // asks: limits low-to-high, then stops near-to-far
1818                MatchAction::FillLimit(ClientOrderId::from("O-A-LMT-LOW")),
1819                MatchAction::FillLimit(ClientOrderId::from("O-A-LMT-HIGH")),
1820                MatchAction::TriggerStop(ClientOrderId::from("O-A-STP-NEAR")),
1821                MatchAction::TriggerStop(ClientOrderId::from("O-A-STP-FAR")),
1822            ],
1823        );
1824    }
1825
1826    #[rstest]
1827    fn test_pending_orders_skipped_in_iterate_but_visible_in_get_orders() {
1828        let instrument_id = InstrumentId::from("AAPL.XNAS");
1829        let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1830        core.set_bid_raw(Price::from("99.00"));
1831        core.set_ask_raw(Price::from("100.00"));
1832
1833        // Real orders.
1834        core.add_order(limit_order(OrderSide::Buy, "100.00", "O-LMT"));
1835
1836        // A pending (no key) order.
1837        let pending = RestingOrder::new(
1838            ClientOrderId::from("O-PENDING"),
1839            OrderSideSpecified::Buy,
1840            OrderType::MarketToLimit,
1841            None,
1842            None,
1843            true,
1844        );
1845        core.add_order(pending);
1846
1847        // iterate sees only the limit; the pending order has no price to match.
1848        assert_eq!(
1849            core.iterate_bids(),
1850            vec![MatchAction::FillLimit(ClientOrderId::from("O-LMT"))],
1851        );
1852
1853        // get_orders sees both: bucketed first, pending appended.
1854        let bid_ids: Vec<_> = core
1855            .get_orders_bid()
1856            .iter()
1857            .map(|o| o.client_order_id)
1858            .collect();
1859        assert_eq!(
1860            bid_ids,
1861            vec![
1862                ClientOrderId::from("O-LMT"),
1863                ClientOrderId::from("O-PENDING"),
1864            ],
1865        );
1866    }
1867
1868    #[rstest]
1869    fn test_modify_then_readd_moves_order_to_back_of_new_level() {
1870        // A price-changing modify is delete + add; the re-added order must
1871        // land at the back of the new price level (queue-position loss),
1872        // matching real-venue behavior.
1873        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1874        core.set_ask_raw(Price::from("99.00"));
1875
1876        core.add_order(limit_order(OrderSide::Buy, "100.00", "O-A"));
1877        core.add_order(limit_order(OrderSide::Buy, "100.00", "O-B"));
1878        core.add_order(limit_order(OrderSide::Buy, "100.00", "O-C"));
1879
1880        // O-A modifies its price to 100.50 (better): moves to a new level.
1881        core.delete_order(ClientOrderId::from("O-A")).unwrap();
1882        core.add_order(limit_order(OrderSide::Buy, "100.50", "O-A"));
1883
1884        // O-B then modifies to 100.00 in place (price unchanged via re-add):
1885        // loses queue position to O-C at the same level.
1886        core.delete_order(ClientOrderId::from("O-B")).unwrap();
1887        core.add_order(limit_order(OrderSide::Buy, "100.00", "O-B"));
1888
1889        let actions = core.iterate_bids();
1890        assert_eq!(
1891            actions,
1892            vec![
1893                MatchAction::FillLimit(ClientOrderId::from("O-A")), // 100.50 best
1894                MatchAction::FillLimit(ClientOrderId::from("O-C")), // 100.00 oldest
1895                MatchAction::FillLimit(ClientOrderId::from("O-B")), // 100.00 newest
1896            ],
1897        );
1898    }
1899
1900    #[rstest]
1901    fn test_delete_unknown_order_returns_not_found() {
1902        let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1903        let result = core.delete_order(ClientOrderId::from("O-MISSING"));
1904        assert!(matches!(result, Err(OrderError::NotFound(_))));
1905    }
1906}