1use std::collections::BTreeMap;
104
105use ahash::AHashMap;
106use nautilus_model::{
107 enums::{OrderSide, OrderType, TriggerType},
108 identifiers::{ClientOrderId, InstrumentId},
109 orders::{Order, OrderError, PassiveOrderAny, StopOrderAny},
110 types::Price,
111};
112use smallvec::SmallVec;
113
114const INLINE_ORDERS_PER_LEVEL: usize = 4;
118
119type OrderBucket = SmallVec<[RestingOrder; INLINE_ORDERS_PER_LEVEL]>;
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum MatchAction {
124 FillLimit(ClientOrderId),
125 TriggerStop(ClientOrderId),
126}
127
128#[derive(Clone, Debug)]
131pub struct OrderMatchingCore {
132 pub instrument_id: InstrumentId,
134 pub price_increment: Price,
136 pub bid: Option<Price>,
138 pub ask: Option<Price>,
140 pub last: Option<Price>,
142 fill_limit_inside_spread: bool,
143 bid_limits: BTreeMap<Price, OrderBucket>,
144 ask_limits: BTreeMap<Price, OrderBucket>,
145 bid_stops: BTreeMap<Price, OrderBucket>,
146 ask_stops: BTreeMap<Price, OrderBucket>,
147 pending_bid: SmallVec<[RestingOrder; 2]>,
148 pending_ask: SmallVec<[RestingOrder; 2]>,
149 order_index: AHashMap<ClientOrderId, (OrderSide, Option<(BookKind, Price)>)>,
150}
151
152impl OrderMatchingCore {
153 #[must_use]
155 pub fn new(instrument_id: InstrumentId, price_increment: Price) -> Self {
156 Self {
157 instrument_id,
158 price_increment,
159 bid: None,
160 ask: None,
161 last: None,
162 fill_limit_inside_spread: false,
163 bid_limits: BTreeMap::new(),
164 ask_limits: BTreeMap::new(),
165 bid_stops: BTreeMap::new(),
166 ask_stops: BTreeMap::new(),
167 pending_bid: SmallVec::new(),
168 pending_ask: SmallVec::new(),
169 order_index: AHashMap::new(),
170 }
171 }
172
173 #[must_use]
175 pub const fn price_precision(&self) -> u8 {
176 self.price_increment.precision
177 }
178
179 #[must_use]
181 pub fn get_order(&self, client_order_id: ClientOrderId) -> Option<&RestingOrder> {
182 let (side, location) = self.order_index.get(&client_order_id).copied()?;
183 let orders: &[RestingOrder] = if let Some((kind, price)) = location {
184 self.book_for(side, kind).get(&price)?
185 } else {
186 self.pending_for(side)
187 };
188
189 orders.iter().find(|o| o.client_order_id == client_order_id)
190 }
191
192 pub fn iter_bid_orders(&self) -> impl Iterator<Item = &RestingOrder> {
197 self.iter_bid_book_orders().chain(self.pending_bid.iter())
198 }
199
200 pub fn iter_ask_orders(&self) -> impl Iterator<Item = &RestingOrder> {
205 self.iter_ask_book_orders().chain(self.pending_ask.iter())
206 }
207
208 pub fn iter_orders(&self) -> impl Iterator<Item = &RestingOrder> {
212 self.iter_bid_orders().chain(self.iter_ask_orders())
213 }
214
215 #[must_use]
220 pub fn get_orders_bid(&self) -> Vec<RestingOrder> {
221 self.iter_bid_orders().copied().collect()
222 }
223
224 #[must_use]
229 pub fn get_orders_ask(&self) -> Vec<RestingOrder> {
230 self.iter_ask_orders().copied().collect()
231 }
232
233 #[must_use]
237 pub fn get_orders(&self) -> Vec<RestingOrder> {
238 self.iter_orders().copied().collect()
239 }
240
241 #[must_use]
243 pub fn order_exists(&self, client_order_id: ClientOrderId) -> bool {
244 self.order_index.contains_key(&client_order_id)
245 }
246
247 pub const fn set_last_raw(&mut self, last: Price) {
249 self.last = Some(last);
250 }
251
252 pub const fn set_bid_raw(&mut self, bid: Price) {
254 self.bid = Some(bid);
255 }
256
257 pub const fn set_ask_raw(&mut self, ask: Price) {
259 self.ask = Some(ask);
260 }
261
262 pub const fn update_price_increment(&mut self, price_increment: Price) {
264 self.price_increment = price_increment;
265 }
266
267 pub fn reset(&mut self) {
269 self.bid = None;
270 self.ask = None;
271 self.last = None;
272 self.bid_limits.clear();
273 self.ask_limits.clear();
274 self.bid_stops.clear();
275 self.ask_stops.clear();
276 self.pending_bid.clear();
277 self.pending_ask.clear();
278 self.order_index.clear();
279 }
280
281 pub fn add_order(&mut self, order: RestingOrder) {
301 debug_assert!(
302 !self.order_exists(order.client_order_id),
303 "duplicate add_order for {}; caller must delete before re-adding",
304 order.client_order_id,
305 );
306
307 let side = order.order_side;
308 let client_order_id = order.client_order_id;
309 let location = Self::locate(&order);
310
311 if let Some((kind, price)) = location {
312 let book = self.book_for_mut(side, kind);
313 book.entry(price).or_default().push(order);
314 } else {
315 self.pending_for_mut(side).push(order);
316 }
317 self.order_index.insert(client_order_id, (side, location));
318 }
319
320 pub fn delete_order(&mut self, client_order_id: ClientOrderId) -> Result<(), OrderError> {
331 let Some((side, location)) = self.order_index.remove(&client_order_id) else {
332 return Err(OrderError::NotFound(client_order_id));
333 };
334
335 if let Some((kind, price)) = location {
336 let book = self.book_for_mut(side, kind);
337 let bucket = book
338 .get_mut(&price)
339 .expect("order_index points to existing bucket");
340 let pos = bucket
341 .iter()
342 .position(|o| o.client_order_id == client_order_id)
343 .expect("order_index points to existing slot");
344
345 bucket.remove(pos);
346 if bucket.is_empty() {
347 book.remove(&price);
348 }
349 } else {
350 let pending = self.pending_for_mut(side);
351 let pos = pending
352 .iter()
353 .position(|o| o.client_order_id == client_order_id)
354 .expect("order_index points to existing pending slot");
355 pending.remove(pos);
356 }
357 Ok(())
358 }
359
360 pub fn iterate(&self) -> Vec<MatchAction> {
363 let mut actions = self.iterate_bids();
364 actions.extend(self.iterate_asks());
365 actions
366 }
367
368 pub fn iterate_bids(&self) -> Vec<MatchAction> {
371 self.iter_bid_book_orders()
372 .filter_map(|order| self.match_order(order))
373 .collect()
374 }
375
376 pub fn iterate_asks(&self) -> Vec<MatchAction> {
379 self.iter_ask_book_orders()
380 .filter_map(|order| self.match_order(order))
381 .collect()
382 }
383
384 pub fn match_order(&self, order: &RestingOrder) -> Option<MatchAction> {
387 if order.is_stop() {
388 self.match_stop_order(order)
389 } else if order.is_limit() {
390 self.match_limit_order(order)
391 } else {
392 None
393 }
394 }
395
396 #[must_use]
399 pub fn is_limit_matched(&self, side: OrderSide, price: Price) -> bool {
400 match side {
401 OrderSide::Buy => self.ask.is_some_and(|a| a <= price),
402 OrderSide::Sell => self.bid.is_some_and(|b| b >= price),
403 }
404 }
405
406 #[must_use]
409 pub fn is_stop_matched(&self, side: OrderSide, price: Price) -> bool {
410 self.is_stop_matched_with_trigger_type(side, price, TriggerType::BidAsk)
411 }
412
413 #[must_use]
414 pub(crate) fn is_stop_matched_with_trigger_type(
415 &self,
416 side: OrderSide,
417 price: Price,
418 trigger_type: TriggerType,
419 ) -> bool {
420 self.market_price_for_trigger(side, trigger_type)
421 .is_some_and(|market_price| match side {
422 OrderSide::Buy => market_price >= price,
423 OrderSide::Sell => market_price <= price,
424 })
425 }
426
427 #[must_use]
430 pub fn is_touch_triggered(&self, side: OrderSide, trigger_price: Price) -> bool {
431 self.is_touch_triggered_with_trigger_type(side, trigger_price, TriggerType::BidAsk)
432 }
433
434 #[must_use]
435 pub(crate) fn is_touch_triggered_with_trigger_type(
436 &self,
437 side: OrderSide,
438 trigger_price: Price,
439 trigger_type: TriggerType,
440 ) -> bool {
441 self.market_price_for_trigger(side, trigger_type)
442 .is_some_and(|market_price| match side {
443 OrderSide::Buy => market_price <= trigger_price,
444 OrderSide::Sell => market_price >= trigger_price,
445 })
446 }
447
448 pub fn set_fill_limit_inside_spread(&mut self, value: bool) {
450 self.fill_limit_inside_spread = value;
451 }
452
453 #[must_use]
459 pub fn is_limit_fillable(&self, side: OrderSide, price: Price) -> bool {
460 if self.is_limit_matched(side, price) {
461 return true;
462 }
463
464 if !self.fill_limit_inside_spread {
465 return false;
466 }
467
468 if let (Some(bid), Some(ask)) = (self.bid, self.ask) {
469 match side {
470 OrderSide::Buy => price >= bid,
471 OrderSide::Sell => price <= ask,
472 }
473 } else {
474 false
475 }
476 }
477
478 fn book_for(&self, side: OrderSide, kind: BookKind) -> &BTreeMap<Price, OrderBucket> {
479 match (side, kind) {
480 (OrderSide::Buy, BookKind::Limit) => &self.bid_limits,
481 (OrderSide::Buy, BookKind::Stop) => &self.bid_stops,
482 (OrderSide::Sell, BookKind::Limit) => &self.ask_limits,
483 (OrderSide::Sell, BookKind::Stop) => &self.ask_stops,
484 }
485 }
486
487 fn book_for_mut(
488 &mut self,
489 side: OrderSide,
490 kind: BookKind,
491 ) -> &mut BTreeMap<Price, OrderBucket> {
492 match (side, kind) {
493 (OrderSide::Buy, BookKind::Limit) => &mut self.bid_limits,
494 (OrderSide::Buy, BookKind::Stop) => &mut self.bid_stops,
495 (OrderSide::Sell, BookKind::Limit) => &mut self.ask_limits,
496 (OrderSide::Sell, BookKind::Stop) => &mut self.ask_stops,
497 }
498 }
499
500 fn pending_for(&self, side: OrderSide) -> &[RestingOrder] {
501 match side {
502 OrderSide::Buy => &self.pending_bid,
503 OrderSide::Sell => &self.pending_ask,
504 }
505 }
506
507 fn pending_for_mut(&mut self, side: OrderSide) -> &mut SmallVec<[RestingOrder; 2]> {
508 match side {
509 OrderSide::Buy => &mut self.pending_bid,
510 OrderSide::Sell => &mut self.pending_ask,
511 }
512 }
513
514 fn iter_bid_book_orders(&self) -> impl Iterator<Item = &RestingOrder> {
515 self.bid_limits
516 .values()
517 .rev()
518 .flat_map(|bucket| bucket.iter())
519 .chain(self.bid_stops.values().flat_map(|bucket| bucket.iter()))
520 }
521
522 fn iter_ask_book_orders(&self) -> impl Iterator<Item = &RestingOrder> {
523 self.ask_limits
524 .values()
525 .flat_map(|bucket| bucket.iter())
526 .chain(
527 self.ask_stops
528 .values()
529 .rev()
530 .flat_map(|bucket| bucket.iter()),
531 )
532 }
533
534 fn locate(order: &RestingOrder) -> Option<(BookKind, Price)> {
535 if let Some(price) = order.trigger_price {
536 Some((BookKind::Stop, price))
537 } else {
538 order.limit_price.map(|price| (BookKind::Limit, price))
539 }
540 }
541
542 fn match_limit_order(&self, order: &RestingOrder) -> Option<MatchAction> {
543 if let Some(limit_price) = order.limit_price
544 && self.is_limit_fillable(order.order_side, limit_price)
545 {
546 Some(MatchAction::FillLimit(order.client_order_id))
547 } else {
548 None
549 }
550 }
551
552 fn match_stop_order(&self, order: &RestingOrder) -> Option<MatchAction> {
553 if !order.is_activated {
554 return None;
555 }
556
557 let trigger_price = order.trigger_price?;
558
559 let is_triggered = match order.order_type {
560 OrderType::MarketIfTouched | OrderType::LimitIfTouched => self
561 .is_touch_triggered_with_trigger_type(
562 order.order_side,
563 trigger_price,
564 order.trigger_type.unwrap_or(TriggerType::Default),
565 ),
566 _ => self.is_stop_matched_with_trigger_type(
567 order.order_side,
568 trigger_price,
569 order.trigger_type.unwrap_or(TriggerType::Default),
570 ),
571 };
572
573 if is_triggered {
574 Some(MatchAction::TriggerStop(order.client_order_id))
575 } else {
576 None
577 }
578 }
579
580 fn market_price_for_trigger(
581 &self,
582 side: OrderSide,
583 trigger_type: TriggerType,
584 ) -> Option<Price> {
585 let quote_price = match side {
586 OrderSide::Buy => self.ask,
587 OrderSide::Sell => self.bid,
588 };
589
590 match trigger_type {
591 TriggerType::LastPrice => self.last,
592 TriggerType::LastOrBidAsk => self.last.or(quote_price),
593 _ => quote_price,
594 }
595 }
596}
597
598#[derive(Clone, Copy, Debug, PartialEq, Eq)]
600pub struct RestingOrder {
601 pub client_order_id: ClientOrderId,
602 pub order_side: OrderSide,
603 pub order_type: OrderType,
604 pub trigger_type: Option<TriggerType>,
605 pub trigger_price: Option<Price>,
606 pub limit_price: Option<Price>,
607 pub is_activated: bool,
608}
609
610impl RestingOrder {
611 #[must_use]
618 pub const fn new(
619 client_order_id: ClientOrderId,
620 order_side: OrderSide,
621 order_type: OrderType,
622 trigger_price: Option<Price>,
623 limit_price: Option<Price>,
624 is_activated: bool,
625 ) -> Self {
626 Self::new_with_trigger_type(
627 client_order_id,
628 order_side,
629 order_type,
630 match trigger_price {
631 Some(_) => Some(TriggerType::Default),
632 None => None,
633 },
634 trigger_price,
635 limit_price,
636 is_activated,
637 )
638 }
639
640 #[must_use]
641 pub(crate) const fn new_with_trigger_type(
642 client_order_id: ClientOrderId,
643 order_side: OrderSide,
644 order_type: OrderType,
645 trigger_type: Option<TriggerType>,
646 trigger_price: Option<Price>,
647 limit_price: Option<Price>,
648 is_activated: bool,
649 ) -> Self {
650 Self {
651 client_order_id,
652 order_side,
653 order_type,
654 trigger_type,
655 trigger_price,
656 limit_price,
657 is_activated,
658 }
659 }
660
661 #[must_use]
663 pub const fn is_stop(&self) -> bool {
664 self.trigger_price.is_some()
665 }
666
667 #[must_use]
669 pub const fn is_limit(&self) -> bool {
670 self.limit_price.is_some() && self.trigger_price.is_none()
671 }
672}
673
674impl From<&PassiveOrderAny> for RestingOrder {
675 fn from(order: &PassiveOrderAny) -> Self {
676 match order {
677 PassiveOrderAny::Limit(limit) => Self {
678 client_order_id: limit.client_order_id(),
679 order_side: limit.order_side(),
680 order_type: limit.order_type(),
681 trigger_type: None,
682 trigger_price: None,
683 limit_price: Some(limit.limit_px()),
684 is_activated: true,
685 },
686 PassiveOrderAny::Stop(stop) => {
687 let limit_price = match stop {
688 StopOrderAny::LimitIfTouched(o) => Some(o.price),
689 StopOrderAny::StopLimit(o) => Some(o.price),
690 StopOrderAny::TrailingStopLimit(o) => o.price,
691 StopOrderAny::MarketIfTouched(_)
692 | StopOrderAny::StopMarket(_)
693 | StopOrderAny::TrailingStopMarket(_) => None,
694 };
695 let is_activated = match stop {
696 StopOrderAny::TrailingStopMarket(o) => o.is_activated,
697 StopOrderAny::TrailingStopLimit(o) => o.is_activated,
698 _ => true,
699 };
700 Self {
701 client_order_id: stop.client_order_id(),
702 order_side: stop.order_side(),
703 order_type: stop.order_type(),
704 trigger_type: Some(stop.trigger_type().unwrap_or(TriggerType::Default)),
705 trigger_price: stop.stop_px(),
706 limit_price,
707 is_activated,
708 }
709 }
710 }
711 }
712}
713
714#[derive(Debug, Clone, Copy, PartialEq, Eq)]
715enum BookKind {
716 Limit,
717 Stop,
718}
719
720#[cfg(test)]
721mod tests {
722 use nautilus_model::{
723 enums::{OrderSide, OrderType, TrailingOffsetType, TriggerType},
724 events::{OrderEventAny, OrderInitialized, order::spec::OrderInitializedSpec},
725 orders::{Order, OrderAny, builder::OrderTestBuilder},
726 types::Quantity,
727 };
728 use rstest::rstest;
729 use rust_decimal::Decimal;
730
731 use super::*;
732
733 fn create_matching_core(
734 instrument_id: InstrumentId,
735 price_increment: Price,
736 ) -> OrderMatchingCore {
737 OrderMatchingCore::new(instrument_id, price_increment)
738 }
739
740 #[rstest]
741 fn test_add_order_bid_side() {
742 let instrument_id = InstrumentId::from("AAPL.XNAS");
743 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
744
745 let order = OrderTestBuilder::new(OrderType::Limit)
746 .instrument_id(instrument_id)
747 .side(OrderSide::Buy)
748 .price(Price::from("100.00"))
749 .quantity(Quantity::from("100"))
750 .build();
751
752 let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
753 matching_core.add_order(match_info);
754
755 assert!(matching_core.get_orders_bid().contains(&match_info));
756 assert!(!matching_core.get_orders_ask().contains(&match_info));
757 assert_eq!(matching_core.get_orders_bid().len(), 1);
758 assert!(matching_core.get_orders_ask().is_empty());
759 assert!(matching_core.order_exists(match_info.client_order_id));
760 }
761
762 #[rstest]
763 fn test_add_order_ask_side() {
764 let instrument_id = InstrumentId::from("AAPL.XNAS");
765 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
766
767 let order = OrderTestBuilder::new(OrderType::Limit)
768 .instrument_id(instrument_id)
769 .side(OrderSide::Sell)
770 .price(Price::from("100.00"))
771 .quantity(Quantity::from("100"))
772 .build();
773
774 let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
775 matching_core.add_order(match_info);
776
777 assert!(matching_core.get_orders_ask().contains(&match_info));
778 assert!(!matching_core.get_orders_bid().contains(&match_info));
779 assert_eq!(matching_core.get_orders_ask().len(), 1);
780 assert!(matching_core.get_orders_bid().is_empty());
781 assert!(matching_core.order_exists(match_info.client_order_id));
782 }
783
784 #[rstest]
785 fn test_reset() {
786 let instrument_id = InstrumentId::from("AAPL.XNAS");
787 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
788
789 let orders = [
790 limit_order(OrderSide::Buy, "110.00", "O-B-LMT"),
791 stop_order(OrderSide::Buy, "101.00", "O-B-STP"),
792 pending_order(OrderSide::Buy, "O-B-PENDING"),
793 limit_order(OrderSide::Sell, "90.00", "O-A-LMT"),
794 stop_order(OrderSide::Sell, "99.00", "O-A-STP"),
795 pending_order(OrderSide::Sell, "O-A-PENDING"),
796 ];
797
798 for order in orders {
799 matching_core.add_order(order);
800 }
801 matching_core.set_bid_raw(Price::from("94.00"));
802 matching_core.set_ask_raw(Price::from("106.00"));
803 matching_core.set_last_raw(Price::from("100.00"));
804
805 assert_eq!(matching_core.get_orders(), orders);
806
807 matching_core.reset();
808
809 assert!(matching_core.bid.is_none());
810 assert!(matching_core.ask.is_none());
811 assert!(matching_core.last.is_none());
812 assert!(matching_core.get_orders_bid().is_empty());
813 assert!(matching_core.get_orders_ask().is_empty());
814 assert!(matching_core.get_orders().is_empty());
815 for order in orders {
816 assert_eq!(matching_core.get_order(order.client_order_id), None);
817 assert!(!matching_core.order_exists(order.client_order_id));
818 }
819
820 matching_core.set_bid_raw(Price::from("94.00"));
821 matching_core.set_ask_raw(Price::from("106.00"));
822
823 assert!(matching_core.iterate().is_empty());
824
825 for order in orders {
826 matching_core.add_order(order);
827 }
828
829 assert_eq!(matching_core.get_orders(), orders);
830 }
831
832 #[rstest]
833 fn test_delete_order_when_not_exists() {
834 let instrument_id = InstrumentId::from("AAPL.XNAS");
835 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
836
837 let order = OrderTestBuilder::new(OrderType::Limit)
838 .instrument_id(instrument_id)
839 .side(OrderSide::Buy)
840 .price(Price::from("100.00"))
841 .quantity(Quantity::from("100"))
842 .build();
843
844 let result = matching_core.delete_order(order.client_order_id());
845 assert!(result.is_err());
846 }
847
848 #[rstest]
849 #[case(OrderSide::Buy)]
850 #[case(OrderSide::Sell)]
851 fn test_delete_order_when_exists(#[case] order_side: OrderSide) {
852 let instrument_id = InstrumentId::from("AAPL.XNAS");
853 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
854
855 let order = OrderTestBuilder::new(OrderType::Limit)
856 .instrument_id(instrument_id)
857 .side(order_side)
858 .price(Price::from("100.00"))
859 .quantity(Quantity::from("100"))
860 .build();
861
862 let client_order_id = order.client_order_id();
863 let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
864 matching_core.add_order(match_info);
865 matching_core.delete_order(client_order_id).unwrap();
866
867 assert!(matching_core.get_orders_ask().is_empty());
868 assert!(matching_core.get_orders_bid().is_empty());
869 }
870
871 #[rstest]
872 #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
873 #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
874 #[case(
875 Some(Price::from("100.00")),
876 Some(Price::from("101.00")),
877 Price::from("100.00"),
878 OrderSide::Buy,
879 false
880 )]
881 #[case(
882 Some(Price::from("100.00")),
883 Some(Price::from("101.00")),
884 Price::from("101.00"),
885 OrderSide::Buy,
886 true
887 )]
888 #[case(
889 Some(Price::from("100.00")),
890 Some(Price::from("101.00")),
891 Price::from("102.00"),
892 OrderSide::Buy,
893 true
894 )]
895 #[case(
896 Some(Price::from("100.00")),
897 Some(Price::from("101.00")),
898 Price::from("101.00"),
899 OrderSide::Sell,
900 false
901 )]
902 #[case(
903 Some(Price::from("100.00")),
904 Some(Price::from("101.00")),
905 Price::from("100.00"),
906 OrderSide::Sell,
907 true
908 )]
909 #[case(
910 Some(Price::from("100.00")),
911 Some(Price::from("101.00")),
912 Price::from("99.00"),
913 OrderSide::Sell,
914 true
915 )]
916 fn test_is_limit_matched(
917 #[case] bid: Option<Price>,
918 #[case] ask: Option<Price>,
919 #[case] price: Price,
920 #[case] order_side: OrderSide,
921 #[case] expected: bool,
922 ) {
923 let instrument_id = InstrumentId::from("AAPL.XNAS");
924 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
925 matching_core.bid = bid;
926 matching_core.ask = ask;
927
928 let order = OrderTestBuilder::new(OrderType::Limit)
929 .instrument_id(instrument_id)
930 .side(order_side)
931 .price(price)
932 .quantity(Quantity::from("100"))
933 .build();
934
935 let result = matching_core.is_limit_matched(order.order_side(), order.price().unwrap());
936 assert_eq!(result, expected);
937 }
938
939 #[rstest]
940 #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
941 #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
942 #[case(
943 Some(Price::from("100.00")),
944 Some(Price::from("101.00")),
945 Price::from("102.00"),
946 OrderSide::Buy,
947 false
948 )]
949 #[case(
950 Some(Price::from("100.00")),
951 Some(Price::from("101.00")),
952 Price::from("101.00"),
953 OrderSide::Buy,
954 true
955 )]
956 #[case(
957 Some(Price::from("100.00")),
958 Some(Price::from("101.00")),
959 Price::from("100.00"),
960 OrderSide::Buy,
961 true
962 )]
963 #[case(
964 Some(Price::from("100.00")),
965 Some(Price::from("101.00")),
966 Price::from("99.00"),
967 OrderSide::Sell,
968 false
969 )]
970 #[case(
971 Some(Price::from("100.00")),
972 Some(Price::from("101.00")),
973 Price::from("100.00"),
974 OrderSide::Sell,
975 true
976 )]
977 #[case(
978 Some(Price::from("100.00")),
979 Some(Price::from("101.00")),
980 Price::from("101.00"),
981 OrderSide::Sell,
982 true
983 )]
984 fn test_is_stop_matched(
985 #[case] bid: Option<Price>,
986 #[case] ask: Option<Price>,
987 #[case] trigger_price: Price,
988 #[case] order_side: OrderSide,
989 #[case] expected: bool,
990 ) {
991 let instrument_id = InstrumentId::from("AAPL.XNAS");
992 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
993 matching_core.bid = bid;
994 matching_core.ask = ask;
995
996 let order = OrderTestBuilder::new(OrderType::StopMarket)
997 .instrument_id(instrument_id)
998 .side(order_side)
999 .trigger_price(trigger_price)
1000 .quantity(Quantity::from("100"))
1001 .build();
1002
1003 let result =
1004 matching_core.is_stop_matched(order.order_side(), order.trigger_price().unwrap());
1005 assert_eq!(result, expected);
1006 }
1007
1008 #[rstest]
1009 #[case::last_price_below_trigger(Some(Price::from("99.00")), TriggerType::LastPrice, false)]
1010 #[case::last_price_at_trigger(Some(Price::from("100.00")), TriggerType::LastPrice, true)]
1011 #[case::last_price_unavailable(None, TriggerType::LastPrice, false)]
1012 #[case::last_or_bid_ask_prefers_last(
1013 Some(Price::from("99.00")),
1014 TriggerType::LastOrBidAsk,
1015 false
1016 )]
1017 #[case::last_or_bid_ask_falls_back_to_quote(None, TriggerType::LastOrBidAsk, true)]
1018 #[case::bid_ask_uses_quote(Some(Price::from("99.00")), TriggerType::BidAsk, true)]
1019 fn test_is_stop_matched_uses_trigger_type(
1020 #[case] last: Option<Price>,
1021 #[case] trigger_type: TriggerType,
1022 #[case] expected: bool,
1023 ) {
1024 let instrument_id = InstrumentId::from("AAPL.XNAS");
1025 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1026 matching_core.ask = Some(Price::from("101.00"));
1027 matching_core.last = last;
1028
1029 let result = matching_core.is_stop_matched_with_trigger_type(
1030 OrderSide::Buy,
1031 Price::from("100.00"),
1032 trigger_type,
1033 );
1034
1035 assert_eq!(result, expected);
1036 }
1037
1038 #[rstest]
1039 fn test_iterate_returns_empty_when_no_orders() {
1040 let instrument_id = InstrumentId::from("AAPL.XNAS");
1041 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1042 matching_core.set_bid_raw(Price::from("100.00"));
1043 matching_core.set_ask_raw(Price::from("101.00"));
1044
1045 let actions = matching_core.iterate();
1046
1047 assert!(actions.is_empty());
1048 }
1049
1050 #[rstest]
1051 fn test_iterate_returns_empty_when_no_market_data() {
1052 let instrument_id = InstrumentId::from("AAPL.XNAS");
1053 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1054
1055 let order = OrderTestBuilder::new(OrderType::Limit)
1056 .instrument_id(instrument_id)
1057 .side(OrderSide::Buy)
1058 .price(Price::from("100.00"))
1059 .quantity(Quantity::from("100"))
1060 .build();
1061 let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1062 matching_core.add_order(match_info);
1063
1064 let actions = matching_core.iterate();
1065
1066 assert!(actions.is_empty());
1067 }
1068
1069 #[rstest]
1070 fn test_iterate_returns_fill_limit_for_matched_buy() {
1071 let instrument_id = InstrumentId::from("AAPL.XNAS");
1072 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1073 matching_core.set_ask_raw(Price::from("100.00"));
1074
1075 let order = OrderTestBuilder::new(OrderType::Limit)
1076 .instrument_id(instrument_id)
1077 .side(OrderSide::Buy)
1078 .price(Price::from("100.00"))
1079 .quantity(Quantity::from("100"))
1080 .build();
1081 let client_order_id = order.client_order_id();
1082 let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1083 matching_core.add_order(match_info);
1084
1085 let actions = matching_core.iterate();
1086
1087 assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
1088 }
1089
1090 #[rstest]
1091 fn test_iterate_returns_fill_limit_for_matched_sell() {
1092 let instrument_id = InstrumentId::from("AAPL.XNAS");
1093 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1094 matching_core.set_bid_raw(Price::from("100.00"));
1095
1096 let order = OrderTestBuilder::new(OrderType::Limit)
1097 .instrument_id(instrument_id)
1098 .side(OrderSide::Sell)
1099 .price(Price::from("100.00"))
1100 .quantity(Quantity::from("100"))
1101 .build();
1102 let client_order_id = order.client_order_id();
1103 let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1104 matching_core.add_order(match_info);
1105
1106 let actions = matching_core.iterate();
1107
1108 assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
1109 }
1110
1111 #[rstest]
1112 fn test_iterate_returns_no_fill_for_unmatched_limit() {
1113 let instrument_id = InstrumentId::from("AAPL.XNAS");
1114 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1115 matching_core.set_ask_raw(Price::from("101.00"));
1116
1117 let order = OrderTestBuilder::new(OrderType::Limit)
1118 .instrument_id(instrument_id)
1119 .side(OrderSide::Buy)
1120 .price(Price::from("100.00"))
1121 .quantity(Quantity::from("100"))
1122 .build();
1123 let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1124 matching_core.add_order(match_info);
1125
1126 let actions = matching_core.iterate();
1127
1128 assert!(actions.is_empty());
1129 }
1130
1131 #[rstest]
1132 fn test_iterate_returns_trigger_stop_for_matched_buy() {
1133 let instrument_id = InstrumentId::from("AAPL.XNAS");
1134 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1135 matching_core.set_ask_raw(Price::from("101.00"));
1136
1137 let order = OrderTestBuilder::new(OrderType::StopMarket)
1138 .instrument_id(instrument_id)
1139 .side(OrderSide::Buy)
1140 .trigger_price(Price::from("101.00"))
1141 .trigger_type(TriggerType::Default)
1142 .quantity(Quantity::from("100"))
1143 .build();
1144 let client_order_id = order.client_order_id();
1145 let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1146 matching_core.add_order(match_info);
1147
1148 let actions = matching_core.iterate();
1149
1150 assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
1151 }
1152
1153 #[rstest]
1154 fn test_iterate_returns_trigger_stop_for_matched_sell() {
1155 let instrument_id = InstrumentId::from("AAPL.XNAS");
1156 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1157 matching_core.set_bid_raw(Price::from("99.00"));
1158
1159 let order = OrderTestBuilder::new(OrderType::StopMarket)
1160 .instrument_id(instrument_id)
1161 .side(OrderSide::Sell)
1162 .trigger_price(Price::from("99.00"))
1163 .quantity(Quantity::from("100"))
1164 .build();
1165 let client_order_id = order.client_order_id();
1166 let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1167 matching_core.add_order(match_info);
1168
1169 let actions = matching_core.iterate();
1170
1171 assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
1172 }
1173
1174 #[rstest]
1175 fn test_iterate_skips_unactivated_stop_order() {
1176 let instrument_id = InstrumentId::from("AAPL.XNAS");
1177 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1178 matching_core.set_ask_raw(Price::from("110.00"));
1179
1180 let match_info = RestingOrder::new(
1181 ClientOrderId::from("O-001"),
1182 OrderSide::Buy,
1183 OrderType::TrailingStopMarket,
1184 Some(Price::from("105.00")),
1185 None,
1186 false,
1187 );
1188 matching_core.add_order(match_info);
1189
1190 let actions = matching_core.iterate();
1191
1192 assert!(actions.is_empty());
1193 }
1194
1195 #[rstest]
1196 fn test_iterate_triggers_activated_stop_order() {
1197 let instrument_id = InstrumentId::from("AAPL.XNAS");
1198 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1199 matching_core.set_ask_raw(Price::from("110.00"));
1200
1201 let client_order_id = ClientOrderId::from("O-001");
1202 let match_info = RestingOrder::new(
1203 client_order_id,
1204 OrderSide::Buy,
1205 OrderType::TrailingStopMarket,
1206 Some(Price::from("105.00")),
1207 None,
1208 true,
1209 );
1210 matching_core.add_order(match_info);
1211
1212 let actions = matching_core.iterate();
1213
1214 assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
1215 }
1216
1217 #[rstest]
1218 fn test_iterate_returns_mixed_actions_for_limits_and_stops() {
1219 let instrument_id = InstrumentId::from("AAPL.XNAS");
1220 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1221 matching_core.set_bid_raw(Price::from("99.00"));
1222 matching_core.set_ask_raw(Price::from("101.00"));
1223
1224 let buy_limit = OrderTestBuilder::new(OrderType::Limit)
1225 .instrument_id(instrument_id)
1226 .side(OrderSide::Buy)
1227 .price(Price::from("101.00"))
1228 .quantity(Quantity::from("100"))
1229 .client_order_id(ClientOrderId::from("O-BUY-LIMIT"))
1230 .build();
1231 let buy_limit_id = buy_limit.client_order_id();
1232 matching_core.add_order(RestingOrder::from(
1233 &PassiveOrderAny::try_from(buy_limit).unwrap(),
1234 ));
1235
1236 let sell_stop = OrderTestBuilder::new(OrderType::StopMarket)
1237 .instrument_id(instrument_id)
1238 .side(OrderSide::Sell)
1239 .trigger_price(Price::from("99.00"))
1240 .quantity(Quantity::from("50"))
1241 .client_order_id(ClientOrderId::from("O-SELL-STOP"))
1242 .build();
1243 let sell_stop_id = sell_stop.client_order_id();
1244 matching_core.add_order(RestingOrder::from(
1245 &PassiveOrderAny::try_from(sell_stop).unwrap(),
1246 ));
1247
1248 let actions = matching_core.iterate();
1249
1250 assert_eq!(actions.len(), 2);
1251 assert_eq!(actions[0], MatchAction::FillLimit(buy_limit_id));
1252 assert_eq!(actions[1], MatchAction::TriggerStop(sell_stop_id));
1253 }
1254
1255 #[rstest]
1256 fn test_is_limit_fillable_delegates_to_is_limit_matched_by_default() {
1257 let instrument_id = InstrumentId::from("AAPL.XNAS");
1258 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1259 core.set_bid_raw(Price::from("100.00"));
1260 core.set_ask_raw(Price::from("101.00"));
1261
1262 assert!(core.is_limit_fillable(OrderSide::Buy, Price::from("101.00")));
1263 assert!(!core.is_limit_fillable(OrderSide::Buy, Price::from("100.00")));
1264 assert!(core.is_limit_fillable(OrderSide::Sell, Price::from("100.00")));
1265 assert!(!core.is_limit_fillable(OrderSide::Sell, Price::from("101.00")));
1266 }
1267
1268 #[rstest]
1269 fn test_is_limit_fillable_inside_spread_buy_at_bid() {
1270 let instrument_id = InstrumentId::from("AAPL.XNAS");
1271 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1272 core.set_bid_raw(Price::from("100.00"));
1273 core.set_ask_raw(Price::from("101.00"));
1274 core.set_fill_limit_inside_spread(true);
1275
1276 assert!(core.is_limit_fillable(OrderSide::Buy, Price::from("100.00")));
1277 assert!(core.is_limit_fillable(OrderSide::Buy, Price::from("100.50")));
1278 assert!(!core.is_limit_fillable(OrderSide::Buy, Price::from("99.00")));
1279 }
1280
1281 #[rstest]
1282 fn test_is_limit_fillable_inside_spread_sell_at_ask() {
1283 let instrument_id = InstrumentId::from("AAPL.XNAS");
1284 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1285 core.set_bid_raw(Price::from("100.00"));
1286 core.set_ask_raw(Price::from("101.00"));
1287 core.set_fill_limit_inside_spread(true);
1288
1289 assert!(core.is_limit_fillable(OrderSide::Sell, Price::from("101.00")));
1290 assert!(core.is_limit_fillable(OrderSide::Sell, Price::from("100.50")));
1291 assert!(!core.is_limit_fillable(OrderSide::Sell, Price::from("102.00")));
1292 }
1293
1294 #[rstest]
1295 fn test_is_limit_fillable_inside_spread_requires_both_quotes_present() {
1296 let instrument_id = InstrumentId::from("AAPL.XNAS");
1297 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1298 core.set_fill_limit_inside_spread(true);
1299
1300 core.set_bid_raw(Price::from("100.00"));
1301 assert!(!core.is_limit_fillable(OrderSide::Buy, Price::from("100.00")));
1302
1303 let mut core2 = create_matching_core(instrument_id, Price::from("0.01"));
1304 core2.set_fill_limit_inside_spread(true);
1305 core2.set_ask_raw(Price::from("101.00"));
1306 assert!(!core2.is_limit_fillable(OrderSide::Sell, Price::from("101.00")));
1307
1308 let mut core3 = create_matching_core(instrument_id, Price::from("0.01"));
1309 core3.set_fill_limit_inside_spread(true);
1310 core3.set_bid_raw(Price::from("100.00"));
1311 core3.set_ask_raw(Price::from("101.00"));
1312 core3.ask = None;
1313 assert!(!core3.is_limit_fillable(OrderSide::Buy, Price::from("100.00")));
1314 }
1315
1316 #[rstest]
1317 fn test_iterate_fills_limit_inside_spread_when_enabled() {
1318 let instrument_id = InstrumentId::from("AAPL.XNAS");
1319 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1320 core.set_bid_raw(Price::from("100.00"));
1321 core.set_ask_raw(Price::from("101.00"));
1322 core.set_fill_limit_inside_spread(true);
1323
1324 let order = OrderTestBuilder::new(OrderType::Limit)
1325 .instrument_id(instrument_id)
1326 .side(OrderSide::Buy)
1327 .price(Price::from("100.00"))
1328 .quantity(Quantity::from("100"))
1329 .build();
1330 let client_order_id = order.client_order_id();
1331 let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1332 core.add_order(match_info);
1333
1334 let actions = core.iterate();
1335 assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
1336 }
1337
1338 #[rstest]
1339 #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
1340 #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
1341 #[case(
1342 Some(Price::from("100.00")),
1343 Some(Price::from("101.00")),
1344 Price::from("102.00"),
1345 OrderSide::Buy,
1346 true
1347 )]
1348 #[case(
1349 Some(Price::from("100.00")),
1350 Some(Price::from("101.00")),
1351 Price::from("101.00"),
1352 OrderSide::Buy,
1353 true
1354 )]
1355 #[case(
1356 Some(Price::from("100.00")),
1357 Some(Price::from("101.00")),
1358 Price::from("100.00"),
1359 OrderSide::Buy,
1360 false
1361 )]
1362 #[case(
1363 Some(Price::from("100.00")),
1364 Some(Price::from("101.00")),
1365 Price::from("99.00"),
1366 OrderSide::Sell,
1367 true
1368 )]
1369 #[case(
1370 Some(Price::from("100.00")),
1371 Some(Price::from("101.00")),
1372 Price::from("100.00"),
1373 OrderSide::Sell,
1374 true
1375 )]
1376 #[case(
1377 Some(Price::from("100.00")),
1378 Some(Price::from("101.00")),
1379 Price::from("101.00"),
1380 OrderSide::Sell,
1381 false
1382 )]
1383 fn test_is_touch_triggered(
1384 #[case] bid: Option<Price>,
1385 #[case] ask: Option<Price>,
1386 #[case] trigger_price: Price,
1387 #[case] order_side: OrderSide,
1388 #[case] expected: bool,
1389 ) {
1390 let instrument_id = InstrumentId::from("AAPL.XNAS");
1391 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1392 matching_core.bid = bid;
1393 matching_core.ask = ask;
1394
1395 let result = matching_core.is_touch_triggered(order_side, trigger_price);
1396 assert_eq!(result, expected);
1397 }
1398
1399 #[rstest]
1400 fn test_update_price_increment_updates_increment_and_precision() {
1401 let instrument_id = InstrumentId::from("AAPL.XNAS");
1402 let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
1403
1404 assert_eq!(matching_core.price_increment, Price::from("0.01"));
1405 assert_eq!(matching_core.price_precision(), 2);
1406
1407 matching_core.update_price_increment(Price::from("0.001"));
1408
1409 assert_eq!(matching_core.price_increment, Price::from("0.001"));
1410 assert_eq!(matching_core.price_precision(), 3);
1411 }
1412
1413 fn order_from_init(spec: OrderInitialized) -> OrderAny {
1414 OrderAny::from_events(vec![OrderEventAny::Initialized(spec)]).unwrap()
1415 }
1416
1417 #[rstest]
1418 fn test_get_order_finds_orders_on_either_side() {
1419 let instrument_id = InstrumentId::from("AAPL.XNAS");
1420 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1421
1422 let buy = order_from_init(
1423 OrderInitializedSpec::builder()
1424 .instrument_id(instrument_id)
1425 .client_order_id(ClientOrderId::from("O-BUY"))
1426 .order_side(OrderSide::Buy)
1427 .order_type(OrderType::Limit)
1428 .quantity(Quantity::from("10"))
1429 .price(Price::from("100.00"))
1430 .build(),
1431 );
1432 let buy_id = buy.client_order_id();
1433 core.add_order(RestingOrder::from(&PassiveOrderAny::try_from(buy).unwrap()));
1434
1435 let sell = order_from_init(
1436 OrderInitializedSpec::builder()
1437 .instrument_id(instrument_id)
1438 .client_order_id(ClientOrderId::from("O-SELL"))
1439 .order_side(OrderSide::Sell)
1440 .order_type(OrderType::Limit)
1441 .quantity(Quantity::from("10"))
1442 .price(Price::from("101.00"))
1443 .build(),
1444 );
1445 let sell_id = sell.client_order_id();
1446 core.add_order(RestingOrder::from(
1447 &PassiveOrderAny::try_from(sell).unwrap(),
1448 ));
1449
1450 assert_eq!(
1451 core.get_order(buy_id).map(|o| o.client_order_id),
1452 Some(buy_id)
1453 );
1454 assert_eq!(
1455 core.get_order(sell_id).map(|o| o.client_order_id),
1456 Some(sell_id)
1457 );
1458 assert!(core.get_order(ClientOrderId::from("O-MISSING")).is_none());
1459 }
1460
1461 #[rstest]
1462 fn test_match_order_returns_none_when_neither_price_set() {
1463 let instrument_id = InstrumentId::from("AAPL.XNAS");
1464 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1465 core.set_bid_raw(Price::from("100.00"));
1466 core.set_ask_raw(Price::from("101.00"));
1467
1468 let info = RestingOrder::new(
1469 ClientOrderId::from("O-NEITHER"),
1470 OrderSide::Buy,
1471 OrderType::MarketToLimit,
1472 None,
1473 None,
1474 true,
1475 );
1476 assert!(core.match_order(&info).is_none());
1477 }
1478
1479 #[rstest]
1480 fn test_from_passive_order_extracts_limit_price_for_stop_limit() {
1481 let order = order_from_init(
1482 OrderInitializedSpec::builder()
1483 .order_type(OrderType::StopLimit)
1484 .order_side(OrderSide::Buy)
1485 .quantity(Quantity::from("10"))
1486 .price(Price::from("101.00"))
1487 .trigger_price(Price::from("100.00"))
1488 .trigger_type(TriggerType::Default)
1489 .build(),
1490 );
1491
1492 let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1493
1494 assert_eq!(info.trigger_price, Some(Price::from("100.00")));
1495 assert_eq!(info.limit_price, Some(Price::from("101.00")));
1496 assert!(info.is_activated);
1497 }
1498
1499 #[rstest]
1500 fn test_from_passive_order_extracts_limit_price_for_limit_if_touched() {
1501 let order = order_from_init(
1502 OrderInitializedSpec::builder()
1503 .order_type(OrderType::LimitIfTouched)
1504 .order_side(OrderSide::Sell)
1505 .quantity(Quantity::from("10"))
1506 .price(Price::from("99.00"))
1507 .trigger_price(Price::from("100.00"))
1508 .trigger_type(TriggerType::LastPrice)
1509 .build(),
1510 );
1511
1512 let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1513
1514 assert_eq!(info.trigger_price, Some(Price::from("100.00")));
1515 assert_eq!(info.limit_price, Some(Price::from("99.00")));
1516 assert_eq!(info.trigger_type, Some(TriggerType::LastPrice));
1517 assert!(info.is_activated);
1518 }
1519
1520 #[rstest]
1521 fn test_from_passive_order_extracts_is_activated_for_trailing_stop_market() {
1522 let order = order_from_init(
1523 OrderInitializedSpec::builder()
1524 .order_type(OrderType::TrailingStopMarket)
1525 .order_side(OrderSide::Buy)
1526 .quantity(Quantity::from("10"))
1527 .trigger_price(Price::from("101.00"))
1528 .trigger_type(TriggerType::Default)
1529 .trailing_offset(Decimal::from(1))
1530 .trailing_offset_type(TrailingOffsetType::Price)
1531 .build(),
1532 );
1533
1534 let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1535
1536 assert_eq!(info.trigger_price, Some(Price::from("101.00")));
1537 assert_eq!(info.limit_price, None);
1538 assert!(!info.is_activated);
1539 }
1540
1541 #[rstest]
1542 fn test_from_passive_order_extracts_limit_and_is_activated_for_trailing_stop_limit() {
1543 let order = order_from_init(
1544 OrderInitializedSpec::builder()
1545 .order_type(OrderType::TrailingStopLimit)
1546 .order_side(OrderSide::Sell)
1547 .quantity(Quantity::from("10"))
1548 .price(Price::from("99.00"))
1549 .trigger_price(Price::from("100.00"))
1550 .trigger_type(TriggerType::Default)
1551 .limit_offset(Decimal::from(1))
1552 .trailing_offset(Decimal::from(1))
1553 .trailing_offset_type(TrailingOffsetType::Price)
1554 .build(),
1555 );
1556
1557 let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
1558
1559 assert_eq!(info.trigger_price, Some(Price::from("100.00")));
1560 assert_eq!(info.limit_price, Some(Price::from("99.00")));
1561 assert!(!info.is_activated);
1562 }
1563
1564 fn limit_order(side: OrderSide, price: &str, id: &str) -> RestingOrder {
1565 let order = order_from_init(
1566 OrderInitializedSpec::builder()
1567 .client_order_id(ClientOrderId::from(id))
1568 .order_type(OrderType::Limit)
1569 .order_side(side)
1570 .quantity(Quantity::from("10"))
1571 .price(Price::from(price))
1572 .build(),
1573 );
1574 RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap())
1575 }
1576
1577 fn stop_order(side: OrderSide, trigger: &str, id: &str) -> RestingOrder {
1578 let order = order_from_init(
1579 OrderInitializedSpec::builder()
1580 .client_order_id(ClientOrderId::from(id))
1581 .order_type(OrderType::StopMarket)
1582 .order_side(side)
1583 .quantity(Quantity::from("10"))
1584 .trigger_price(Price::from(trigger))
1585 .trigger_type(TriggerType::Default)
1586 .build(),
1587 );
1588 RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap())
1589 }
1590
1591 fn stop_limit_order(side: OrderSide, trigger: &str, limit: &str, id: &str) -> RestingOrder {
1592 let order = order_from_init(
1593 OrderInitializedSpec::builder()
1594 .client_order_id(ClientOrderId::from(id))
1595 .order_type(OrderType::StopLimit)
1596 .order_side(side)
1597 .quantity(Quantity::from("10"))
1598 .price(Price::from(limit))
1599 .trigger_price(Price::from(trigger))
1600 .trigger_type(TriggerType::Default)
1601 .build(),
1602 );
1603 RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap())
1604 }
1605
1606 #[rstest]
1607 fn test_iterate_bids_returns_limits_in_descending_price_order() {
1608 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1609 core.set_ask_raw(Price::from("99.00"));
1610
1611 core.add_order(limit_order(OrderSide::Buy, "100.00", "O-MID"));
1612 core.add_order(limit_order(OrderSide::Buy, "100.50", "O-HIGH"));
1613 core.add_order(limit_order(OrderSide::Buy, "99.50", "O-LOW"));
1614
1615 let actions = core.iterate_bids();
1616 assert_eq!(
1617 actions,
1618 vec![
1619 MatchAction::FillLimit(ClientOrderId::from("O-HIGH")),
1620 MatchAction::FillLimit(ClientOrderId::from("O-MID")),
1621 MatchAction::FillLimit(ClientOrderId::from("O-LOW")),
1622 ],
1623 );
1624 }
1625
1626 #[rstest]
1627 fn test_iterate_asks_returns_limits_in_ascending_price_order() {
1628 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1629 core.set_bid_raw(Price::from("101.00"));
1630
1631 core.add_order(limit_order(OrderSide::Sell, "100.50", "O-MID"));
1632 core.add_order(limit_order(OrderSide::Sell, "100.00", "O-LOW"));
1633 core.add_order(limit_order(OrderSide::Sell, "100.75", "O-HIGH"));
1634
1635 let actions = core.iterate_asks();
1636 assert_eq!(
1637 actions,
1638 vec![
1639 MatchAction::FillLimit(ClientOrderId::from("O-LOW")),
1640 MatchAction::FillLimit(ClientOrderId::from("O-MID")),
1641 MatchAction::FillLimit(ClientOrderId::from("O-HIGH")),
1642 ],
1643 );
1644 }
1645
1646 #[rstest]
1647 fn test_iterate_limits_preserves_fifo_within_same_price() {
1648 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1649 core.set_ask_raw(Price::from("99.00"));
1650
1651 for id in ["O-1", "O-2", "O-3", "O-4"] {
1652 core.add_order(limit_order(OrderSide::Buy, "100.00", id));
1653 }
1654
1655 let actions = core.iterate_bids();
1656 assert_eq!(
1657 actions,
1658 vec![
1659 MatchAction::FillLimit(ClientOrderId::from("O-1")),
1660 MatchAction::FillLimit(ClientOrderId::from("O-2")),
1661 MatchAction::FillLimit(ClientOrderId::from("O-3")),
1662 MatchAction::FillLimit(ClientOrderId::from("O-4")),
1663 ],
1664 );
1665 }
1666
1667 #[rstest]
1668 fn test_buy_stops_trigger_in_ascending_price_order_when_ask_crosses_multiple() {
1669 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1670 core.set_ask_raw(Price::from("106.00"));
1671
1672 core.add_order(stop_order(OrderSide::Buy, "105.00", "O-FAR"));
1673 core.add_order(stop_order(OrderSide::Buy, "101.00", "O-NEAR"));
1674
1675 let actions = core.iterate_bids();
1676 assert_eq!(
1677 actions,
1678 vec![
1679 MatchAction::TriggerStop(ClientOrderId::from("O-NEAR")),
1680 MatchAction::TriggerStop(ClientOrderId::from("O-FAR")),
1681 ],
1682 );
1683 }
1684
1685 #[rstest]
1686 fn test_sell_stops_trigger_in_descending_price_order_when_bid_crosses_multiple() {
1687 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1688 core.set_bid_raw(Price::from("94.00"));
1689
1690 core.add_order(stop_order(OrderSide::Sell, "95.00", "O-FAR"));
1691 core.add_order(stop_order(OrderSide::Sell, "99.00", "O-NEAR"));
1692
1693 let actions = core.iterate_asks();
1694 assert_eq!(
1695 actions,
1696 vec![
1697 MatchAction::TriggerStop(ClientOrderId::from("O-NEAR")),
1698 MatchAction::TriggerStop(ClientOrderId::from("O-FAR")),
1699 ],
1700 );
1701 }
1702
1703 #[rstest]
1704 fn test_iterate_stops_preserves_fifo_within_same_trigger() {
1705 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1706 core.set_ask_raw(Price::from("106.00"));
1707
1708 for id in ["O-S1", "O-S2", "O-S3"] {
1709 core.add_order(stop_order(OrderSide::Buy, "101.00", id));
1710 }
1711
1712 let actions = core.iterate_bids();
1713 assert_eq!(
1714 actions,
1715 vec![
1716 MatchAction::TriggerStop(ClientOrderId::from("O-S1")),
1717 MatchAction::TriggerStop(ClientOrderId::from("O-S2")),
1718 MatchAction::TriggerStop(ClientOrderId::from("O-S3")),
1719 ],
1720 );
1721 }
1722
1723 #[rstest]
1724 fn test_iterate_bids_processes_limits_before_stops() {
1725 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1726 core.set_ask_raw(Price::from("106.00"));
1727
1728 core.add_order(limit_order(OrderSide::Buy, "110.00", "O-LMT"));
1729 core.add_order(stop_order(OrderSide::Buy, "101.00", "O-STP"));
1730
1731 let actions = core.iterate_bids();
1732 assert_eq!(
1733 actions,
1734 vec![
1735 MatchAction::FillLimit(ClientOrderId::from("O-LMT")),
1736 MatchAction::TriggerStop(ClientOrderId::from("O-STP")),
1737 ],
1738 );
1739 }
1740
1741 #[rstest]
1742 fn test_iterate_asks_processes_limits_before_stops() {
1743 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1744 core.set_bid_raw(Price::from("94.00"));
1745
1746 core.add_order(limit_order(OrderSide::Sell, "90.00", "O-LMT"));
1747 core.add_order(stop_order(OrderSide::Sell, "99.00", "O-STP"));
1748
1749 let actions = core.iterate_asks();
1750 assert_eq!(
1751 actions,
1752 vec![
1753 MatchAction::FillLimit(ClientOrderId::from("O-LMT")),
1754 MatchAction::TriggerStop(ClientOrderId::from("O-STP")),
1755 ],
1756 );
1757 }
1758
1759 #[rstest]
1760 fn test_stop_limit_routed_to_stop_book_keyed_by_trigger() {
1761 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1762 core.set_ask_raw(Price::from("106.00"));
1763
1764 core.add_order(stop_limit_order(
1766 OrderSide::Buy,
1767 "105.00",
1768 "110.00",
1769 "O-FAR",
1770 ));
1771 core.add_order(stop_limit_order(
1772 OrderSide::Buy,
1773 "101.00",
1774 "110.00",
1775 "O-NEAR",
1776 ));
1777
1778 let actions = core.iterate_bids();
1779 assert_eq!(
1780 actions,
1781 vec![
1782 MatchAction::TriggerStop(ClientOrderId::from("O-NEAR")),
1783 MatchAction::TriggerStop(ClientOrderId::from("O-FAR")),
1784 ],
1785 );
1786 }
1787
1788 #[rstest]
1789 fn test_iterate_full_walk_combines_bids_then_asks_each_with_limits_then_stops() {
1790 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1792 core.set_bid_raw(Price::from("94.00"));
1793 core.set_ask_raw(Price::from("106.00"));
1794
1795 core.add_order(limit_order(OrderSide::Buy, "110.00", "O-B-LMT-HIGH"));
1796 core.add_order(limit_order(OrderSide::Buy, "107.00", "O-B-LMT-LOW"));
1797 core.add_order(stop_order(OrderSide::Buy, "105.00", "O-B-STP-FAR"));
1798 core.add_order(stop_order(OrderSide::Buy, "101.00", "O-B-STP-NEAR"));
1799
1800 core.add_order(limit_order(OrderSide::Sell, "90.00", "O-A-LMT-LOW"));
1801 core.add_order(limit_order(OrderSide::Sell, "93.00", "O-A-LMT-HIGH"));
1802 core.add_order(stop_order(OrderSide::Sell, "95.00", "O-A-STP-FAR"));
1803 core.add_order(stop_order(OrderSide::Sell, "99.00", "O-A-STP-NEAR"));
1804
1805 let actions = core.iterate();
1806 assert_eq!(
1807 actions,
1808 vec![
1809 MatchAction::FillLimit(ClientOrderId::from("O-B-LMT-HIGH")),
1810 MatchAction::FillLimit(ClientOrderId::from("O-B-LMT-LOW")),
1811 MatchAction::TriggerStop(ClientOrderId::from("O-B-STP-NEAR")),
1812 MatchAction::TriggerStop(ClientOrderId::from("O-B-STP-FAR")),
1813 MatchAction::FillLimit(ClientOrderId::from("O-A-LMT-LOW")),
1814 MatchAction::FillLimit(ClientOrderId::from("O-A-LMT-HIGH")),
1815 MatchAction::TriggerStop(ClientOrderId::from("O-A-STP-NEAR")),
1816 MatchAction::TriggerStop(ClientOrderId::from("O-A-STP-FAR")),
1817 ],
1818 );
1819 }
1820
1821 #[rstest]
1822 fn test_pending_orders_skipped_in_iterate_but_visible_in_get_orders() {
1823 let instrument_id = InstrumentId::from("AAPL.XNAS");
1824 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1825 core.set_bid_raw(Price::from("99.00"));
1826 core.set_ask_raw(Price::from("100.00"));
1827
1828 core.add_order(limit_order(OrderSide::Buy, "100.00", "O-LMT"));
1829
1830 let pending = RestingOrder::new(
1831 ClientOrderId::from("O-PENDING"),
1832 OrderSide::Buy,
1833 OrderType::MarketToLimit,
1834 None,
1835 None,
1836 true,
1837 );
1838 core.add_order(pending);
1839
1840 assert_eq!(
1841 core.iterate_bids(),
1842 vec![MatchAction::FillLimit(ClientOrderId::from("O-LMT"))],
1843 );
1844
1845 let bid_ids: Vec<_> = core
1846 .get_orders_bid()
1847 .iter()
1848 .map(|o| o.client_order_id)
1849 .collect();
1850 assert_eq!(
1851 bid_ids,
1852 vec![
1853 ClientOrderId::from("O-LMT"),
1854 ClientOrderId::from("O-PENDING"),
1855 ],
1856 );
1857 }
1858
1859 #[rstest]
1860 fn test_modify_then_readd_moves_order_to_back_of_new_level() {
1861 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1862 core.set_ask_raw(Price::from("99.00"));
1863
1864 core.add_order(limit_order(OrderSide::Buy, "100.00", "O-A"));
1865 core.add_order(limit_order(OrderSide::Buy, "100.00", "O-B"));
1866 core.add_order(limit_order(OrderSide::Buy, "100.00", "O-C"));
1867
1868 core.delete_order(ClientOrderId::from("O-A")).unwrap();
1869 core.add_order(limit_order(OrderSide::Buy, "100.50", "O-A"));
1870
1871 core.delete_order(ClientOrderId::from("O-B")).unwrap();
1873 core.add_order(limit_order(OrderSide::Buy, "100.00", "O-B"));
1874
1875 let actions = core.iterate_bids();
1876 assert_eq!(
1877 actions,
1878 vec![
1879 MatchAction::FillLimit(ClientOrderId::from("O-A")),
1880 MatchAction::FillLimit(ClientOrderId::from("O-C")),
1881 MatchAction::FillLimit(ClientOrderId::from("O-B")),
1882 ],
1883 );
1884 }
1885
1886 #[rstest]
1887 fn test_delete_unknown_order_returns_not_found() {
1888 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1889 let result = core.delete_order(ClientOrderId::from("O-MISSING"));
1890 assert!(matches!(result, Err(OrderError::NotFound(_))));
1891 }
1892
1893 #[rstest]
1894 #[case::buy(OrderSide::Buy)]
1895 #[case::sell(OrderSide::Sell)]
1896 fn test_pending_order_lookup_delete_and_conversion(#[case] side: OrderSide) {
1897 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1898 core.set_bid_raw(Price::from("100.00"));
1899 core.set_ask_raw(Price::from("100.00"));
1900 let first = pending_order(side, "O-FIRST");
1901 let second = pending_order(side, "O-SECOND");
1902 core.add_order(first);
1903 core.add_order(second);
1904
1905 assert_eq!(core.get_order(first.client_order_id), Some(&first));
1906 assert_eq!(core.get_order(second.client_order_id), Some(&second));
1907 assert!(core.order_exists(first.client_order_id));
1908 assert!(core.order_exists(second.client_order_id));
1909 assert_eq!(core.get_orders(), vec![first, second]);
1910 assert!(core.iterate().is_empty());
1911
1912 core.delete_order(second.client_order_id).unwrap();
1913
1914 assert_eq!(core.get_order(second.client_order_id), None);
1915 assert!(!core.order_exists(second.client_order_id));
1916 assert_eq!(core.get_order(first.client_order_id), Some(&first));
1917 assert!(core.order_exists(first.client_order_id));
1918 assert_eq!(core.get_orders(), vec![first]);
1919
1920 core.delete_order(first.client_order_id).unwrap();
1921
1922 assert_eq!(core.get_order(first.client_order_id), None);
1923 assert!(!core.order_exists(first.client_order_id));
1924 assert!(core.get_orders().is_empty());
1925
1926 let converted = RestingOrder {
1927 limit_price: Some(Price::from("100.00")),
1928 ..first
1929 };
1930 core.add_order(converted);
1931
1932 assert_eq!(core.get_order(first.client_order_id), Some(&converted));
1933 assert!(core.order_exists(first.client_order_id));
1934 assert_eq!(core.get_orders(), vec![converted]);
1935 assert_eq!(
1936 core.iterate(),
1937 vec![MatchAction::FillLimit(first.client_order_id)]
1938 );
1939 }
1940
1941 #[rstest]
1942 fn test_order_views_preserve_price_time_priority() {
1943 let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
1944 let bids = [
1945 limit_order(OrderSide::Buy, "101.00", "O-B-LMT-1"),
1946 limit_order(OrderSide::Buy, "101.00", "O-B-LMT-2"),
1947 limit_order(OrderSide::Buy, "100.00", "O-B-LMT-3"),
1948 stop_order(OrderSide::Buy, "102.00", "O-B-STP-1"),
1949 stop_order(OrderSide::Buy, "102.00", "O-B-STP-2"),
1950 stop_order(OrderSide::Buy, "103.00", "O-B-STP-3"),
1951 pending_order(OrderSide::Buy, "O-B-PENDING-1"),
1952 pending_order(OrderSide::Buy, "O-B-PENDING-2"),
1953 ];
1954 let asks = [
1955 limit_order(OrderSide::Sell, "100.00", "O-A-LMT-1"),
1956 limit_order(OrderSide::Sell, "100.00", "O-A-LMT-2"),
1957 limit_order(OrderSide::Sell, "101.00", "O-A-LMT-3"),
1958 stop_order(OrderSide::Sell, "99.00", "O-A-STP-1"),
1959 stop_order(OrderSide::Sell, "99.00", "O-A-STP-2"),
1960 stop_order(OrderSide::Sell, "98.00", "O-A-STP-3"),
1961 pending_order(OrderSide::Sell, "O-A-PENDING-1"),
1962 pending_order(OrderSide::Sell, "O-A-PENDING-2"),
1963 ];
1964
1965 for index in [6, 5, 2, 3, 0, 4, 1, 7] {
1966 core.add_order(asks[index]);
1967 core.add_order(bids[index]);
1968 }
1969 let orders = [bids, asks].concat();
1970
1971 assert_eq!(core.iter_bid_orders().copied().collect::<Vec<_>>(), bids);
1972 assert_eq!(core.get_orders_bid(), bids);
1973 assert_eq!(core.iter_ask_orders().copied().collect::<Vec<_>>(), asks);
1974 assert_eq!(core.get_orders_ask(), asks);
1975 assert_eq!(core.iter_orders().copied().collect::<Vec<_>>(), orders);
1976 assert_eq!(core.get_orders(), orders);
1977 }
1978
1979 #[rstest]
1980 #[case::buy(OrderSide::Buy, "99.00", "101.00")]
1981 #[case::sell(OrderSide::Sell, "101.00", "99.00")]
1982 fn test_touch_order_last_or_bid_ask_prefers_last_then_falls_back(
1983 #[case] side: OrderSide,
1984 #[case] crossed_price: &str,
1985 #[case] uncrossed_price: &str,
1986 #[values(OrderType::MarketIfTouched, OrderType::LimitIfTouched)] order_type: OrderType,
1987 ) {
1988 let instrument_id = InstrumentId::from("AAPL.XNAS");
1989 let mut core = create_matching_core(instrument_id, Price::from("0.01"));
1990 let trigger_price = Price::from("100.00");
1991 let crossed = Price::from(crossed_price);
1992 let uncrossed = Price::from(uncrossed_price);
1993 let order = OrderTestBuilder::new(order_type)
1994 .instrument_id(instrument_id)
1995 .client_order_id(ClientOrderId::from("O-TOUCH"))
1996 .side(side)
1997 .price(uncrossed)
1998 .trigger_price(trigger_price)
1999 .trigger_type(TriggerType::LastOrBidAsk)
2000 .quantity(Quantity::from("10"))
2001 .build();
2002 let resting = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
2003 let action = Some(MatchAction::TriggerStop(resting.client_order_id));
2004
2005 match side {
2006 OrderSide::Buy => {
2007 core.set_bid_raw(Price::from("98.00"));
2008 core.set_ask_raw(crossed);
2009 }
2010 OrderSide::Sell => {
2011 core.set_bid_raw(crossed);
2012 core.set_ask_raw(Price::from("102.00"));
2013 }
2014 }
2015 core.set_last_raw(uncrossed);
2016
2017 assert_eq!(core.match_order(&resting), None);
2018
2019 match side {
2020 OrderSide::Buy => core.set_ask_raw(uncrossed),
2021 OrderSide::Sell => core.set_bid_raw(uncrossed),
2022 }
2023 core.set_last_raw(trigger_price);
2024
2025 assert_eq!(core.match_order(&resting), action);
2026
2027 core.last = None;
2028
2029 assert_eq!(core.match_order(&resting), None);
2030
2031 match side {
2032 OrderSide::Buy => core.set_ask_raw(crossed),
2033 OrderSide::Sell => core.set_bid_raw(crossed),
2034 }
2035
2036 assert_eq!(core.match_order(&resting), action);
2037
2038 match side {
2039 OrderSide::Buy => core.ask = None,
2040 OrderSide::Sell => core.bid = None,
2041 }
2042
2043 assert_eq!(core.match_order(&resting), None);
2044 }
2045
2046 fn pending_order(side: OrderSide, id: &str) -> RestingOrder {
2047 RestingOrder::new(
2048 ClientOrderId::from(id),
2049 side,
2050 OrderType::MarketToLimit,
2051 None,
2052 None,
2053 true,
2054 )
2055 }
2056}