Skip to main content

nautilus_execution/
matching_core.rs

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