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 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#[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"), OrderSide::Buy,
434 false
435 )]
436 #[case(
437 Some(Price::from("100.00")),
438 Some(Price::from("101.00")),
439 Price::from("101.00"), OrderSide::Buy,
441 true
442 )]
443 #[case(
444 Some(Price::from("100.00")),
445 Some(Price::from("101.00")),
446 Price::from("102.00"), OrderSide::Buy,
448 true
449 )]
450 #[case(
451 Some(Price::from("100.00")),
452 Some(Price::from("101.00")),
453 Price::from("101.00"), OrderSide::Sell,
455 false
456 )]
457 #[case(
458 Some(Price::from("100.00")),
459 Some(Price::from("101.00")),
460 Price::from("100.00"), OrderSide::Sell,
462 true
463 )]
464 #[case(
465 Some(Price::from("100.00")),
466 Some(Price::from("101.00")),
467 Price::from("99.00"), 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"), OrderSide::Buy,
503 false
504 )]
505 #[case(
506 Some(Price::from("100.00")),
507 Some(Price::from("101.00")),
508 Price::from("101.00"), OrderSide::Buy,
510 true
511 )]
512 #[case(
513 Some(Price::from("100.00")),
514 Some(Price::from("101.00")),
515 Price::from("100.00"), OrderSide::Buy,
517 true
518 )]
519 #[case(
520 Some(Price::from("100.00")),
521 Some(Price::from("101.00")),
522 Price::from("99.00"), OrderSide::Sell,
524 false
525 )]
526 #[case(
527 Some(Price::from("100.00")),
528 Some(Price::from("101.00")),
529 Price::from("100.00"), OrderSide::Sell,
531 true
532 )]
533 #[case(
534 Some(Price::from("100.00")),
535 Some(Price::from("101.00")),
536 Price::from("101.00"), 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}