nautilus_execution/matching_core/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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            if let Some(handler) = &mut self.fill_limit_order {
253                handler
254                    .0
255                    .fill_limit_order(&mut OrderAny::from(order.clone()));
256            }
257        }
258    }
259
260    pub fn match_stop_order(&mut self, order: &StopOrderAny) {
261        if self.is_stop_matched(order.order_side_specified(), order.stop_px()) {
262            if let Some(handler) = &mut self.trigger_stop_order {
263                handler
264                    .0
265                    .trigger_stop_order(&mut OrderAny::from(order.clone()));
266            }
267        }
268    }
269
270    #[must_use]
271    pub fn is_limit_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
272        match side {
273            OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= price),
274            OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= price),
275        }
276    }
277
278    #[must_use]
279    pub fn is_stop_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
280        match side {
281            OrderSideSpecified::Buy => self.ask.is_some_and(|a| a >= price),
282            OrderSideSpecified::Sell => self.bid.is_some_and(|b| b <= price),
283        }
284    }
285
286    #[must_use]
287    pub fn is_touch_triggered(&self, side: OrderSideSpecified, trigger_price: Price) -> bool {
288        match side {
289            OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= trigger_price),
290            OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= trigger_price),
291        }
292    }
293}
294
295////////////////////////////////////////////////////////////////////////////////
296// Tests
297////////////////////////////////////////////////////////////////////////////////
298#[cfg(test)]
299mod tests {
300    use nautilus_model::{
301        enums::{OrderSide, OrderType},
302        orders::{Order, builder::OrderTestBuilder},
303        types::Quantity,
304    };
305    use rstest::rstest;
306
307    use super::*;
308
309    const fn create_matching_core(
310        instrument_id: InstrumentId,
311        price_increment: Price,
312    ) -> OrderMatchingCore {
313        OrderMatchingCore::new(instrument_id, price_increment, None, None, None)
314    }
315
316    #[rstest]
317    fn test_add_order_bid_side() {
318        let instrument_id = InstrumentId::from("AAPL.XNAS");
319        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
320
321        let order = OrderTestBuilder::new(OrderType::Limit)
322            .instrument_id(instrument_id)
323            .side(OrderSide::Buy)
324            .price(Price::from("100.00"))
325            .quantity(Quantity::from("100"))
326            .build();
327
328        matching_core.add_order(order.clone().into()).unwrap();
329
330        let passive_order: PassiveOrderAny = order.into();
331        assert!(matching_core.get_orders_bid().contains(&passive_order));
332        assert!(!matching_core.get_orders_ask().contains(&passive_order));
333        assert_eq!(matching_core.get_orders_bid().len(), 1);
334        assert!(matching_core.get_orders_ask().is_empty());
335        assert!(matching_core.order_exists(passive_order.client_order_id()));
336    }
337
338    #[rstest]
339    fn test_add_order_ask_side() {
340        let instrument_id = InstrumentId::from("AAPL.XNAS");
341        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
342
343        let order = OrderTestBuilder::new(OrderType::Limit)
344            .instrument_id(instrument_id)
345            .side(OrderSide::Sell)
346            .price(Price::from("100.00"))
347            .quantity(Quantity::from("100"))
348            .build();
349
350        matching_core.add_order(order.clone().into()).unwrap();
351
352        let passive_order: PassiveOrderAny = order.into();
353        assert!(matching_core.get_orders_ask().contains(&passive_order));
354        assert!(!matching_core.get_orders_bid().contains(&passive_order));
355        assert_eq!(matching_core.get_orders_ask().len(), 1);
356        assert!(matching_core.get_orders_bid().is_empty());
357        assert!(matching_core.order_exists(passive_order.client_order_id()));
358    }
359
360    #[rstest]
361    fn test_reset() {
362        let instrument_id = InstrumentId::from("AAPL.XNAS");
363        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
364
365        let order = OrderTestBuilder::new(OrderType::Limit)
366            .instrument_id(instrument_id)
367            .side(OrderSide::Sell)
368            .price(Price::from("100.00"))
369            .quantity(Quantity::from("100"))
370            .build();
371
372        let client_order_id = order.client_order_id();
373
374        matching_core.add_order(order.into()).unwrap();
375        matching_core.bid = Some(Price::from("100.00"));
376        matching_core.ask = Some(Price::from("100.00"));
377        matching_core.last = Some(Price::from("100.00"));
378
379        matching_core.reset();
380
381        assert!(matching_core.bid.is_none());
382        assert!(matching_core.ask.is_none());
383        assert!(matching_core.last.is_none());
384        assert!(matching_core.get_orders_bid().is_empty());
385        assert!(matching_core.get_orders_ask().is_empty());
386        assert!(!matching_core.order_exists(client_order_id));
387    }
388
389    #[rstest]
390    fn test_delete_order_when_not_exists() {
391        let instrument_id = InstrumentId::from("AAPL.XNAS");
392        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
393
394        let order = OrderTestBuilder::new(OrderType::Limit)
395            .instrument_id(instrument_id)
396            .side(OrderSide::Buy)
397            .price(Price::from("100.00"))
398            .quantity(Quantity::from("100"))
399            .build();
400
401        let result = matching_core.delete_order(&order.into());
402        assert!(result.is_err());
403    }
404
405    #[rstest]
406    #[case(OrderSide::Buy)]
407    #[case(OrderSide::Sell)]
408    fn test_delete_order_when_exists(#[case] order_side: OrderSide) {
409        let instrument_id = InstrumentId::from("AAPL.XNAS");
410        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
411
412        let order = OrderTestBuilder::new(OrderType::Limit)
413            .instrument_id(instrument_id)
414            .side(order_side)
415            .price(Price::from("100.00"))
416            .quantity(Quantity::from("100"))
417            .build();
418
419        matching_core.add_order(order.clone().into()).unwrap();
420        matching_core.delete_order(&order.into()).unwrap();
421
422        assert!(matching_core.get_orders_ask().is_empty());
423        assert!(matching_core.get_orders_bid().is_empty());
424    }
425
426    #[rstest]
427    #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
428    #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
429    #[case(
430        Some(Price::from("100.00")),
431        Some(Price::from("101.00")),
432        Price::from("100.00"),  // <-- Price below ask
433        OrderSide::Buy,
434        false
435    )]
436    #[case(
437        Some(Price::from("100.00")),
438        Some(Price::from("101.00")),
439        Price::from("101.00"),  // <-- Price at ask
440        OrderSide::Buy,
441        true
442    )]
443    #[case(
444        Some(Price::from("100.00")),
445        Some(Price::from("101.00")),
446        Price::from("102.00"),  // <-- Price above ask (marketable)
447        OrderSide::Buy,
448        true
449    )]
450    #[case(
451        Some(Price::from("100.00")),
452        Some(Price::from("101.00")),
453        Price::from("101.00"), // <-- Price above bid
454        OrderSide::Sell,
455        false
456    )]
457    #[case(
458        Some(Price::from("100.00")),
459        Some(Price::from("101.00")),
460        Price::from("100.00"),  // <-- Price at bid
461        OrderSide::Sell,
462        true
463    )]
464    #[case(
465        Some(Price::from("100.00")),
466        Some(Price::from("101.00")),
467        Price::from("99.00"),  // <-- Price below bid (marketable)
468        OrderSide::Sell,
469        true
470    )]
471    fn test_is_limit_matched(
472        #[case] bid: Option<Price>,
473        #[case] ask: Option<Price>,
474        #[case] price: Price,
475        #[case] order_side: OrderSide,
476        #[case] expected: bool,
477    ) {
478        let instrument_id = InstrumentId::from("AAPL.XNAS");
479        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
480        matching_core.bid = bid;
481        matching_core.ask = ask;
482
483        let order = OrderTestBuilder::new(OrderType::Limit)
484            .instrument_id(instrument_id)
485            .side(order_side)
486            .price(price)
487            .quantity(Quantity::from("100"))
488            .build();
489
490        let result =
491            matching_core.is_limit_matched(order.order_side_specified(), order.price().unwrap());
492        assert_eq!(result, expected);
493    }
494
495    #[rstest]
496    #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
497    #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
498    #[case(
499        Some(Price::from("100.00")),
500        Some(Price::from("101.00")),
501        Price::from("102.00"),  // <-- Trigger above ask
502        OrderSide::Buy,
503        false
504    )]
505    #[case(
506        Some(Price::from("100.00")),
507        Some(Price::from("101.00")),
508        Price::from("101.00"),  // <-- Trigger at ask
509        OrderSide::Buy,
510        true
511    )]
512    #[case(
513        Some(Price::from("100.00")),
514        Some(Price::from("101.00")),
515        Price::from("100.00"),  // <-- Trigger below ask
516        OrderSide::Buy,
517        true
518    )]
519    #[case(
520        Some(Price::from("100.00")),
521        Some(Price::from("101.00")),
522        Price::from("99.00"),  // Trigger below bid
523        OrderSide::Sell,
524        false
525    )]
526    #[case(
527        Some(Price::from("100.00")),
528        Some(Price::from("101.00")),
529        Price::from("100.00"),  // <-- Trigger at bid
530        OrderSide::Sell,
531        true
532    )]
533    #[case(
534        Some(Price::from("100.00")),
535        Some(Price::from("101.00")),
536        Price::from("101.00"),  // <-- Trigger above bid
537        OrderSide::Sell,
538        true
539    )]
540    fn test_is_stop_matched(
541        #[case] bid: Option<Price>,
542        #[case] ask: Option<Price>,
543        #[case] trigger_price: Price,
544        #[case] order_side: OrderSide,
545        #[case] expected: bool,
546    ) {
547        let instrument_id = InstrumentId::from("AAPL.XNAS");
548        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
549        matching_core.bid = bid;
550        matching_core.ask = ask;
551
552        let order = OrderTestBuilder::new(OrderType::StopMarket)
553            .instrument_id(instrument_id)
554            .side(order_side)
555            .trigger_price(trigger_price)
556            .quantity(Quantity::from("100"))
557            .build();
558
559        let result = matching_core
560            .is_stop_matched(order.order_side_specified(), order.trigger_price().unwrap());
561        assert_eq!(result, expected);
562    }
563}