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