Skip to main content

nautilus_execution/matching_core/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! A common `OrderMatchingCore` for the `OrderMatchingEngine` and other components.
17
18pub mod handlers;
19
20use nautilus_model::{
21    enums::{OrderSideSpecified, OrderType},
22    identifiers::{ClientOrderId, InstrumentId},
23    orders::{Order, OrderError, PassiveOrderAny, StopOrderAny},
24    types::Price,
25};
26
27use crate::matching_core::handlers::{
28    FillLimitOrderHandler, ShareableFillLimitOrderHandler, ShareableFillMarketOrderHandler,
29    ShareableTriggerStopOrderHandler, TriggerStopOrderHandler,
30};
31
32/// Lightweight order information for matching/trigger checking.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct OrderMatchInfo {
35    pub client_order_id: ClientOrderId,
36    pub order_side: OrderSideSpecified,
37    pub order_type: OrderType,
38    pub trigger_price: Option<Price>,
39    pub limit_price: Option<Price>,
40    pub is_activated: bool,
41}
42
43impl OrderMatchInfo {
44    /// Creates a new [`OrderMatchInfo`] instance.
45    #[must_use]
46    pub const fn new(
47        client_order_id: ClientOrderId,
48        order_side: OrderSideSpecified,
49        order_type: OrderType,
50        trigger_price: Option<Price>,
51        limit_price: Option<Price>,
52        is_activated: bool,
53    ) -> Self {
54        Self {
55            client_order_id,
56            order_side,
57            order_type,
58            trigger_price,
59            limit_price,
60            is_activated,
61        }
62    }
63
64    /// Returns true if this is a stop order type that needs trigger checking.
65    #[must_use]
66    pub const fn is_stop(&self) -> bool {
67        self.trigger_price.is_some()
68    }
69
70    /// Returns true if this is a limit order type that needs fill checking.
71    #[must_use]
72    pub const fn is_limit(&self) -> bool {
73        self.limit_price.is_some() && self.trigger_price.is_none()
74    }
75}
76
77impl From<&PassiveOrderAny> for OrderMatchInfo {
78    fn from(order: &PassiveOrderAny) -> Self {
79        match order {
80            PassiveOrderAny::Limit(limit) => Self {
81                client_order_id: limit.client_order_id(),
82                order_side: limit.order_side_specified(),
83                order_type: limit.order_type(),
84                trigger_price: None,
85                limit_price: Some(limit.limit_px()),
86                is_activated: true,
87            },
88            PassiveOrderAny::Stop(stop) => {
89                let limit_price = match stop {
90                    StopOrderAny::LimitIfTouched(o) => Some(o.price),
91                    StopOrderAny::StopLimit(o) => Some(o.price),
92                    StopOrderAny::TrailingStopLimit(o) => Some(o.price),
93                    StopOrderAny::MarketIfTouched(_)
94                    | StopOrderAny::StopMarket(_)
95                    | StopOrderAny::TrailingStopMarket(_) => None,
96                };
97                let is_activated = match stop {
98                    StopOrderAny::TrailingStopMarket(o) => o.is_activated,
99                    StopOrderAny::TrailingStopLimit(o) => o.is_activated,
100                    _ => true,
101                };
102                Self {
103                    client_order_id: stop.client_order_id(),
104                    order_side: stop.order_side_specified(),
105                    order_type: stop.order_type(),
106                    trigger_price: Some(stop.stop_px()),
107                    limit_price,
108                    is_activated,
109                }
110            }
111        }
112    }
113}
114
115/// A generic order matching core.
116#[derive(Clone, Debug)]
117pub struct OrderMatchingCore {
118    /// The instrument ID for the matching core.
119    pub instrument_id: InstrumentId,
120    /// The price increment for the matching core.
121    pub price_increment: Price,
122    /// The current bid price for the matching core.
123    pub bid: Option<Price>,
124    /// The current ask price for the matching core.
125    pub ask: Option<Price>,
126    /// The last price for the matching core.
127    pub last: Option<Price>,
128    pub is_bid_initialized: bool,
129    pub is_ask_initialized: bool,
130    pub is_last_initialized: bool,
131    orders_bid: Vec<OrderMatchInfo>,
132    orders_ask: Vec<OrderMatchInfo>,
133    trigger_stop_order: Option<ShareableTriggerStopOrderHandler>,
134    fill_market_order: Option<ShareableFillMarketOrderHandler>,
135    fill_limit_order: Option<ShareableFillLimitOrderHandler>,
136}
137
138impl OrderMatchingCore {
139    // Creates a new [`OrderMatchingCore`] instance.
140    #[must_use]
141    pub const fn new(
142        instrument_id: InstrumentId,
143        price_increment: Price,
144        trigger_stop_order: Option<ShareableTriggerStopOrderHandler>,
145        fill_market_order: Option<ShareableFillMarketOrderHandler>,
146        fill_limit_order: Option<ShareableFillLimitOrderHandler>,
147    ) -> Self {
148        Self {
149            instrument_id,
150            price_increment,
151            bid: None,
152            ask: None,
153            last: None,
154            is_bid_initialized: false,
155            is_ask_initialized: false,
156            is_last_initialized: false,
157            orders_bid: Vec::new(),
158            orders_ask: Vec::new(),
159            trigger_stop_order,
160            fill_market_order,
161            fill_limit_order,
162        }
163    }
164
165    pub fn set_fill_limit_order_handler(&mut self, handler: ShareableFillLimitOrderHandler) {
166        self.fill_limit_order = Some(handler);
167    }
168
169    pub fn set_trigger_stop_order_handler(&mut self, handler: ShareableTriggerStopOrderHandler) {
170        self.trigger_stop_order = Some(handler);
171    }
172
173    pub fn set_fill_market_order_handler(&mut self, handler: ShareableFillMarketOrderHandler) {
174        self.fill_market_order = Some(handler);
175    }
176
177    // -- QUERIES ---------------------------------------------------------------------------------
178
179    #[must_use]
180    pub const fn price_precision(&self) -> u8 {
181        self.price_increment.precision
182    }
183
184    #[must_use]
185    pub fn get_order(&self, client_order_id: ClientOrderId) -> Option<&OrderMatchInfo> {
186        self.orders_bid
187            .iter()
188            .find(|o| o.client_order_id == client_order_id)
189            .or_else(|| {
190                self.orders_ask
191                    .iter()
192                    .find(|o| o.client_order_id == client_order_id)
193            })
194    }
195
196    #[must_use]
197    pub const fn get_orders_bid(&self) -> &[OrderMatchInfo] {
198        self.orders_bid.as_slice()
199    }
200
201    #[must_use]
202    pub const fn get_orders_ask(&self) -> &[OrderMatchInfo] {
203        self.orders_ask.as_slice()
204    }
205
206    #[must_use]
207    pub fn get_orders(&self) -> Vec<OrderMatchInfo> {
208        let mut orders = self.orders_bid.clone();
209        orders.extend_from_slice(&self.orders_ask);
210        orders
211    }
212
213    #[must_use]
214    pub fn order_exists(&self, client_order_id: ClientOrderId) -> bool {
215        self.orders_bid
216            .iter()
217            .any(|o| o.client_order_id == client_order_id)
218            || self
219                .orders_ask
220                .iter()
221                .any(|o| o.client_order_id == client_order_id)
222    }
223
224    // -- COMMANDS --------------------------------------------------------------------------------
225
226    pub const fn set_last_raw(&mut self, last: Price) {
227        self.last = Some(last);
228        self.is_last_initialized = true;
229    }
230
231    pub const fn set_bid_raw(&mut self, bid: Price) {
232        self.bid = Some(bid);
233        self.is_bid_initialized = true;
234    }
235
236    pub const fn set_ask_raw(&mut self, ask: Price) {
237        self.ask = Some(ask);
238        self.is_ask_initialized = true;
239    }
240
241    pub fn reset(&mut self) {
242        self.bid = None;
243        self.ask = None;
244        self.last = None;
245        self.orders_bid.clear();
246        self.orders_ask.clear();
247    }
248
249    /// Adds an order to the matching core.
250    pub fn add_order(&mut self, order: OrderMatchInfo) {
251        match order.order_side {
252            OrderSideSpecified::Buy => self.orders_bid.push(order),
253            OrderSideSpecified::Sell => self.orders_ask.push(order),
254        }
255    }
256
257    /// Deletes an order from the matching core by client order ID.
258    ///
259    /// # Errors
260    ///
261    /// Returns an [`OrderError::NotFound`] if the order is not present.
262    pub fn delete_order(&mut self, client_order_id: ClientOrderId) -> Result<(), OrderError> {
263        if let Some(index) = self
264            .orders_bid
265            .iter()
266            .position(|o| o.client_order_id == client_order_id)
267        {
268            self.orders_bid.remove(index);
269            return Ok(());
270        }
271
272        if let Some(index) = self
273            .orders_ask
274            .iter()
275            .position(|o| o.client_order_id == client_order_id)
276        {
277            self.orders_ask.remove(index);
278            return Ok(());
279        }
280
281        Err(OrderError::NotFound(client_order_id))
282    }
283
284    pub fn iterate(&mut self) {
285        self.iterate_bids();
286        self.iterate_asks();
287    }
288
289    pub fn iterate_bids(&mut self) {
290        let orders: Vec<_> = self.orders_bid.clone();
291        for order in &orders {
292            self.match_order(order);
293        }
294    }
295
296    pub fn iterate_asks(&mut self) {
297        let orders: Vec<_> = self.orders_ask.clone();
298        for order in &orders {
299            self.match_order(order);
300        }
301    }
302
303    // -- MATCHING --------------------------------------------------------------------------------
304
305    pub fn match_order(&mut self, order: &OrderMatchInfo) {
306        if order.is_stop() {
307            self.match_stop_order(order);
308        } else if order.is_limit() {
309            self.match_limit_order(order);
310        }
311    }
312
313    fn match_limit_order(&mut self, order: &OrderMatchInfo) {
314        if let Some(limit_price) = order.limit_price
315            && self.is_limit_matched(order.order_side, limit_price)
316            && let Some(handler) = &mut self.fill_limit_order
317        {
318            handler.0.fill_limit_order(order.client_order_id);
319        }
320    }
321
322    fn match_stop_order(&mut self, order: &OrderMatchInfo) {
323        if !order.is_activated {
324            return;
325        }
326
327        if let Some(trigger_price) = order.trigger_price
328            && self.is_stop_matched(order.order_side, trigger_price)
329            && let Some(handler) = &mut self.trigger_stop_order
330        {
331            handler.0.trigger_stop_order(order.client_order_id);
332        }
333    }
334
335    #[must_use]
336    pub fn is_limit_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
337        match side {
338            OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= price),
339            OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= price),
340        }
341    }
342
343    #[must_use]
344    pub fn is_stop_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
345        match side {
346            OrderSideSpecified::Buy => self.ask.is_some_and(|a| a >= price),
347            OrderSideSpecified::Sell => self.bid.is_some_and(|b| b <= price),
348        }
349    }
350
351    #[must_use]
352    pub fn is_touch_triggered(&self, side: OrderSideSpecified, trigger_price: Price) -> bool {
353        match side {
354            OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= trigger_price),
355            OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= trigger_price),
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use nautilus_model::{
363        enums::{OrderSide, OrderType},
364        orders::{Order, builder::OrderTestBuilder},
365        types::Quantity,
366    };
367    use rstest::rstest;
368
369    use super::*;
370
371    const fn create_matching_core(
372        instrument_id: InstrumentId,
373        price_increment: Price,
374    ) -> OrderMatchingCore {
375        OrderMatchingCore::new(instrument_id, price_increment, None, None, None)
376    }
377
378    #[rstest]
379    fn test_add_order_bid_side() {
380        let instrument_id = InstrumentId::from("AAPL.XNAS");
381        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
382
383        let order = OrderTestBuilder::new(OrderType::Limit)
384            .instrument_id(instrument_id)
385            .side(OrderSide::Buy)
386            .price(Price::from("100.00"))
387            .quantity(Quantity::from("100"))
388            .build();
389
390        let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
391        matching_core.add_order(match_info.clone());
392
393        assert!(matching_core.get_orders_bid().contains(&match_info));
394        assert!(!matching_core.get_orders_ask().contains(&match_info));
395        assert_eq!(matching_core.get_orders_bid().len(), 1);
396        assert!(matching_core.get_orders_ask().is_empty());
397        assert!(matching_core.order_exists(match_info.client_order_id));
398    }
399
400    #[rstest]
401    fn test_add_order_ask_side() {
402        let instrument_id = InstrumentId::from("AAPL.XNAS");
403        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
404
405        let order = OrderTestBuilder::new(OrderType::Limit)
406            .instrument_id(instrument_id)
407            .side(OrderSide::Sell)
408            .price(Price::from("100.00"))
409            .quantity(Quantity::from("100"))
410            .build();
411
412        let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
413        matching_core.add_order(match_info.clone());
414
415        assert!(matching_core.get_orders_ask().contains(&match_info));
416        assert!(!matching_core.get_orders_bid().contains(&match_info));
417        assert_eq!(matching_core.get_orders_ask().len(), 1);
418        assert!(matching_core.get_orders_bid().is_empty());
419        assert!(matching_core.order_exists(match_info.client_order_id));
420    }
421
422    #[rstest]
423    fn test_reset() {
424        let instrument_id = InstrumentId::from("AAPL.XNAS");
425        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
426
427        let order = OrderTestBuilder::new(OrderType::Limit)
428            .instrument_id(instrument_id)
429            .side(OrderSide::Sell)
430            .price(Price::from("100.00"))
431            .quantity(Quantity::from("100"))
432            .build();
433
434        let client_order_id = order.client_order_id();
435        let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
436        matching_core.add_order(match_info);
437        matching_core.bid = Some(Price::from("100.00"));
438        matching_core.ask = Some(Price::from("100.00"));
439        matching_core.last = Some(Price::from("100.00"));
440
441        matching_core.reset();
442
443        assert!(matching_core.bid.is_none());
444        assert!(matching_core.ask.is_none());
445        assert!(matching_core.last.is_none());
446        assert!(matching_core.get_orders_bid().is_empty());
447        assert!(matching_core.get_orders_ask().is_empty());
448        assert!(!matching_core.order_exists(client_order_id));
449    }
450
451    #[rstest]
452    fn test_delete_order_when_not_exists() {
453        let instrument_id = InstrumentId::from("AAPL.XNAS");
454        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
455
456        let order = OrderTestBuilder::new(OrderType::Limit)
457            .instrument_id(instrument_id)
458            .side(OrderSide::Buy)
459            .price(Price::from("100.00"))
460            .quantity(Quantity::from("100"))
461            .build();
462
463        let result = matching_core.delete_order(order.client_order_id());
464        assert!(result.is_err());
465    }
466
467    #[rstest]
468    #[case(OrderSide::Buy)]
469    #[case(OrderSide::Sell)]
470    fn test_delete_order_when_exists(#[case] order_side: OrderSide) {
471        let instrument_id = InstrumentId::from("AAPL.XNAS");
472        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
473
474        let order = OrderTestBuilder::new(OrderType::Limit)
475            .instrument_id(instrument_id)
476            .side(order_side)
477            .price(Price::from("100.00"))
478            .quantity(Quantity::from("100"))
479            .build();
480
481        let client_order_id = order.client_order_id();
482        let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
483        matching_core.add_order(match_info);
484        matching_core.delete_order(client_order_id).unwrap();
485
486        assert!(matching_core.get_orders_ask().is_empty());
487        assert!(matching_core.get_orders_bid().is_empty());
488    }
489
490    #[rstest]
491    #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
492    #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
493    #[case(
494        Some(Price::from("100.00")),
495        Some(Price::from("101.00")),
496        Price::from("100.00"),  // <-- Price below ask
497        OrderSide::Buy,
498        false
499    )]
500    #[case(
501        Some(Price::from("100.00")),
502        Some(Price::from("101.00")),
503        Price::from("101.00"),  // <-- Price at ask
504        OrderSide::Buy,
505        true
506    )]
507    #[case(
508        Some(Price::from("100.00")),
509        Some(Price::from("101.00")),
510        Price::from("102.00"),  // <-- Price above ask (marketable)
511        OrderSide::Buy,
512        true
513    )]
514    #[case(
515        Some(Price::from("100.00")),
516        Some(Price::from("101.00")),
517        Price::from("101.00"), // <-- Price above bid
518        OrderSide::Sell,
519        false
520    )]
521    #[case(
522        Some(Price::from("100.00")),
523        Some(Price::from("101.00")),
524        Price::from("100.00"),  // <-- Price at bid
525        OrderSide::Sell,
526        true
527    )]
528    #[case(
529        Some(Price::from("100.00")),
530        Some(Price::from("101.00")),
531        Price::from("99.00"),  // <-- Price below bid (marketable)
532        OrderSide::Sell,
533        true
534    )]
535    fn test_is_limit_matched(
536        #[case] bid: Option<Price>,
537        #[case] ask: Option<Price>,
538        #[case] price: Price,
539        #[case] order_side: OrderSide,
540        #[case] expected: bool,
541    ) {
542        let instrument_id = InstrumentId::from("AAPL.XNAS");
543        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
544        matching_core.bid = bid;
545        matching_core.ask = ask;
546
547        let order = OrderTestBuilder::new(OrderType::Limit)
548            .instrument_id(instrument_id)
549            .side(order_side)
550            .price(price)
551            .quantity(Quantity::from("100"))
552            .build();
553
554        let result =
555            matching_core.is_limit_matched(order.order_side_specified(), order.price().unwrap());
556        assert_eq!(result, expected);
557    }
558
559    #[rstest]
560    #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
561    #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
562    #[case(
563        Some(Price::from("100.00")),
564        Some(Price::from("101.00")),
565        Price::from("102.00"),  // <-- Trigger above ask
566        OrderSide::Buy,
567        false
568    )]
569    #[case(
570        Some(Price::from("100.00")),
571        Some(Price::from("101.00")),
572        Price::from("101.00"),  // <-- Trigger at ask
573        OrderSide::Buy,
574        true
575    )]
576    #[case(
577        Some(Price::from("100.00")),
578        Some(Price::from("101.00")),
579        Price::from("100.00"),  // <-- Trigger below ask
580        OrderSide::Buy,
581        true
582    )]
583    #[case(
584        Some(Price::from("100.00")),
585        Some(Price::from("101.00")),
586        Price::from("99.00"),  // Trigger below bid
587        OrderSide::Sell,
588        false
589    )]
590    #[case(
591        Some(Price::from("100.00")),
592        Some(Price::from("101.00")),
593        Price::from("100.00"),  // <-- Trigger at bid
594        OrderSide::Sell,
595        true
596    )]
597    #[case(
598        Some(Price::from("100.00")),
599        Some(Price::from("101.00")),
600        Price::from("101.00"),  // <-- Trigger above bid
601        OrderSide::Sell,
602        true
603    )]
604    fn test_is_stop_matched(
605        #[case] bid: Option<Price>,
606        #[case] ask: Option<Price>,
607        #[case] trigger_price: Price,
608        #[case] order_side: OrderSide,
609        #[case] expected: bool,
610    ) {
611        let instrument_id = InstrumentId::from("AAPL.XNAS");
612        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
613        matching_core.bid = bid;
614        matching_core.ask = ask;
615
616        let order = OrderTestBuilder::new(OrderType::StopMarket)
617            .instrument_id(instrument_id)
618            .side(order_side)
619            .trigger_price(trigger_price)
620            .quantity(Quantity::from("100"))
621            .build();
622
623        let result = matching_core
624            .is_stop_matched(order.order_side_specified(), order.trigger_price().unwrap());
625        assert_eq!(result, expected);
626    }
627}