use std::collections::BTreeMap;
use ahash::AHashMap;
use nautilus_model::{
enums::{OrderSideSpecified, OrderType},
identifiers::{ClientOrderId, InstrumentId},
orders::{Order, OrderError, PassiveOrderAny, StopOrderAny},
types::Price,
};
use smallvec::SmallVec;
pub const INLINE_ORDERS_PER_LEVEL: usize = 4;
type OrderBucket = SmallVec<[RestingOrder; INLINE_ORDERS_PER_LEVEL]>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BookKind {
Limit,
Stop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchAction {
FillLimit(ClientOrderId),
TriggerStop(ClientOrderId),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RestingOrder {
pub client_order_id: ClientOrderId,
pub order_side: OrderSideSpecified,
pub order_type: OrderType,
pub trigger_price: Option<Price>,
pub limit_price: Option<Price>,
pub is_activated: bool,
}
impl RestingOrder {
#[must_use]
pub const fn new(
client_order_id: ClientOrderId,
order_side: OrderSideSpecified,
order_type: OrderType,
trigger_price: Option<Price>,
limit_price: Option<Price>,
is_activated: bool,
) -> Self {
Self {
client_order_id,
order_side,
order_type,
trigger_price,
limit_price,
is_activated,
}
}
#[must_use]
pub const fn is_stop(&self) -> bool {
self.trigger_price.is_some()
}
#[must_use]
pub const fn is_limit(&self) -> bool {
self.limit_price.is_some() && self.trigger_price.is_none()
}
}
impl From<&PassiveOrderAny> for RestingOrder {
fn from(order: &PassiveOrderAny) -> Self {
match order {
PassiveOrderAny::Limit(limit) => Self {
client_order_id: limit.client_order_id(),
order_side: limit.order_side_specified(),
order_type: limit.order_type(),
trigger_price: None,
limit_price: Some(limit.limit_px()),
is_activated: true,
},
PassiveOrderAny::Stop(stop) => {
let limit_price = match stop {
StopOrderAny::LimitIfTouched(o) => Some(o.price),
StopOrderAny::StopLimit(o) => Some(o.price),
StopOrderAny::TrailingStopLimit(o) => Some(o.price),
StopOrderAny::MarketIfTouched(_)
| StopOrderAny::StopMarket(_)
| StopOrderAny::TrailingStopMarket(_) => None,
};
let is_activated = match stop {
StopOrderAny::TrailingStopMarket(o) => o.is_activated,
StopOrderAny::TrailingStopLimit(o) => o.is_activated,
_ => true,
};
Self {
client_order_id: stop.client_order_id(),
order_side: stop.order_side_specified(),
order_type: stop.order_type(),
trigger_price: Some(stop.stop_px()),
limit_price,
is_activated,
}
}
}
}
}
#[derive(Clone, Debug)]
pub struct OrderMatchingCore {
pub instrument_id: InstrumentId,
pub price_increment: Price,
pub bid: Option<Price>,
pub ask: Option<Price>,
pub last: Option<Price>,
fill_limit_inside_spread: bool,
bid_limits: BTreeMap<Price, OrderBucket>,
ask_limits: BTreeMap<Price, OrderBucket>,
bid_stops: BTreeMap<Price, OrderBucket>,
ask_stops: BTreeMap<Price, OrderBucket>,
pending_bid: SmallVec<[RestingOrder; 2]>,
pending_ask: SmallVec<[RestingOrder; 2]>,
order_index: AHashMap<ClientOrderId, (OrderSideSpecified, Option<(BookKind, Price)>)>,
}
impl OrderMatchingCore {
#[must_use]
pub fn new(instrument_id: InstrumentId, price_increment: Price) -> Self {
Self {
instrument_id,
price_increment,
bid: None,
ask: None,
last: None,
fill_limit_inside_spread: false,
bid_limits: BTreeMap::new(),
ask_limits: BTreeMap::new(),
bid_stops: BTreeMap::new(),
ask_stops: BTreeMap::new(),
pending_bid: SmallVec::new(),
pending_ask: SmallVec::new(),
order_index: AHashMap::new(),
}
}
#[must_use]
pub const fn price_precision(&self) -> u8 {
self.price_increment.precision
}
#[must_use]
pub fn get_order(&self, client_order_id: ClientOrderId) -> Option<&RestingOrder> {
let (side, location) = self.order_index.get(&client_order_id).copied()?;
if let Some((kind, price)) = location {
self.book_for(side, kind)
.get(&price)?
.iter()
.find(|o| o.client_order_id == client_order_id)
} else {
self.pending_for(side)
.iter()
.find(|o| o.client_order_id == client_order_id)
}
}
pub fn iter_bid_orders(&self) -> impl Iterator<Item = &RestingOrder> {
self.bid_limits
.values()
.rev()
.flat_map(|b| b.iter())
.chain(self.bid_stops.values().flat_map(|b| b.iter()))
.chain(self.pending_bid.iter())
}
pub fn iter_ask_orders(&self) -> impl Iterator<Item = &RestingOrder> {
self.ask_limits
.values()
.flat_map(|b| b.iter())
.chain(self.ask_stops.values().rev().flat_map(|b| b.iter()))
.chain(self.pending_ask.iter())
}
pub fn iter_orders(&self) -> impl Iterator<Item = &RestingOrder> {
self.iter_bid_orders().chain(self.iter_ask_orders())
}
#[must_use]
pub fn get_orders_bid(&self) -> Vec<RestingOrder> {
self.iter_bid_orders().copied().collect()
}
#[must_use]
pub fn get_orders_ask(&self) -> Vec<RestingOrder> {
self.iter_ask_orders().copied().collect()
}
fn book_for(&self, side: OrderSideSpecified, kind: BookKind) -> &BTreeMap<Price, OrderBucket> {
match (side, kind) {
(OrderSideSpecified::Buy, BookKind::Limit) => &self.bid_limits,
(OrderSideSpecified::Buy, BookKind::Stop) => &self.bid_stops,
(OrderSideSpecified::Sell, BookKind::Limit) => &self.ask_limits,
(OrderSideSpecified::Sell, BookKind::Stop) => &self.ask_stops,
}
}
fn pending_for(&self, side: OrderSideSpecified) -> &[RestingOrder] {
match side {
OrderSideSpecified::Buy => &self.pending_bid,
OrderSideSpecified::Sell => &self.pending_ask,
}
}
#[must_use]
pub fn get_orders(&self) -> Vec<RestingOrder> {
self.iter_orders().copied().collect()
}
#[must_use]
pub fn order_exists(&self, client_order_id: ClientOrderId) -> bool {
self.order_index.contains_key(&client_order_id)
}
pub const fn set_last_raw(&mut self, last: Price) {
self.last = Some(last);
}
pub const fn set_bid_raw(&mut self, bid: Price) {
self.bid = Some(bid);
}
pub const fn set_ask_raw(&mut self, ask: Price) {
self.ask = Some(ask);
}
pub const fn update_price_increment(&mut self, price_increment: Price) {
self.price_increment = price_increment;
}
pub fn reset(&mut self) {
self.bid = None;
self.ask = None;
self.last = None;
self.bid_limits.clear();
self.ask_limits.clear();
self.bid_stops.clear();
self.ask_stops.clear();
self.pending_bid.clear();
self.pending_ask.clear();
self.order_index.clear();
}
fn locate(order: &RestingOrder) -> Option<(BookKind, Price)> {
if order.is_stop() {
Some((BookKind::Stop, order.trigger_price.unwrap()))
} else {
order.limit_price.map(|p| (BookKind::Limit, p))
}
}
pub fn add_order(&mut self, order: RestingOrder) {
debug_assert!(
!self.order_exists(order.client_order_id),
"duplicate add_order for {}; caller must delete before re-adding",
order.client_order_id,
);
let side = order.order_side;
let client_order_id = order.client_order_id;
let location = Self::locate(&order);
if let Some((kind, price)) = location {
let book = match (side, kind) {
(OrderSideSpecified::Buy, BookKind::Limit) => &mut self.bid_limits,
(OrderSideSpecified::Buy, BookKind::Stop) => &mut self.bid_stops,
(OrderSideSpecified::Sell, BookKind::Limit) => &mut self.ask_limits,
(OrderSideSpecified::Sell, BookKind::Stop) => &mut self.ask_stops,
};
book.entry(price).or_default().push(order);
} else {
match side {
OrderSideSpecified::Buy => self.pending_bid.push(order),
OrderSideSpecified::Sell => self.pending_ask.push(order),
}
}
self.order_index.insert(client_order_id, (side, location));
}
pub fn delete_order(&mut self, client_order_id: ClientOrderId) -> Result<(), OrderError> {
let Some((side, location)) = self.order_index.remove(&client_order_id) else {
return Err(OrderError::NotFound(client_order_id));
};
if let Some((kind, price)) = location {
let book = match (side, kind) {
(OrderSideSpecified::Buy, BookKind::Limit) => &mut self.bid_limits,
(OrderSideSpecified::Buy, BookKind::Stop) => &mut self.bid_stops,
(OrderSideSpecified::Sell, BookKind::Limit) => &mut self.ask_limits,
(OrderSideSpecified::Sell, BookKind::Stop) => &mut self.ask_stops,
};
let bucket = book
.get_mut(&price)
.expect("order_index points to existing bucket");
let pos = bucket
.iter()
.position(|o| o.client_order_id == client_order_id)
.expect("order_index points to existing slot");
bucket.remove(pos);
if bucket.is_empty() {
book.remove(&price);
}
} else {
let pending = match side {
OrderSideSpecified::Buy => &mut self.pending_bid,
OrderSideSpecified::Sell => &mut self.pending_ask,
};
let pos = pending
.iter()
.position(|o| o.client_order_id == client_order_id)
.expect("order_index points to existing pending slot");
pending.remove(pos);
}
Ok(())
}
pub fn iterate(&self) -> Vec<MatchAction> {
let mut actions = self.iterate_bids();
actions.extend(self.iterate_asks());
actions
}
pub fn iterate_bids(&self) -> Vec<MatchAction> {
self.bid_limits
.iter()
.rev()
.flat_map(|(_, b)| b.iter())
.chain(self.bid_stops.values().flat_map(|b| b.iter()))
.filter_map(|order| self.match_order(order))
.collect()
}
pub fn iterate_asks(&self) -> Vec<MatchAction> {
self.ask_limits
.values()
.flat_map(|b| b.iter())
.chain(self.ask_stops.iter().rev().flat_map(|(_, b)| b.iter()))
.filter_map(|order| self.match_order(order))
.collect()
}
pub fn match_order(&self, order: &RestingOrder) -> Option<MatchAction> {
if order.is_stop() {
self.match_stop_order(order)
} else if order.is_limit() {
self.match_limit_order(order)
} else {
None
}
}
fn match_limit_order(&self, order: &RestingOrder) -> Option<MatchAction> {
if let Some(limit_price) = order.limit_price
&& self.is_limit_fillable(order.order_side, limit_price)
{
Some(MatchAction::FillLimit(order.client_order_id))
} else {
None
}
}
fn match_stop_order(&self, order: &RestingOrder) -> Option<MatchAction> {
if !order.is_activated {
return None;
}
if let Some(trigger_price) = order.trigger_price
&& self.is_stop_matched(order.order_side, trigger_price)
{
Some(MatchAction::TriggerStop(order.client_order_id))
} else {
None
}
}
#[must_use]
pub fn is_limit_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
match side {
OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= price),
OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= price),
}
}
#[must_use]
pub fn is_stop_matched(&self, side: OrderSideSpecified, price: Price) -> bool {
match side {
OrderSideSpecified::Buy => self.ask.is_some_and(|a| a >= price),
OrderSideSpecified::Sell => self.bid.is_some_and(|b| b <= price),
}
}
#[must_use]
pub fn is_touch_triggered(&self, side: OrderSideSpecified, trigger_price: Price) -> bool {
match side {
OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= trigger_price),
OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= trigger_price),
}
}
pub fn set_fill_limit_inside_spread(&mut self, value: bool) {
self.fill_limit_inside_spread = value;
}
#[must_use]
pub fn is_limit_fillable(&self, side: OrderSideSpecified, price: Price) -> bool {
if self.is_limit_matched(side, price) {
return true;
}
if !self.fill_limit_inside_spread {
return false;
}
if let (Some(bid), Some(ask)) = (self.bid, self.ask) {
match side {
OrderSideSpecified::Buy => price >= bid,
OrderSideSpecified::Sell => price <= ask,
}
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use nautilus_model::{
enums::{OrderSide, OrderType, TrailingOffsetType, TriggerType},
events::{OrderEventAny, OrderInitialized, order::spec::OrderInitializedSpec},
orders::{Order, OrderAny, builder::OrderTestBuilder},
types::Quantity,
};
use rstest::rstest;
use rust_decimal::Decimal;
use super::*;
fn create_matching_core(
instrument_id: InstrumentId,
price_increment: Price,
) -> OrderMatchingCore {
OrderMatchingCore::new(instrument_id, price_increment)
}
#[rstest]
fn test_add_order_bid_side() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(OrderSide::Buy)
.price(Price::from("100.00"))
.quantity(Quantity::from("100"))
.build();
let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
matching_core.add_order(match_info);
assert!(matching_core.get_orders_bid().contains(&match_info));
assert!(!matching_core.get_orders_ask().contains(&match_info));
assert_eq!(matching_core.get_orders_bid().len(), 1);
assert!(matching_core.get_orders_ask().is_empty());
assert!(matching_core.order_exists(match_info.client_order_id));
}
#[rstest]
fn test_add_order_ask_side() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(OrderSide::Sell)
.price(Price::from("100.00"))
.quantity(Quantity::from("100"))
.build();
let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
matching_core.add_order(match_info);
assert!(matching_core.get_orders_ask().contains(&match_info));
assert!(!matching_core.get_orders_bid().contains(&match_info));
assert_eq!(matching_core.get_orders_ask().len(), 1);
assert!(matching_core.get_orders_bid().is_empty());
assert!(matching_core.order_exists(match_info.client_order_id));
}
#[rstest]
fn test_reset() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(OrderSide::Sell)
.price(Price::from("100.00"))
.quantity(Quantity::from("100"))
.build();
let client_order_id = order.client_order_id();
let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
matching_core.add_order(match_info);
matching_core.set_bid_raw(Price::from("100.00"));
matching_core.set_ask_raw(Price::from("100.00"));
matching_core.set_last_raw(Price::from("100.00"));
matching_core.reset();
assert!(matching_core.bid.is_none());
assert!(matching_core.ask.is_none());
assert!(matching_core.last.is_none());
assert!(matching_core.get_orders_bid().is_empty());
assert!(matching_core.get_orders_ask().is_empty());
assert!(!matching_core.order_exists(client_order_id));
}
#[rstest]
fn test_delete_order_when_not_exists() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(OrderSide::Buy)
.price(Price::from("100.00"))
.quantity(Quantity::from("100"))
.build();
let result = matching_core.delete_order(order.client_order_id());
assert!(result.is_err());
}
#[rstest]
#[case(OrderSide::Buy)]
#[case(OrderSide::Sell)]
fn test_delete_order_when_exists(#[case] order_side: OrderSide) {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(order_side)
.price(Price::from("100.00"))
.quantity(Quantity::from("100"))
.build();
let client_order_id = order.client_order_id();
let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
matching_core.add_order(match_info);
matching_core.delete_order(client_order_id).unwrap();
assert!(matching_core.get_orders_ask().is_empty());
assert!(matching_core.get_orders_bid().is_empty());
}
#[rstest]
#[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
#[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("100.00"), // <-- Price below ask
OrderSide::Buy,
false
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("101.00"), // <-- Price at ask
OrderSide::Buy,
true
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("102.00"), // <-- Price above ask (marketable)
OrderSide::Buy,
true
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("101.00"), // <-- Price above bid
OrderSide::Sell,
false
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("100.00"), // <-- Price at bid
OrderSide::Sell,
true
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("99.00"), // <-- Price below bid (marketable)
OrderSide::Sell,
true
)]
fn test_is_limit_matched(
#[case] bid: Option<Price>,
#[case] ask: Option<Price>,
#[case] price: Price,
#[case] order_side: OrderSide,
#[case] expected: bool,
) {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.bid = bid;
matching_core.ask = ask;
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(order_side)
.price(price)
.quantity(Quantity::from("100"))
.build();
let result =
matching_core.is_limit_matched(order.order_side_specified(), order.price().unwrap());
assert_eq!(result, expected);
}
#[rstest]
#[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
#[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("102.00"), // <-- Trigger above ask
OrderSide::Buy,
false
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("101.00"), // <-- Trigger at ask
OrderSide::Buy,
true
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("100.00"), // <-- Trigger below ask
OrderSide::Buy,
true
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("99.00"), // Trigger below bid
OrderSide::Sell,
false
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("100.00"), // <-- Trigger at bid
OrderSide::Sell,
true
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("101.00"), // <-- Trigger above bid
OrderSide::Sell,
true
)]
fn test_is_stop_matched(
#[case] bid: Option<Price>,
#[case] ask: Option<Price>,
#[case] trigger_price: Price,
#[case] order_side: OrderSide,
#[case] expected: bool,
) {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.bid = bid;
matching_core.ask = ask;
let order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_id)
.side(order_side)
.trigger_price(trigger_price)
.quantity(Quantity::from("100"))
.build();
let result = matching_core
.is_stop_matched(order.order_side_specified(), order.trigger_price().unwrap());
assert_eq!(result, expected);
}
#[rstest]
fn test_iterate_returns_empty_when_no_orders() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.set_bid_raw(Price::from("100.00"));
matching_core.set_ask_raw(Price::from("101.00"));
let actions = matching_core.iterate();
assert!(actions.is_empty());
}
#[rstest]
fn test_iterate_returns_empty_when_no_market_data() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(OrderSide::Buy)
.price(Price::from("100.00"))
.quantity(Quantity::from("100"))
.build();
let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
matching_core.add_order(match_info);
let actions = matching_core.iterate();
assert!(actions.is_empty());
}
#[rstest]
fn test_iterate_returns_fill_limit_for_matched_buy() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.set_ask_raw(Price::from("100.00"));
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(OrderSide::Buy)
.price(Price::from("100.00"))
.quantity(Quantity::from("100"))
.build();
let client_order_id = order.client_order_id();
let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
matching_core.add_order(match_info);
let actions = matching_core.iterate();
assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
}
#[rstest]
fn test_iterate_returns_fill_limit_for_matched_sell() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.set_bid_raw(Price::from("100.00"));
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(OrderSide::Sell)
.price(Price::from("100.00"))
.quantity(Quantity::from("100"))
.build();
let client_order_id = order.client_order_id();
let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
matching_core.add_order(match_info);
let actions = matching_core.iterate();
assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
}
#[rstest]
fn test_iterate_returns_no_fill_for_unmatched_limit() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.set_ask_raw(Price::from("101.00"));
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(OrderSide::Buy)
.price(Price::from("100.00"))
.quantity(Quantity::from("100"))
.build();
let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
matching_core.add_order(match_info);
let actions = matching_core.iterate();
assert!(actions.is_empty());
}
#[rstest]
fn test_iterate_returns_trigger_stop_for_matched_buy() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.set_ask_raw(Price::from("101.00"));
let order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_id)
.side(OrderSide::Buy)
.trigger_price(Price::from("101.00"))
.trigger_type(TriggerType::Default)
.quantity(Quantity::from("100"))
.build();
let client_order_id = order.client_order_id();
let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
matching_core.add_order(match_info);
let actions = matching_core.iterate();
assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
}
#[rstest]
fn test_iterate_returns_trigger_stop_for_matched_sell() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.set_bid_raw(Price::from("99.00"));
let order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_id)
.side(OrderSide::Sell)
.trigger_price(Price::from("99.00"))
.quantity(Quantity::from("100"))
.build();
let client_order_id = order.client_order_id();
let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
matching_core.add_order(match_info);
let actions = matching_core.iterate();
assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
}
#[rstest]
fn test_iterate_skips_unactivated_stop_order() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.set_ask_raw(Price::from("110.00"));
let match_info = RestingOrder::new(
ClientOrderId::from("O-001"),
OrderSideSpecified::Buy,
OrderType::TrailingStopMarket,
Some(Price::from("105.00")),
None,
false, );
matching_core.add_order(match_info);
let actions = matching_core.iterate();
assert!(actions.is_empty());
}
#[rstest]
fn test_iterate_triggers_activated_stop_order() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.set_ask_raw(Price::from("110.00"));
let client_order_id = ClientOrderId::from("O-001");
let match_info = RestingOrder::new(
client_order_id,
OrderSideSpecified::Buy,
OrderType::TrailingStopMarket,
Some(Price::from("105.00")),
None,
true, );
matching_core.add_order(match_info);
let actions = matching_core.iterate();
assert_eq!(actions, vec![MatchAction::TriggerStop(client_order_id)]);
}
#[rstest]
fn test_iterate_returns_mixed_actions_for_limits_and_stops() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.set_bid_raw(Price::from("99.00"));
matching_core.set_ask_raw(Price::from("101.00"));
let buy_limit = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(OrderSide::Buy)
.price(Price::from("101.00"))
.quantity(Quantity::from("100"))
.client_order_id(ClientOrderId::from("O-BUY-LIMIT"))
.build();
let buy_limit_id = buy_limit.client_order_id();
matching_core.add_order(RestingOrder::from(
&PassiveOrderAny::try_from(buy_limit).unwrap(),
));
let sell_stop = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_id)
.side(OrderSide::Sell)
.trigger_price(Price::from("99.00"))
.quantity(Quantity::from("50"))
.client_order_id(ClientOrderId::from("O-SELL-STOP"))
.build();
let sell_stop_id = sell_stop.client_order_id();
matching_core.add_order(RestingOrder::from(
&PassiveOrderAny::try_from(sell_stop).unwrap(),
));
let actions = matching_core.iterate();
assert_eq!(actions.len(), 2);
assert_eq!(actions[0], MatchAction::FillLimit(buy_limit_id));
assert_eq!(actions[1], MatchAction::TriggerStop(sell_stop_id));
}
#[rstest]
fn test_is_limit_fillable_delegates_to_is_limit_matched_by_default() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut core = create_matching_core(instrument_id, Price::from("0.01"));
core.set_bid_raw(Price::from("100.00"));
core.set_ask_raw(Price::from("101.00"));
assert!(core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("101.00")));
assert!(!core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
assert!(core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("100.00")));
assert!(!core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("101.00")));
}
#[rstest]
fn test_is_limit_fillable_inside_spread_buy_at_bid() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut core = create_matching_core(instrument_id, Price::from("0.01"));
core.set_bid_raw(Price::from("100.00"));
core.set_ask_raw(Price::from("101.00"));
core.set_fill_limit_inside_spread(true);
assert!(core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
assert!(core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.50")));
assert!(!core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("99.00")));
}
#[rstest]
fn test_is_limit_fillable_inside_spread_sell_at_ask() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut core = create_matching_core(instrument_id, Price::from("0.01"));
core.set_bid_raw(Price::from("100.00"));
core.set_ask_raw(Price::from("101.00"));
core.set_fill_limit_inside_spread(true);
assert!(core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("101.00")));
assert!(core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("100.50")));
assert!(!core.is_limit_fillable(OrderSideSpecified::Sell, Price::from("102.00")));
}
#[rstest]
fn test_is_limit_fillable_inside_spread_requires_both_quotes_present() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut core = create_matching_core(instrument_id, Price::from("0.01"));
core.set_fill_limit_inside_spread(true);
core.set_bid_raw(Price::from("100.00"));
assert!(!core.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
let mut core2 = create_matching_core(instrument_id, Price::from("0.01"));
core2.set_fill_limit_inside_spread(true);
core2.set_ask_raw(Price::from("101.00"));
assert!(!core2.is_limit_fillable(OrderSideSpecified::Sell, Price::from("101.00")));
let mut core3 = create_matching_core(instrument_id, Price::from("0.01"));
core3.set_fill_limit_inside_spread(true);
core3.set_bid_raw(Price::from("100.00"));
core3.set_ask_raw(Price::from("101.00"));
core3.ask = None;
assert!(!core3.is_limit_fillable(OrderSideSpecified::Buy, Price::from("100.00")));
}
#[rstest]
fn test_iterate_fills_limit_inside_spread_when_enabled() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut core = create_matching_core(instrument_id, Price::from("0.01"));
core.set_bid_raw(Price::from("100.00"));
core.set_ask_raw(Price::from("101.00"));
core.set_fill_limit_inside_spread(true);
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_id)
.side(OrderSide::Buy)
.price(Price::from("100.00"))
.quantity(Quantity::from("100"))
.build();
let client_order_id = order.client_order_id();
let match_info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
core.add_order(match_info);
let actions = core.iterate();
assert_eq!(actions, vec![MatchAction::FillLimit(client_order_id)]);
}
#[rstest]
#[case(None, None, Price::from("100.00"), OrderSide::Buy, false)]
#[case(None, None, Price::from("100.00"), OrderSide::Sell, false)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("102.00"), // <-- Ask below trigger
OrderSide::Buy,
true
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("101.00"), // <-- Ask at trigger
OrderSide::Buy,
true
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("100.00"), // <-- Ask above trigger
OrderSide::Buy,
false
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("99.00"), // <-- Bid above trigger
OrderSide::Sell,
true
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("100.00"), // <-- Bid at trigger
OrderSide::Sell,
true
)]
#[case(
Some(Price::from("100.00")),
Some(Price::from("101.00")),
Price::from("101.00"), // <-- Bid below trigger
OrderSide::Sell,
false
)]
fn test_is_touch_triggered(
#[case] bid: Option<Price>,
#[case] ask: Option<Price>,
#[case] trigger_price: Price,
#[case] order_side: OrderSide,
#[case] expected: bool,
) {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
matching_core.bid = bid;
matching_core.ask = ask;
let result = matching_core.is_touch_triggered(order_side.as_specified(), trigger_price);
assert_eq!(result, expected);
}
#[rstest]
fn test_update_price_increment_updates_increment_and_precision() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut matching_core = create_matching_core(instrument_id, Price::from("0.01"));
assert_eq!(matching_core.price_increment, Price::from("0.01"));
assert_eq!(matching_core.price_precision(), 2);
matching_core.update_price_increment(Price::from("0.001"));
assert_eq!(matching_core.price_increment, Price::from("0.001"));
assert_eq!(matching_core.price_precision(), 3);
}
fn order_from_init(spec: OrderInitialized) -> OrderAny {
OrderAny::from_events(vec![OrderEventAny::Initialized(spec)]).unwrap()
}
#[rstest]
fn test_get_order_finds_orders_on_either_side() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut core = create_matching_core(instrument_id, Price::from("0.01"));
let buy = order_from_init(
OrderInitializedSpec::builder()
.instrument_id(instrument_id)
.client_order_id(ClientOrderId::from("O-BUY"))
.order_side(OrderSide::Buy)
.order_type(OrderType::Limit)
.quantity(Quantity::from("10"))
.price(Price::from("100.00"))
.build(),
);
let buy_id = buy.client_order_id();
core.add_order(RestingOrder::from(&PassiveOrderAny::try_from(buy).unwrap()));
let sell = order_from_init(
OrderInitializedSpec::builder()
.instrument_id(instrument_id)
.client_order_id(ClientOrderId::from("O-SELL"))
.order_side(OrderSide::Sell)
.order_type(OrderType::Limit)
.quantity(Quantity::from("10"))
.price(Price::from("101.00"))
.build(),
);
let sell_id = sell.client_order_id();
core.add_order(RestingOrder::from(
&PassiveOrderAny::try_from(sell).unwrap(),
));
assert_eq!(
core.get_order(buy_id).map(|o| o.client_order_id),
Some(buy_id)
);
assert_eq!(
core.get_order(sell_id).map(|o| o.client_order_id),
Some(sell_id)
);
assert!(core.get_order(ClientOrderId::from("O-MISSING")).is_none());
}
#[rstest]
fn test_match_order_returns_none_when_neither_price_set() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut core = create_matching_core(instrument_id, Price::from("0.01"));
core.set_bid_raw(Price::from("100.00"));
core.set_ask_raw(Price::from("101.00"));
let info = RestingOrder::new(
ClientOrderId::from("O-NEITHER"),
OrderSideSpecified::Buy,
OrderType::MarketToLimit,
None,
None,
true,
);
assert!(core.match_order(&info).is_none());
}
#[rstest]
fn test_from_passive_order_extracts_limit_price_for_stop_limit() {
let order = order_from_init(
OrderInitializedSpec::builder()
.order_type(OrderType::StopLimit)
.order_side(OrderSide::Buy)
.quantity(Quantity::from("10"))
.price(Price::from("101.00"))
.trigger_price(Price::from("100.00"))
.trigger_type(TriggerType::Default)
.build(),
);
let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
assert_eq!(info.trigger_price, Some(Price::from("100.00")));
assert_eq!(info.limit_price, Some(Price::from("101.00")));
assert!(info.is_activated);
}
#[rstest]
fn test_from_passive_order_extracts_limit_price_for_limit_if_touched() {
let order = order_from_init(
OrderInitializedSpec::builder()
.order_type(OrderType::LimitIfTouched)
.order_side(OrderSide::Sell)
.quantity(Quantity::from("10"))
.price(Price::from("99.00"))
.trigger_price(Price::from("100.00"))
.trigger_type(TriggerType::Default)
.build(),
);
let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
assert_eq!(info.trigger_price, Some(Price::from("100.00")));
assert_eq!(info.limit_price, Some(Price::from("99.00")));
assert!(info.is_activated);
}
#[rstest]
fn test_from_passive_order_extracts_is_activated_for_trailing_stop_market() {
let order = order_from_init(
OrderInitializedSpec::builder()
.order_type(OrderType::TrailingStopMarket)
.order_side(OrderSide::Buy)
.quantity(Quantity::from("10"))
.trigger_price(Price::from("101.00"))
.trigger_type(TriggerType::Default)
.trailing_offset(Decimal::from(1))
.trailing_offset_type(TrailingOffsetType::Price)
.build(),
);
let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
assert_eq!(info.trigger_price, Some(Price::from("101.00")));
assert_eq!(info.limit_price, None);
assert!(!info.is_activated);
}
#[rstest]
fn test_from_passive_order_extracts_limit_and_is_activated_for_trailing_stop_limit() {
let order = order_from_init(
OrderInitializedSpec::builder()
.order_type(OrderType::TrailingStopLimit)
.order_side(OrderSide::Sell)
.quantity(Quantity::from("10"))
.price(Price::from("99.00"))
.trigger_price(Price::from("100.00"))
.trigger_type(TriggerType::Default)
.limit_offset(Decimal::from(1))
.trailing_offset(Decimal::from(1))
.trailing_offset_type(TrailingOffsetType::Price)
.build(),
);
let info = RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap());
assert_eq!(info.trigger_price, Some(Price::from("100.00")));
assert_eq!(info.limit_price, Some(Price::from("99.00")));
assert!(!info.is_activated);
}
fn limit_order(side: OrderSide, price: &str, id: &str) -> RestingOrder {
let order = order_from_init(
OrderInitializedSpec::builder()
.client_order_id(ClientOrderId::from(id))
.order_type(OrderType::Limit)
.order_side(side)
.quantity(Quantity::from("10"))
.price(Price::from(price))
.build(),
);
RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap())
}
fn stop_order(side: OrderSide, trigger: &str, id: &str) -> RestingOrder {
let order = order_from_init(
OrderInitializedSpec::builder()
.client_order_id(ClientOrderId::from(id))
.order_type(OrderType::StopMarket)
.order_side(side)
.quantity(Quantity::from("10"))
.trigger_price(Price::from(trigger))
.trigger_type(TriggerType::Default)
.build(),
);
RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap())
}
fn stop_limit_order(side: OrderSide, trigger: &str, limit: &str, id: &str) -> RestingOrder {
let order = order_from_init(
OrderInitializedSpec::builder()
.client_order_id(ClientOrderId::from(id))
.order_type(OrderType::StopLimit)
.order_side(side)
.quantity(Quantity::from("10"))
.price(Price::from(limit))
.trigger_price(Price::from(trigger))
.trigger_type(TriggerType::Default)
.build(),
);
RestingOrder::from(&PassiveOrderAny::try_from(order).unwrap())
}
#[rstest]
fn test_iterate_bids_returns_limits_in_descending_price_order() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
core.set_ask_raw(Price::from("99.00"));
core.add_order(limit_order(OrderSide::Buy, "100.00", "O-MID"));
core.add_order(limit_order(OrderSide::Buy, "100.50", "O-HIGH"));
core.add_order(limit_order(OrderSide::Buy, "99.50", "O-LOW"));
let actions = core.iterate_bids();
assert_eq!(
actions,
vec![
MatchAction::FillLimit(ClientOrderId::from("O-HIGH")),
MatchAction::FillLimit(ClientOrderId::from("O-MID")),
MatchAction::FillLimit(ClientOrderId::from("O-LOW")),
],
);
}
#[rstest]
fn test_iterate_asks_returns_limits_in_ascending_price_order() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
core.set_bid_raw(Price::from("101.00"));
core.add_order(limit_order(OrderSide::Sell, "100.50", "O-MID"));
core.add_order(limit_order(OrderSide::Sell, "100.00", "O-LOW"));
core.add_order(limit_order(OrderSide::Sell, "100.75", "O-HIGH"));
let actions = core.iterate_asks();
assert_eq!(
actions,
vec![
MatchAction::FillLimit(ClientOrderId::from("O-LOW")),
MatchAction::FillLimit(ClientOrderId::from("O-MID")),
MatchAction::FillLimit(ClientOrderId::from("O-HIGH")),
],
);
}
#[rstest]
fn test_iterate_limits_preserves_fifo_within_same_price() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
core.set_ask_raw(Price::from("99.00"));
for id in ["O-1", "O-2", "O-3", "O-4"] {
core.add_order(limit_order(OrderSide::Buy, "100.00", id));
}
let actions = core.iterate_bids();
assert_eq!(
actions,
vec![
MatchAction::FillLimit(ClientOrderId::from("O-1")),
MatchAction::FillLimit(ClientOrderId::from("O-2")),
MatchAction::FillLimit(ClientOrderId::from("O-3")),
MatchAction::FillLimit(ClientOrderId::from("O-4")),
],
);
}
#[rstest]
fn test_buy_stops_trigger_in_ascending_price_order_when_ask_crosses_multiple() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
core.set_ask_raw(Price::from("106.00"));
core.add_order(stop_order(OrderSide::Buy, "105.00", "O-FAR"));
core.add_order(stop_order(OrderSide::Buy, "101.00", "O-NEAR"));
let actions = core.iterate_bids();
assert_eq!(
actions,
vec![
MatchAction::TriggerStop(ClientOrderId::from("O-NEAR")),
MatchAction::TriggerStop(ClientOrderId::from("O-FAR")),
],
);
}
#[rstest]
fn test_sell_stops_trigger_in_descending_price_order_when_bid_crosses_multiple() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
core.set_bid_raw(Price::from("94.00"));
core.add_order(stop_order(OrderSide::Sell, "95.00", "O-FAR"));
core.add_order(stop_order(OrderSide::Sell, "99.00", "O-NEAR"));
let actions = core.iterate_asks();
assert_eq!(
actions,
vec![
MatchAction::TriggerStop(ClientOrderId::from("O-NEAR")),
MatchAction::TriggerStop(ClientOrderId::from("O-FAR")),
],
);
}
#[rstest]
fn test_iterate_stops_preserves_fifo_within_same_trigger() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
core.set_ask_raw(Price::from("106.00"));
for id in ["O-S1", "O-S2", "O-S3"] {
core.add_order(stop_order(OrderSide::Buy, "101.00", id));
}
let actions = core.iterate_bids();
assert_eq!(
actions,
vec![
MatchAction::TriggerStop(ClientOrderId::from("O-S1")),
MatchAction::TriggerStop(ClientOrderId::from("O-S2")),
MatchAction::TriggerStop(ClientOrderId::from("O-S3")),
],
);
}
#[rstest]
fn test_iterate_bids_processes_limits_before_stops() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
core.set_ask_raw(Price::from("106.00"));
core.add_order(limit_order(OrderSide::Buy, "110.00", "O-LMT"));
core.add_order(stop_order(OrderSide::Buy, "101.00", "O-STP"));
let actions = core.iterate_bids();
assert_eq!(
actions,
vec![
MatchAction::FillLimit(ClientOrderId::from("O-LMT")),
MatchAction::TriggerStop(ClientOrderId::from("O-STP")),
],
);
}
#[rstest]
fn test_iterate_asks_processes_limits_before_stops() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
core.set_bid_raw(Price::from("94.00"));
core.add_order(limit_order(OrderSide::Sell, "90.00", "O-LMT"));
core.add_order(stop_order(OrderSide::Sell, "99.00", "O-STP"));
let actions = core.iterate_asks();
assert_eq!(
actions,
vec![
MatchAction::FillLimit(ClientOrderId::from("O-LMT")),
MatchAction::TriggerStop(ClientOrderId::from("O-STP")),
],
);
}
#[rstest]
fn test_stop_limit_routed_to_stop_book_keyed_by_trigger() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
core.set_ask_raw(Price::from("106.00"));
core.add_order(stop_limit_order(
OrderSide::Buy,
"105.00",
"110.00",
"O-FAR",
));
core.add_order(stop_limit_order(
OrderSide::Buy,
"101.00",
"110.00",
"O-NEAR",
));
let actions = core.iterate_bids();
assert_eq!(
actions,
vec![
MatchAction::TriggerStop(ClientOrderId::from("O-NEAR")),
MatchAction::TriggerStop(ClientOrderId::from("O-FAR")),
],
);
}
#[rstest]
fn test_iterate_full_walk_combines_bids_then_asks_each_with_limits_then_stops() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
core.set_bid_raw(Price::from("94.00"));
core.set_ask_raw(Price::from("106.00"));
core.add_order(limit_order(OrderSide::Buy, "110.00", "O-B-LMT-HIGH"));
core.add_order(limit_order(OrderSide::Buy, "107.00", "O-B-LMT-LOW"));
core.add_order(stop_order(OrderSide::Buy, "105.00", "O-B-STP-FAR"));
core.add_order(stop_order(OrderSide::Buy, "101.00", "O-B-STP-NEAR"));
core.add_order(limit_order(OrderSide::Sell, "90.00", "O-A-LMT-LOW"));
core.add_order(limit_order(OrderSide::Sell, "93.00", "O-A-LMT-HIGH"));
core.add_order(stop_order(OrderSide::Sell, "95.00", "O-A-STP-FAR"));
core.add_order(stop_order(OrderSide::Sell, "99.00", "O-A-STP-NEAR"));
let actions = core.iterate();
assert_eq!(
actions,
vec![
MatchAction::FillLimit(ClientOrderId::from("O-B-LMT-HIGH")),
MatchAction::FillLimit(ClientOrderId::from("O-B-LMT-LOW")),
MatchAction::TriggerStop(ClientOrderId::from("O-B-STP-NEAR")),
MatchAction::TriggerStop(ClientOrderId::from("O-B-STP-FAR")),
MatchAction::FillLimit(ClientOrderId::from("O-A-LMT-LOW")),
MatchAction::FillLimit(ClientOrderId::from("O-A-LMT-HIGH")),
MatchAction::TriggerStop(ClientOrderId::from("O-A-STP-NEAR")),
MatchAction::TriggerStop(ClientOrderId::from("O-A-STP-FAR")),
],
);
}
#[rstest]
fn test_pending_orders_skipped_in_iterate_but_visible_in_get_orders() {
let instrument_id = InstrumentId::from("AAPL.XNAS");
let mut core = create_matching_core(instrument_id, Price::from("0.01"));
core.set_bid_raw(Price::from("99.00"));
core.set_ask_raw(Price::from("100.00"));
core.add_order(limit_order(OrderSide::Buy, "100.00", "O-LMT"));
let pending = RestingOrder::new(
ClientOrderId::from("O-PENDING"),
OrderSideSpecified::Buy,
OrderType::MarketToLimit,
None,
None,
true,
);
core.add_order(pending);
assert_eq!(
core.iterate_bids(),
vec![MatchAction::FillLimit(ClientOrderId::from("O-LMT"))],
);
let bid_ids: Vec<_> = core
.get_orders_bid()
.iter()
.map(|o| o.client_order_id)
.collect();
assert_eq!(
bid_ids,
vec![
ClientOrderId::from("O-LMT"),
ClientOrderId::from("O-PENDING"),
],
);
}
#[rstest]
fn test_modify_then_readd_moves_order_to_back_of_new_level() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
core.set_ask_raw(Price::from("99.00"));
core.add_order(limit_order(OrderSide::Buy, "100.00", "O-A"));
core.add_order(limit_order(OrderSide::Buy, "100.00", "O-B"));
core.add_order(limit_order(OrderSide::Buy, "100.00", "O-C"));
core.delete_order(ClientOrderId::from("O-A")).unwrap();
core.add_order(limit_order(OrderSide::Buy, "100.50", "O-A"));
core.delete_order(ClientOrderId::from("O-B")).unwrap();
core.add_order(limit_order(OrderSide::Buy, "100.00", "O-B"));
let actions = core.iterate_bids();
assert_eq!(
actions,
vec![
MatchAction::FillLimit(ClientOrderId::from("O-A")), MatchAction::FillLimit(ClientOrderId::from("O-C")), MatchAction::FillLimit(ClientOrderId::from("O-B")), ],
);
}
#[rstest]
fn test_delete_unknown_order_returns_not_found() {
let mut core = create_matching_core(InstrumentId::from("AAPL.XNAS"), Price::from("0.01"));
let result = core.delete_order(ClientOrderId::from("O-MISSING"));
assert!(matches!(result, Err(OrderError::NotFound(_))));
}
}