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