1#![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#[derive(Clone)]
31pub struct OrderMatchingCore {
32 pub instrument_id: InstrumentId,
34 pub price_increment: Price,
36 pub bid: Option<Price>,
38 pub ask: Option<Price>,
40 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 #[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 #[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 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 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#[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"), OrderSide::Buy,
385 false
386 )]
387 #[case(
388 Some(Price::from("100.00")),
389 Some(Price::from("101.00")),
390 Price::from("101.00"), OrderSide::Buy,
392 true
393 )]
394 #[case(
395 Some(Price::from("100.00")),
396 Some(Price::from("101.00")),
397 Price::from("102.00"), OrderSide::Buy,
399 true
400 )]
401 #[case(
402 Some(Price::from("100.00")),
403 Some(Price::from("101.00")),
404 Price::from("101.00"), OrderSide::Sell,
406 false
407 )]
408 #[case(
409 Some(Price::from("100.00")),
410 Some(Price::from("101.00")),
411 Price::from("100.00"), OrderSide::Sell,
413 true
414 )]
415 #[case(
416 Some(Price::from("100.00")),
417 Some(Price::from("101.00")),
418 Price::from("99.00"), 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"), OrderSide::Buy,
453 false
454 )]
455 #[case(
456 Some(Price::from("100.00")),
457 Some(Price::from("101.00")),
458 Price::from("101.00"), OrderSide::Buy,
460 true
461 )]
462 #[case(
463 Some(Price::from("100.00")),
464 Some(Price::from("101.00")),
465 Price::from("100.00"), OrderSide::Buy,
467 true
468 )]
469 #[case(
470 Some(Price::from("100.00")),
471 Some(Price::from("101.00")),
472 Price::from("99.00"), OrderSide::Sell,
474 false
475 )]
476 #[case(
477 Some(Price::from("100.00")),
478 Some(Price::from("101.00")),
479 Price::from("100.00"), OrderSide::Sell,
481 true
482 )]
483 #[case(
484 Some(Price::from("100.00")),
485 Some(Price::from("101.00")),
486 Price::from("101.00"), 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}