Skip to main content

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