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