nautilus_execution/matching_core/
mod.rs

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