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