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