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, 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)]
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 fn get_orders_bid(&self) -> &[PassiveOrderAny] {
119        self.orders_bid.as_slice()
120    }
121
122    #[must_use]
123    pub 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    pub fn add_order(&mut self, order: PassiveOrderAny) -> Result<(), OrderError> {
171        match order.order_side_specified() {
172            OrderSideSpecified::Buy => {
173                self.orders_bid.push(order);
174                Ok(())
175            }
176            OrderSideSpecified::Sell => {
177                self.orders_ask.push(order);
178                Ok(())
179            }
180        }
181    }
182
183    pub fn delete_order(&mut self, order: &PassiveOrderAny) -> Result<(), OrderError> {
184        match order.order_side_specified() {
185            OrderSideSpecified::Buy => {
186                let index = self
187                    .orders_bid
188                    .iter()
189                    .position(|o| o == order)
190                    .ok_or(OrderError::NotFound(order.client_order_id()))?;
191                self.orders_bid.remove(index);
192                Ok(())
193            }
194            OrderSideSpecified::Sell => {
195                let index = self
196                    .orders_ask
197                    .iter()
198                    .position(|o| o == order)
199                    .ok_or(OrderError::NotFound(order.client_order_id()))?;
200                self.orders_ask.remove(index);
201                Ok(())
202            }
203        }
204    }
205
206    pub fn iterate(&mut self) {
207        self.iterate_bids();
208        self.iterate_asks();
209    }
210
211    pub fn iterate_bids(&mut self) {
212        let orders: Vec<_> = self.orders_bid.clone();
213        for order in &orders {
214            self.match_order(order, false);
215        }
216    }
217
218    pub fn iterate_asks(&mut self) {
219        let orders: Vec<_> = self.orders_ask.clone();
220        for order in &orders {
221            self.match_order(order, false);
222        }
223    }
224
225    fn iterate_orders(&mut self, orders: &[PassiveOrderAny]) {
226        for order in orders {
227            self.match_order(order, false);
228        }
229    }
230
231    // -- MATCHING --------------------------------------------------------------------------------
232
233    pub fn match_order(&mut self, order: &PassiveOrderAny, _initial: bool) {
234        match order {
235            PassiveOrderAny::Limit(o) => self.match_limit_order(o),
236            PassiveOrderAny::Stop(o) => self.match_stop_order(o),
237        }
238    }
239
240    pub fn match_limit_order(&mut self, order: &LimitOrderAny) {
241        if self.is_limit_matched(order.order_side_specified(), order.limit_px()) {
242            if let Some(handler) = &mut self.fill_limit_order {
243                handler
244                    .0
245                    .fill_limit_order(&mut OrderAny::from(order.clone()));
246            }
247        }
248    }
249
250    pub fn match_stop_order(&mut self, order: &StopOrderAny) {
251        if self.is_stop_matched(order.order_side_specified(), order.stop_px()) {
252            if let Some(handler) = &mut self.trigger_stop_order {
253                handler
254                    .0
255                    .trigger_stop_order(&mut OrderAny::from(order.clone()));
256            }
257        }
258    }
259
260    #[must_use]
261    pub fn is_limit_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
262        match side {
263            OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= price),
264            OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= price),
265        }
266    }
267
268    #[must_use]
269    pub fn is_stop_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
270        match side {
271            OrderSideSpecified::Buy => self.ask.is_some_and(|a| a >= price),
272            OrderSideSpecified::Sell => self.bid.is_some_and(|b| b <= price),
273        }
274    }
275
276    #[must_use]
277    pub fn is_touch_triggered(&self, side: OrderSideSpecified, trigger_price: Price) -> bool {
278        match side {
279            OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= trigger_price),
280            OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= trigger_price),
281        }
282    }
283}
284
285////////////////////////////////////////////////////////////////////////////////
286// Tests
287////////////////////////////////////////////////////////////////////////////////
288#[cfg(test)]
289mod tests {
290    use nautilus_model::{
291        enums::{OrderSide, OrderType},
292        orders::builder::OrderTestBuilder,
293        types::Quantity,
294    };
295    use rstest::rstest;
296
297    use super::*;
298
299    const fn create_matching_core(
300        instrument_id: InstrumentId,
301        price_increment: Price,
302    ) -> OrderMatchingCore {
303        OrderMatchingCore::new(instrument_id, price_increment, None, None, None)
304    }
305
306    #[rstest]
307    fn test_add_order_bid_side() {
308        let instrument_id = InstrumentId::from("AAPL.XNAS");
309        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
310
311        let order = OrderTestBuilder::new(OrderType::Limit)
312            .instrument_id(instrument_id)
313            .side(OrderSide::Buy)
314            .price(Price::from("100.00"))
315            .quantity(Quantity::from("100"))
316            .build();
317
318        matching_core.add_order(order.clone().into()).unwrap();
319
320        let passive_order: PassiveOrderAny = order.into();
321        assert!(matching_core.get_orders_bid().contains(&passive_order));
322        assert!(!matching_core.get_orders_ask().contains(&passive_order));
323        assert_eq!(matching_core.get_orders_bid().len(), 1);
324        assert!(matching_core.get_orders_ask().is_empty());
325        assert!(matching_core.order_exists(passive_order.client_order_id()));
326    }
327
328    #[rstest]
329    fn test_add_order_ask_side() {
330        let instrument_id = InstrumentId::from("AAPL.XNAS");
331        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
332
333        let order = OrderTestBuilder::new(OrderType::Limit)
334            .instrument_id(instrument_id)
335            .side(OrderSide::Sell)
336            .price(Price::from("100.00"))
337            .quantity(Quantity::from("100"))
338            .build();
339
340        matching_core.add_order(order.clone().into()).unwrap();
341
342        let passive_order: PassiveOrderAny = order.into();
343        assert!(matching_core.get_orders_ask().contains(&passive_order));
344        assert!(!matching_core.get_orders_bid().contains(&passive_order));
345        assert_eq!(matching_core.get_orders_ask().len(), 1);
346        assert!(matching_core.get_orders_bid().is_empty());
347        assert!(matching_core.order_exists(passive_order.client_order_id()));
348    }
349
350    #[rstest]
351    fn test_reset() {
352        let instrument_id = InstrumentId::from("AAPL.XNAS");
353        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
354
355        let order = OrderTestBuilder::new(OrderType::Limit)
356            .instrument_id(instrument_id)
357            .side(OrderSide::Sell)
358            .price(Price::from("100.00"))
359            .quantity(Quantity::from("100"))
360            .build();
361
362        let client_order_id = order.client_order_id();
363
364        matching_core.add_order(order.into()).unwrap();
365        matching_core.bid = Some(Price::from("100.00"));
366        matching_core.ask = Some(Price::from("100.00"));
367        matching_core.last = Some(Price::from("100.00"));
368
369        matching_core.reset();
370
371        assert!(matching_core.bid.is_none());
372        assert!(matching_core.ask.is_none());
373        assert!(matching_core.last.is_none());
374        assert!(matching_core.get_orders_bid().is_empty());
375        assert!(matching_core.get_orders_ask().is_empty());
376        assert!(!matching_core.order_exists(client_order_id));
377    }
378
379    #[rstest]
380    fn test_delete_order_when_not_exists() {
381        let instrument_id = InstrumentId::from("AAPL.XNAS");
382        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
383
384        let order = OrderTestBuilder::new(OrderType::Limit)
385            .instrument_id(instrument_id)
386            .side(OrderSide::Buy)
387            .price(Price::from("100.00"))
388            .quantity(Quantity::from("100"))
389            .build();
390
391        let result = matching_core.delete_order(&order.into());
392        assert!(result.is_err());
393    }
394
395    #[rstest]
396    #[case(OrderSide::Buy)]
397    #[case(OrderSide::Sell)]
398    fn test_delete_order_when_exists(#[case] order_side: OrderSide) {
399        let instrument_id = InstrumentId::from("AAPL.XNAS");
400        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
401
402        let order = OrderTestBuilder::new(OrderType::Limit)
403            .instrument_id(instrument_id)
404            .side(order_side)
405            .price(Price::from("100.00"))
406            .quantity(Quantity::from("100"))
407            .build();
408
409        matching_core.add_order(order.clone().into()).unwrap();
410        matching_core.delete_order(&order.into()).unwrap();
411
412        assert!(matching_core.get_orders_ask().is_empty());
413        assert!(matching_core.get_orders_bid().is_empty());
414    }
415
416    #[rstest]
417    #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
418    #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
419    #[case(
420        Some(Price::from("100.00")),
421        Some(Price::from("101.00")),
422        Price::from("100.00"),  // <-- Price below ask
423        OrderSide::Buy,
424        false
425    )]
426    #[case(
427        Some(Price::from("100.00")),
428        Some(Price::from("101.00")),
429        Price::from("101.00"),  // <-- Price at ask
430        OrderSide::Buy,
431        true
432    )]
433    #[case(
434        Some(Price::from("100.00")),
435        Some(Price::from("101.00")),
436        Price::from("102.00"),  // <-- Price above ask (marketable)
437        OrderSide::Buy,
438        true
439    )]
440    #[case(
441        Some(Price::from("100.00")),
442        Some(Price::from("101.00")),
443        Price::from("101.00"), // <-- Price above bid
444        OrderSide::Sell,
445        false
446    )]
447    #[case(
448        Some(Price::from("100.00")),
449        Some(Price::from("101.00")),
450        Price::from("100.00"),  // <-- Price at bid
451        OrderSide::Sell,
452        true
453    )]
454    #[case(
455        Some(Price::from("100.00")),
456        Some(Price::from("101.00")),
457        Price::from("99.00"),  // <-- Price below bid (marketable)
458        OrderSide::Sell,
459        true
460    )]
461    fn test_is_limit_matched(
462        #[case] bid: Option<Price>,
463        #[case] ask: Option<Price>,
464        #[case] price: Price,
465        #[case] order_side: OrderSide,
466        #[case] expected: bool,
467    ) {
468        let instrument_id = InstrumentId::from("AAPL.XNAS");
469        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
470        matching_core.bid = bid;
471        matching_core.ask = ask;
472
473        let order = OrderTestBuilder::new(OrderType::Limit)
474            .instrument_id(instrument_id)
475            .side(order_side)
476            .price(price)
477            .quantity(Quantity::from("100"))
478            .build();
479
480        let result =
481            matching_core.is_limit_matched(order.order_side_specified(), order.price().unwrap());
482        assert_eq!(result, expected);
483    }
484
485    #[rstest]
486    #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
487    #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
488    #[case(
489        Some(Price::from("100.00")),
490        Some(Price::from("101.00")),
491        Price::from("102.00"),  // <-- Trigger above ask
492        OrderSide::Buy,
493        false
494    )]
495    #[case(
496        Some(Price::from("100.00")),
497        Some(Price::from("101.00")),
498        Price::from("101.00"),  // <-- Trigger at ask
499        OrderSide::Buy,
500        true
501    )]
502    #[case(
503        Some(Price::from("100.00")),
504        Some(Price::from("101.00")),
505        Price::from("100.00"),  // <-- Trigger below ask
506        OrderSide::Buy,
507        true
508    )]
509    #[case(
510        Some(Price::from("100.00")),
511        Some(Price::from("101.00")),
512        Price::from("99.00"),  // Trigger below bid
513        OrderSide::Sell,
514        false
515    )]
516    #[case(
517        Some(Price::from("100.00")),
518        Some(Price::from("101.00")),
519        Price::from("100.00"),  // <-- Trigger at bid
520        OrderSide::Sell,
521        true
522    )]
523    #[case(
524        Some(Price::from("100.00")),
525        Some(Price::from("101.00")),
526        Price::from("101.00"),  // <-- Trigger above bid
527        OrderSide::Sell,
528        true
529    )]
530    fn test_is_stop_matched(
531        #[case] bid: Option<Price>,
532        #[case] ask: Option<Price>,
533        #[case] trigger_price: Price,
534        #[case] order_side: OrderSide,
535        #[case] expected: bool,
536    ) {
537        let instrument_id = InstrumentId::from("AAPL.XNAS");
538        let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
539        matching_core.bid = bid;
540        matching_core.ask = ask;
541
542        let order = OrderTestBuilder::new(OrderType::StopMarket)
543            .instrument_id(instrument_id)
544            .side(order_side)
545            .trigger_price(trigger_price)
546            .quantity(Quantity::from("100"))
547            .build();
548
549        let result = matching_core
550            .is_stop_matched(order.order_side_specified(), order.trigger_price().unwrap());
551        assert_eq!(result, expected);
552    }
553}