1use nautilus_model::{
19 enums::{OrderSideSpecified, OrderType},
20 identifiers::{ClientOrderId, InstrumentId},
21 orders::{Order, OrderError, PassiveOrderAny, StopOrderAny},
22 types::Price,
23};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MatchAction {
28 FillLimit(ClientOrderId),
29 TriggerStop(ClientOrderId),
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 fill_limit_inside_spread: bool,
132 orders_bid: Vec<OrderMatchInfo>,
133 orders_ask: Vec<OrderMatchInfo>,
134}
135
136impl OrderMatchingCore {
137 #[must_use]
139 pub const fn new(instrument_id: InstrumentId, price_increment: Price) -> Self {
140 Self {
141 instrument_id,
142 price_increment,
143 bid: None,
144 ask: None,
145 last: None,
146 is_bid_initialized: false,
147 is_ask_initialized: false,
148 is_last_initialized: false,
149 fill_limit_inside_spread: false,
150 orders_bid: Vec::new(),
151 orders_ask: Vec::new(),
152 }
153 }
154
155 #[must_use]
156 pub const fn price_precision(&self) -> u8 {
157 self.price_increment.precision
158 }
159
160 #[must_use]
161 pub fn get_order(&self, client_order_id: ClientOrderId) -> Option<&OrderMatchInfo> {
162 self.orders_bid
163 .iter()
164 .find(|o| o.client_order_id == client_order_id)
165 .or_else(|| {
166 self.orders_ask
167 .iter()
168 .find(|o| o.client_order_id == client_order_id)
169 })
170 }
171
172 #[must_use]
173 pub const fn get_orders_bid(&self) -> &[OrderMatchInfo] {
174 self.orders_bid.as_slice()
175 }
176
177 #[must_use]
178 pub const fn get_orders_ask(&self) -> &[OrderMatchInfo] {
179 self.orders_ask.as_slice()
180 }
181
182 #[must_use]
183 pub fn get_orders(&self) -> Vec<OrderMatchInfo> {
184 let mut orders = self.orders_bid.clone();
185 orders.extend_from_slice(&self.orders_ask);
186 orders
187 }
188
189 #[must_use]
190 pub fn order_exists(&self, client_order_id: ClientOrderId) -> bool {
191 self.orders_bid
192 .iter()
193 .any(|o| o.client_order_id == client_order_id)
194 || self
195 .orders_ask
196 .iter()
197 .any(|o| o.client_order_id == client_order_id)
198 }
199
200 pub const fn set_last_raw(&mut self, last: Price) {
201 self.last = Some(last);
202 self.is_last_initialized = true;
203 }
204
205 pub const fn set_bid_raw(&mut self, bid: Price) {
206 self.bid = Some(bid);
207 self.is_bid_initialized = true;
208 }
209
210 pub const fn set_ask_raw(&mut self, ask: Price) {
211 self.ask = Some(ask);
212 self.is_ask_initialized = true;
213 }
214
215 pub fn reset(&mut self) {
216 self.bid = None;
217 self.ask = None;
218 self.last = None;
219 self.orders_bid.clear();
220 self.orders_ask.clear();
221 }
222
223 pub fn add_order(&mut self, order: OrderMatchInfo) {
225 match order.order_side {
226 OrderSideSpecified::Buy => self.orders_bid.push(order),
227 OrderSideSpecified::Sell => self.orders_ask.push(order),
228 }
229 }
230
231 pub fn delete_order(&mut self, client_order_id: ClientOrderId) -> Result<(), OrderError> {
237 if let Some(index) = self
238 .orders_bid
239 .iter()
240 .position(|o| o.client_order_id == client_order_id)
241 {
242 self.orders_bid.remove(index);
243 return Ok(());
244 }
245
246 if let Some(index) = self
247 .orders_ask
248 .iter()
249 .position(|o| o.client_order_id == client_order_id)
250 {
251 self.orders_ask.remove(index);
252 return Ok(());
253 }
254
255 Err(OrderError::NotFound(client_order_id))
256 }
257
258 pub fn iterate(&mut self) -> Vec<MatchAction> {
259 let mut actions = self.iterate_bids();
260 actions.extend(self.iterate_asks());
261 actions
262 }
263
264 pub fn iterate_bids(&mut self) -> Vec<MatchAction> {
265 let orders: Vec<_> = self.orders_bid.clone();
266 orders
267 .iter()
268 .filter_map(|order| self.match_order(order))
269 .collect()
270 }
271
272 pub fn iterate_asks(&mut self) -> Vec<MatchAction> {
273 let orders: Vec<_> = self.orders_ask.clone();
274 orders
275 .iter()
276 .filter_map(|order| self.match_order(order))
277 .collect()
278 }
279
280 pub fn match_order(&self, order: &OrderMatchInfo) -> Option<MatchAction> {
281 if order.is_stop() {
282 self.match_stop_order(order)
283 } else if order.is_limit() {
284 self.match_limit_order(order)
285 } else {
286 None
287 }
288 }
289
290 fn match_limit_order(&self, order: &OrderMatchInfo) -> Option<MatchAction> {
291 if let Some(limit_price) = order.limit_price
292 && self.is_limit_fillable(order.order_side, limit_price)
293 {
294 Some(MatchAction::FillLimit(order.client_order_id))
295 } else {
296 None
297 }
298 }
299
300 fn match_stop_order(&self, order: &OrderMatchInfo) -> Option<MatchAction> {
301 if !order.is_activated {
302 return None;
303 }
304
305 if let Some(trigger_price) = order.trigger_price
306 && self.is_stop_matched(order.order_side, trigger_price)
307 {
308 Some(MatchAction::TriggerStop(order.client_order_id))
309 } else {
310 None
311 }
312 }
313
314 #[must_use]
315 pub fn is_limit_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
316 match side {
317 OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= price),
318 OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= price),
319 }
320 }
321
322 #[must_use]
323 pub fn is_stop_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
324 match side {
325 OrderSideSpecified::Buy => self.ask.is_some_and(|a| a >= price),
326 OrderSideSpecified::Sell => self.bid.is_some_and(|b| b <= price),
327 }
328 }
329
330 #[must_use]
331 pub fn is_touch_triggered(&self, side: OrderSideSpecified, trigger_price: Price) -> bool {
332 match side {
333 OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= trigger_price),
334 OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= trigger_price),
335 }
336 }
337
338 pub fn set_fill_limit_inside_spread(&mut self, value: bool) {
339 self.fill_limit_inside_spread = value;
340 }
341
342 #[must_use]
348 pub fn is_limit_fillable(&self, side: OrderSideSpecified, price: Price) -> bool {
349 if self.is_limit_matched(side, price) {
350 return true;
351 }
352
353 if !self.fill_limit_inside_spread {
354 return false;
355 }
356
357 if let (Some(bid), Some(ask)) = (self.bid, self.ask) {
359 match side {
360 OrderSideSpecified::Buy => price >= bid,
361 OrderSideSpecified::Sell => price <= ask,
362 }
363 } else {
364 false
365 }
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use nautilus_model::{
372 enums::{OrderSide, OrderType},
373 orders::{Order, builder::OrderTestBuilder},
374 types::Quantity,
375 };
376 use rstest::rstest;
377
378 use super::*;
379
380 const fn create_matching_core(
381 instrument_id: InstrumentId,
382 price_increment: Price,
383 ) -> OrderMatchingCore {
384 OrderMatchingCore::new(instrument_id, price_increment)
385 }
386
387 #[rstest]
388 fn test_add_order_bid_side() {
389 let instrument_id = InstrumentId::from("AAPL.XNAS");
390 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
391
392 let order = OrderTestBuilder::new(OrderType::Limit)
393 .instrument_id(instrument_id)
394 .side(OrderSide::Buy)
395 .price(Price::from("100.00"))
396 .quantity(Quantity::from("100"))
397 .build();
398
399 let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
400 matching_core.add_order(match_info.clone());
401
402 assert!(matching_core.get_orders_bid().contains(&match_info));
403 assert!(!matching_core.get_orders_ask().contains(&match_info));
404 assert_eq!(matching_core.get_orders_bid().len(), 1);
405 assert!(matching_core.get_orders_ask().is_empty());
406 assert!(matching_core.order_exists(match_info.client_order_id));
407 }
408
409 #[rstest]
410 fn test_add_order_ask_side() {
411 let instrument_id = InstrumentId::from("AAPL.XNAS");
412 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
413
414 let order = OrderTestBuilder::new(OrderType::Limit)
415 .instrument_id(instrument_id)
416 .side(OrderSide::Sell)
417 .price(Price::from("100.00"))
418 .quantity(Quantity::from("100"))
419 .build();
420
421 let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
422 matching_core.add_order(match_info.clone());
423
424 assert!(matching_core.get_orders_ask().contains(&match_info));
425 assert!(!matching_core.get_orders_bid().contains(&match_info));
426 assert_eq!(matching_core.get_orders_ask().len(), 1);
427 assert!(matching_core.get_orders_bid().is_empty());
428 assert!(matching_core.order_exists(match_info.client_order_id));
429 }
430
431 #[rstest]
432 fn test_reset() {
433 let instrument_id = InstrumentId::from("AAPL.XNAS");
434 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
435
436 let order = OrderTestBuilder::new(OrderType::Limit)
437 .instrument_id(instrument_id)
438 .side(OrderSide::Sell)
439 .price(Price::from("100.00"))
440 .quantity(Quantity::from("100"))
441 .build();
442
443 let client_order_id = order.client_order_id();
444 let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
445 matching_core.add_order(match_info);
446 matching_core.bid = Some(Price::from("100.00"));
447 matching_core.ask = Some(Price::from("100.00"));
448 matching_core.last = Some(Price::from("100.00"));
449
450 matching_core.reset();
451
452 assert!(matching_core.bid.is_none());
453 assert!(matching_core.ask.is_none());
454 assert!(matching_core.last.is_none());
455 assert!(matching_core.get_orders_bid().is_empty());
456 assert!(matching_core.get_orders_ask().is_empty());
457 assert!(!matching_core.order_exists(client_order_id));
458 }
459
460 #[rstest]
461 fn test_delete_order_when_not_exists() {
462 let instrument_id = InstrumentId::from("AAPL.XNAS");
463 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
464
465 let order = OrderTestBuilder::new(OrderType::Limit)
466 .instrument_id(instrument_id)
467 .side(OrderSide::Buy)
468 .price(Price::from("100.00"))
469 .quantity(Quantity::from("100"))
470 .build();
471
472 let result = matching_core.delete_order(order.client_order_id());
473 assert!(result.is_err());
474 }
475
476 #[rstest]
477 #[case(OrderSide::Buy)]
478 #[case(OrderSide::Sell)]
479 fn test_delete_order_when_exists(#[case] order_side: OrderSide) {
480 let instrument_id = InstrumentId::from("AAPL.XNAS");
481 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
482
483 let order = OrderTestBuilder::new(OrderType::Limit)
484 .instrument_id(instrument_id)
485 .side(order_side)
486 .price(Price::from("100.00"))
487 .quantity(Quantity::from("100"))
488 .build();
489
490 let client_order_id = order.client_order_id();
491 let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
492 matching_core.add_order(match_info);
493 matching_core.delete_order(client_order_id).unwrap();
494
495 assert!(matching_core.get_orders_ask().is_empty());
496 assert!(matching_core.get_orders_bid().is_empty());
497 }
498
499 #[rstest]
500 #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
501 #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
502 #[case(
503 Some(Price::from("100.00")),
504 Some(Price::from("101.00")),
505 Price::from("100.00"), OrderSide::Buy,
507 false
508 )]
509 #[case(
510 Some(Price::from("100.00")),
511 Some(Price::from("101.00")),
512 Price::from("101.00"), OrderSide::Buy,
514 true
515 )]
516 #[case(
517 Some(Price::from("100.00")),
518 Some(Price::from("101.00")),
519 Price::from("102.00"), OrderSide::Buy,
521 true
522 )]
523 #[case(
524 Some(Price::from("100.00")),
525 Some(Price::from("101.00")),
526 Price::from("101.00"), OrderSide::Sell,
528 false
529 )]
530 #[case(
531 Some(Price::from("100.00")),
532 Some(Price::from("101.00")),
533 Price::from("100.00"), OrderSide::Sell,
535 true
536 )]
537 #[case(
538 Some(Price::from("100.00")),
539 Some(Price::from("101.00")),
540 Price::from("99.00"), OrderSide::Sell,
542 true
543 )]
544 fn test_is_limit_matched(
545 #[case] bid: Option<Price>,
546 #[case] ask: Option<Price>,
547 #[case] price: Price,
548 #[case] order_side: OrderSide,
549 #[case] expected: bool,
550 ) {
551 let instrument_id = InstrumentId::from("AAPL.XNAS");
552 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
553 matching_core.bid = bid;
554 matching_core.ask = ask;
555
556 let order = OrderTestBuilder::new(OrderType::Limit)
557 .instrument_id(instrument_id)
558 .side(order_side)
559 .price(price)
560 .quantity(Quantity::from("100"))
561 .build();
562
563 let result =
564 matching_core.is_limit_matched(order.order_side_specified(), order.price().unwrap());
565 assert_eq!(result, expected);
566 }
567
568 #[rstest]
569 #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
570 #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
571 #[case(
572 Some(Price::from("100.00")),
573 Some(Price::from("101.00")),
574 Price::from("102.00"), OrderSide::Buy,
576 false
577 )]
578 #[case(
579 Some(Price::from("100.00")),
580 Some(Price::from("101.00")),
581 Price::from("101.00"), OrderSide::Buy,
583 true
584 )]
585 #[case(
586 Some(Price::from("100.00")),
587 Some(Price::from("101.00")),
588 Price::from("100.00"), OrderSide::Buy,
590 true
591 )]
592 #[case(
593 Some(Price::from("100.00")),
594 Some(Price::from("101.00")),
595 Price::from("99.00"), OrderSide::Sell,
597 false
598 )]
599 #[case(
600 Some(Price::from("100.00")),
601 Some(Price::from("101.00")),
602 Price::from("100.00"), OrderSide::Sell,
604 true
605 )]
606 #[case(
607 Some(Price::from("100.00")),
608 Some(Price::from("101.00")),
609 Price::from("101.00"), OrderSide::Sell,
611 true
612 )]
613 fn test_is_stop_matched(
614 #[case] bid: Option<Price>,
615 #[case] ask: Option<Price>,
616 #[case] trigger_price: Price,
617 #[case] order_side: OrderSide,
618 #[case] expected: bool,
619 ) {
620 let instrument_id = InstrumentId::from("AAPL.XNAS");
621 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
622 matching_core.bid = bid;
623 matching_core.ask = ask;
624
625 let order = OrderTestBuilder::new(OrderType::StopMarket)
626 .instrument_id(instrument_id)
627 .side(order_side)
628 .trigger_price(trigger_price)
629 .quantity(Quantity::from("100"))
630 .build();
631
632 let result = matching_core
633 .is_stop_matched(order.order_side_specified(), order.trigger_price().unwrap());
634 assert_eq!(result, expected);
635 }
636
637 #[rstest]
638 fn test_iterate_returns_empty_when_no_orders() {
639 let instrument_id = InstrumentId::from("AAPL.XNAS");
640 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
641 matching_core.bid = Some(Price::from("100.00"));
642 matching_core.ask = Some(Price::from("101.00"));
643
644 let actions = matching_core.iterate();
645
646 assert!(actions.is_empty());
647 }
648
649 #[rstest]
650 fn test_iterate_returns_empty_when_no_market_data() {
651 let instrument_id = InstrumentId::from("AAPL.XNAS");
652 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
653
654 let order = OrderTestBuilder::new(OrderType::Limit)
655 .instrument_id(instrument_id)
656 .side(OrderSide::Buy)
657 .price(Price::from("100.00"))
658 .quantity(Quantity::from("100"))
659 .build();
660 let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
661 matching_core.add_order(match_info);
662
663 let actions = matching_core.iterate();
664
665 assert!(actions.is_empty());
666 }
667
668 #[rstest]
669 fn test_iterate_returns_fill_limit_for_matched_buy() {
670 let instrument_id = InstrumentId::from("AAPL.XNAS");
671 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
672 matching_core.ask = Some(Price::from("100.00"));
673
674 let order = OrderTestBuilder::new(OrderType::Limit)
675 .instrument_id(instrument_id)
676 .side(OrderSide::Buy)
677 .price(Price::from("100.00"))
678 .quantity(Quantity::from("100"))
679 .build();
680 let client_order_id = order.client_order_id();
681 let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
682 matching_core.add_order(match_info);
683
684 let actions = matching_core.iterate();
685
686 assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
687 }
688
689 #[rstest]
690 fn test_iterate_returns_fill_limit_for_matched_sell() {
691 let instrument_id = InstrumentId::from("AAPL.XNAS");
692 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
693 matching_core.bid = Some(Price::from("100.00"));
694
695 let order = OrderTestBuilder::new(OrderType::Limit)
696 .instrument_id(instrument_id)
697 .side(OrderSide::Sell)
698 .price(Price::from("100.00"))
699 .quantity(Quantity::from("100"))
700 .build();
701 let client_order_id = order.client_order_id();
702 let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
703 matching_core.add_order(match_info);
704
705 let actions = matching_core.iterate();
706
707 assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
708 }
709
710 #[rstest]
711 fn test_iterate_returns_no_fill_for_unmatched_limit() {
712 let instrument_id = InstrumentId::from("AAPL.XNAS");
713 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
714 matching_core.ask = Some(Price::from("101.00"));
715
716 let order = OrderTestBuilder::new(OrderType::Limit)
718 .instrument_id(instrument_id)
719 .side(OrderSide::Buy)
720 .price(Price::from("100.00"))
721 .quantity(Quantity::from("100"))
722 .build();
723 let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
724 matching_core.add_order(match_info);
725
726 let actions = matching_core.iterate();
727
728 assert!(actions.is_empty());
729 }
730
731 #[rstest]
732 fn test_iterate_returns_trigger_stop_for_matched_buy() {
733 let instrument_id = InstrumentId::from("AAPL.XNAS");
734 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
735 matching_core.ask = Some(Price::from("101.00"));
736
737 let order = OrderTestBuilder::new(OrderType::StopMarket)
738 .instrument_id(instrument_id)
739 .side(OrderSide::Buy)
740 .trigger_price(Price::from("101.00"))
741 .quantity(Quantity::from("100"))
742 .build();
743 let client_order_id = order.client_order_id();
744 let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
745 matching_core.add_order(match_info);
746
747 let actions = matching_core.iterate();
748
749 assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
750 }
751
752 #[rstest]
753 fn test_iterate_returns_trigger_stop_for_matched_sell() {
754 let instrument_id = InstrumentId::from("AAPL.XNAS");
755 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
756 matching_core.bid = Some(Price::from("99.00"));
757
758 let order = OrderTestBuilder::new(OrderType::StopMarket)
759 .instrument_id(instrument_id)
760 .side(OrderSide::Sell)
761 .trigger_price(Price::from("99.00"))
762 .quantity(Quantity::from("100"))
763 .build();
764 let client_order_id = order.client_order_id();
765 let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
766 matching_core.add_order(match_info);
767
768 let actions = matching_core.iterate();
769
770 assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
771 }
772
773 #[rstest]
774 fn test_iterate_skips_unactivated_stop_order() {
775 let instrument_id = InstrumentId::from("AAPL.XNAS");
776 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
777 matching_core.ask = Some(Price::from("110.00"));
778
779 let match_info = OrderMatchInfo::new(
781 ClientOrderId::from("O-001"),
782 OrderSideSpecified::Buy,
783 OrderType::TrailingStopMarket,
784 Some(Price::from("105.00")),
785 None,
786 false, );
788 matching_core.add_order(match_info);
789
790 let actions = matching_core.iterate();
791
792 assert!(actions.is_empty());
793 }
794
795 #[rstest]
796 fn test_iterate_triggers_activated_stop_order() {
797 let instrument_id = InstrumentId::from("AAPL.XNAS");
798 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
799 matching_core.ask = Some(Price::from("110.00"));
800
801 let client_order_id = ClientOrderId::from("O-001");
802 let match_info = OrderMatchInfo::new(
803 client_order_id,
804 OrderSideSpecified::Buy,
805 OrderType::TrailingStopMarket,
806 Some(Price::from("105.00")),
807 None,
808 true, );
810 matching_core.add_order(match_info);
811
812 let actions = matching_core.iterate();
813
814 assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
815 }
816
817 #[rstest]
818 fn test_iterate_returns_mixed_actions_for_limits_and_stops() {
819 let instrument_id = InstrumentId::from("AAPL.XNAS");
820 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
821 matching_core.bid = Some(Price::from("99.00"));
822 matching_core.ask = Some(Price::from("101.00"));
823
824 let buy_limit = OrderTestBuilder::new(OrderType::Limit)
826 .instrument_id(instrument_id)
827 .side(OrderSide::Buy)
828 .price(Price::from("101.00"))
829 .quantity(Quantity::from("100"))
830 .build();
831 let buy_limit_id = buy_limit.client_order_id();
832 matching_core.add_order(OrderMatchInfo::from(
833 &PassiveOrderAny::try_from(buy_limit).unwrap(),
834 ));
835
836 let sell_stop = OrderTestBuilder::new(OrderType::StopMarket)
838 .instrument_id(instrument_id)
839 .side(OrderSide::Sell)
840 .trigger_price(Price::from("99.00"))
841 .quantity(Quantity::from("50"))
842 .build();
843 let sell_stop_id = sell_stop.client_order_id();
844 matching_core.add_order(OrderMatchInfo::from(
845 &PassiveOrderAny::try_from(sell_stop).unwrap(),
846 ));
847
848 let actions = matching_core.iterate();
849
850 assert_eq!(actions.len(), 2);
852 assert_eq!(actions[0], MatchAction::FillLimit(buy_limit_id));
853 assert_eq!(actions[1], MatchAction::TriggerStop(sell_stop_id));
854 }
855
856 #[rstest]
857 fn test_is_limit_fillable_delegates_to_is_limit_matched_by_default() {
858 let instrument_id = InstrumentId::from("AAPL.XNAS");
859 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
860 core.set_bid_raw(Price::from("100.00"));
861 core.set_ask_raw(Price::from("101.00"));
862
863 assert!(core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("101.00")));
864 assert!(!core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
865 assert!(core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("100.00")));
866 assert!(!core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("101.00")));
867 }
868
869 #[rstest]
870 fn test_is_limit_fillable_inside_spread_buy_at_bid() {
871 let instrument_id = InstrumentId::from("AAPL.XNAS");
872 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
873 core.set_bid_raw(Price::from("100.00"));
874 core.set_ask_raw(Price::from("101.00"));
875 core.set_fill_limit_inside_spread(true);
876
877 assert!(core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
878 assert!(core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.50")));
879 assert!(!core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("99.00")));
880 }
881
882 #[rstest]
883 fn test_is_limit_fillable_inside_spread_sell_at_ask() {
884 let instrument_id = InstrumentId::from("AAPL.XNAS");
885 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
886 core.set_bid_raw(Price::from("100.00"));
887 core.set_ask_raw(Price::from("101.00"));
888 core.set_fill_limit_inside_spread(true);
889
890 assert!(core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("101.00")));
891 assert!(core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("100.50")));
892 assert!(!core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("102.00")));
893 }
894
895 #[rstest]
896 fn test_is_limit_fillable_inside_spread_requires_both_quotes_present() {
897 let instrument_id = InstrumentId::from("AAPL.XNAS");
898 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
899 core.set_fill_limit_inside_spread(true);
900
901 core.set_bid_raw(Price::from("100.00"));
902 assert!(!core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
903
904 let mut core2 = create_matching_core(instrument_id, Price::from("0.01"));
905 core2.set_fill_limit_inside_spread(true);
906 core2.set_ask_raw(Price::from("101.00"));
907 assert!(!core2.is_limit_fillable(OrderSideSpecified::Sell, Price::from("101.00")));
908
909 let mut core3 = create_matching_core(instrument_id, Price::from("0.01"));
911 core3.set_fill_limit_inside_spread(true);
912 core3.set_bid_raw(Price::from("100.00"));
913 core3.set_ask_raw(Price::from("101.00"));
914 core3.ask = None;
915 assert!(!core3.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
916 }
917
918 #[rstest]
919 fn test_iterate_fills_limit_inside_spread_when_enabled() {
920 let instrument_id = InstrumentId::from("AAPL.XNAS");
921 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
922 core.set_bid_raw(Price::from("100.00"));
923 core.set_ask_raw(Price::from("101.00"));
924 core.set_fill_limit_inside_spread(true);
925
926 let order = OrderTestBuilder::new(OrderType::Limit)
927 .instrument_id(instrument_id)
928 .side(OrderSide::Buy)
929 .price(Price::from("100.00"))
930 .quantity(Quantity::from("100"))
931 .build();
932 let client_order_id = order.client_order_id();
933 let match_info = OrderMatchInfo::from(&PassiveOrderAny::try_from(order).unwrap());
934 core.add_order(match_info);
935
936 let actions = core.iterate();
937 assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
938 }
939
940 #[rstest]
941 #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
942 #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
943 #[case(
944 Some(Price::from("100.00")),
945 Some(Price::from("101.00")),
946 Price::from("102.00"), OrderSide::Buy,
948 true
949 )]
950 #[case(
951 Some(Price::from("100.00")),
952 Some(Price::from("101.00")),
953 Price::from("101.00"), OrderSide::Buy,
955 true
956 )]
957 #[case(
958 Some(Price::from("100.00")),
959 Some(Price::from("101.00")),
960 Price::from("100.00"), OrderSide::Buy,
962 false
963 )]
964 #[case(
965 Some(Price::from("100.00")),
966 Some(Price::from("101.00")),
967 Price::from("99.00"), OrderSide::Sell,
969 true
970 )]
971 #[case(
972 Some(Price::from("100.00")),
973 Some(Price::from("101.00")),
974 Price::from("100.00"), OrderSide::Sell,
976 true
977 )]
978 #[case(
979 Some(Price::from("100.00")),
980 Some(Price::from("101.00")),
981 Price::from("101.00"), OrderSide::Sell,
983 false
984 )]
985 fn test_is_touch_triggered(
986 #[case] bid: Option<Price>,
987 #[case] ask: Option<Price>,
988 #[case] trigger_price: Price,
989 #[case] order_side: OrderSide,
990 #[case] expected: bool,
991 ) {
992 let instrument_id = InstrumentId::from("AAPL.XNAS");
993 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
994 matching_core.bid = bid;
995 matching_core.ask = ask;
996
997 let result = matching_core.is_touch_triggered(order_side.as_specified(), trigger_price);
998 assert_eq!(result, expected);
999 }
1000}