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