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