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
335            .add_order(PassiveOrderAny::try_from(order.clone()).unwrap())
336            .unwrap();
337
338        let passive_order: PassiveOrderAny = PassiveOrderAny::try_from(order).unwrap();
339        assert!(matching_core.get_orders_bid().contains(&passive_order));
340        assert!(!matching_core.get_orders_ask().contains(&passive_order));
341        assert_eq!(matching_core.get_orders_bid().len(), 1);
342        assert!(matching_core.get_orders_ask().is_empty());
343        assert!(matching_core.order_exists(passive_order.client_order_id()));
344    }
345
346    #[rstest]
347    fn test_add_order_ask_side() {
348        let instrument_id = InstrumentId::from("AAPL.XNAS");
349        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
350
351        let order = OrderTestBuilder::new(OrderType::Limit)
352            .instrument_id(instrument_id)
353            .side(OrderSide::Sell)
354            .price(Price::from("100.00"))
355            .quantity(Quantity::from("100"))
356            .build();
357
358        matching_core
359            .add_order(PassiveOrderAny::try_from(order.clone()).unwrap())
360            .unwrap();
361
362        let passive_order: PassiveOrderAny = PassiveOrderAny::try_from(order).unwrap();
363        assert!(matching_core.get_orders_ask().contains(&passive_order));
364        assert!(!matching_core.get_orders_bid().contains(&passive_order));
365        assert_eq!(matching_core.get_orders_ask().len(), 1);
366        assert!(matching_core.get_orders_bid().is_empty());
367        assert!(matching_core.order_exists(passive_order.client_order_id()));
368    }
369
370    #[rstest]
371    fn test_reset() {
372        let instrument_id = InstrumentId::from("AAPL.XNAS");
373        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
374
375        let order = OrderTestBuilder::new(OrderType::Limit)
376            .instrument_id(instrument_id)
377            .side(OrderSide::Sell)
378            .price(Price::from("100.00"))
379            .quantity(Quantity::from("100"))
380            .build();
381
382        let client_order_id = order.client_order_id();
383
384        matching_core
385            .add_order(PassiveOrderAny::try_from(order).unwrap())
386            .unwrap();
387        matching_core.bid = Some(Price::from("100.00"));
388        matching_core.ask = Some(Price::from("100.00"));
389        matching_core.last = Some(Price::from("100.00"));
390
391        matching_core.reset();
392
393        assert!(matching_core.bid.is_none());
394        assert!(matching_core.ask.is_none());
395        assert!(matching_core.last.is_none());
396        assert!(matching_core.get_orders_bid().is_empty());
397        assert!(matching_core.get_orders_ask().is_empty());
398        assert!(!matching_core.order_exists(client_order_id));
399    }
400
401    #[rstest]
402    fn test_delete_order_when_not_exists() {
403        let instrument_id = InstrumentId::from("AAPL.XNAS");
404        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
405
406        let order = OrderTestBuilder::new(OrderType::Limit)
407            .instrument_id(instrument_id)
408            .side(OrderSide::Buy)
409            .price(Price::from("100.00"))
410            .quantity(Quantity::from("100"))
411            .build();
412
413        let result = matching_core.delete_order(&PassiveOrderAny::try_from(order).unwrap());
414        assert!(result.is_err());
415    }
416
417    #[rstest]
418    #[case(OrderSide::Buy)]
419    #[case(OrderSide::Sell)]
420    fn test_delete_order_when_exists(#[case] order_side: OrderSide) {
421        let instrument_id = InstrumentId::from("AAPL.XNAS");
422        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
423
424        let order = OrderTestBuilder::new(OrderType::Limit)
425            .instrument_id(instrument_id)
426            .side(order_side)
427            .price(Price::from("100.00"))
428            .quantity(Quantity::from("100"))
429            .build();
430
431        matching_core
432            .add_order(PassiveOrderAny::try_from(order.clone()).unwrap())
433            .unwrap();
434        matching_core
435            .delete_order(&PassiveOrderAny::try_from(order).unwrap())
436            .unwrap();
437
438        assert!(matching_core.get_orders_ask().is_empty());
439        assert!(matching_core.get_orders_bid().is_empty());
440    }
441
442    #[rstest]
443    #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
444    #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
445    #[case(
446        Some(Price::from("100.00")),
447        Some(Price::from("101.00")),
448        Price::from("100.00"),  // <-- Price below ask
449        OrderSide::Buy,
450        false
451    )]
452    #[case(
453        Some(Price::from("100.00")),
454        Some(Price::from("101.00")),
455        Price::from("101.00"),  // <-- Price at ask
456        OrderSide::Buy,
457        true
458    )]
459    #[case(
460        Some(Price::from("100.00")),
461        Some(Price::from("101.00")),
462        Price::from("102.00"),  // <-- Price above ask (marketable)
463        OrderSide::Buy,
464        true
465    )]
466    #[case(
467        Some(Price::from("100.00")),
468        Some(Price::from("101.00")),
469        Price::from("101.00"), // <-- Price above bid
470        OrderSide::Sell,
471        false
472    )]
473    #[case(
474        Some(Price::from("100.00")),
475        Some(Price::from("101.00")),
476        Price::from("100.00"),  // <-- Price at bid
477        OrderSide::Sell,
478        true
479    )]
480    #[case(
481        Some(Price::from("100.00")),
482        Some(Price::from("101.00")),
483        Price::from("99.00"),  // <-- Price below bid (marketable)
484        OrderSide::Sell,
485        true
486    )]
487    fn test_is_limit_matched(
488        #[case] bid: Option<Price>,
489        #[case] ask: Option<Price>,
490        #[case] price: Price,
491        #[case] order_side: OrderSide,
492        #[case] expected: bool,
493    ) {
494        let instrument_id = InstrumentId::from("AAPL.XNAS");
495        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
496        matching_core.bid = bid;
497        matching_core.ask = ask;
498
499        let order = OrderTestBuilder::new(OrderType::Limit)
500            .instrument_id(instrument_id)
501            .side(order_side)
502            .price(price)
503            .quantity(Quantity::from("100"))
504            .build();
505
506        let result =
507            matching_core.is_limit_matched(order.order_side_specified(), order.price().unwrap());
508        assert_eq!(result, expected);
509    }
510
511    #[rstest]
512    #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
513    #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
514    #[case(
515        Some(Price::from("100.00")),
516        Some(Price::from("101.00")),
517        Price::from("102.00"),  // <-- Trigger above ask
518        OrderSide::Buy,
519        false
520    )]
521    #[case(
522        Some(Price::from("100.00")),
523        Some(Price::from("101.00")),
524        Price::from("101.00"),  // <-- Trigger at ask
525        OrderSide::Buy,
526        true
527    )]
528    #[case(
529        Some(Price::from("100.00")),
530        Some(Price::from("101.00")),
531        Price::from("100.00"),  // <-- Trigger below ask
532        OrderSide::Buy,
533        true
534    )]
535    #[case(
536        Some(Price::from("100.00")),
537        Some(Price::from("101.00")),
538        Price::from("99.00"),  // Trigger below bid
539        OrderSide::Sell,
540        false
541    )]
542    #[case(
543        Some(Price::from("100.00")),
544        Some(Price::from("101.00")),
545        Price::from("100.00"),  // <-- Trigger at bid
546        OrderSide::Sell,
547        true
548    )]
549    #[case(
550        Some(Price::from("100.00")),
551        Some(Price::from("101.00")),
552        Price::from("101.00"),  // <-- Trigger above bid
553        OrderSide::Sell,
554        true
555    )]
556    fn test_is_stop_matched(
557        #[case] bid: Option<Price>,
558        #[case] ask: Option<Price>,
559        #[case] trigger_price: Price,
560        #[case] order_side: OrderSide,
561        #[case] expected: bool,
562    ) {
563        let instrument_id = InstrumentId::from("AAPL.XNAS");
564        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
565        matching_core.bid = bid;
566        matching_core.ask = ask;
567
568        let order = OrderTestBuilder::new(OrderType::StopMarket)
569            .instrument_id(instrument_id)
570            .side(order_side)
571            .trigger_price(trigger_price)
572            .quantity(Quantity::from("100"))
573            .build();
574
575        let result = matching_core
576            .is_stop_matched(order.order_side_specified(), order.trigger_price().unwrap());
577        assert_eq!(result, expected);
578    }
579}