use std::{cell::RefCell, rc::Rc};
use chrono::{DateTime, TimeZone, Utc};
use nautilus_common::{
cache::Cache,
clock::TestClock,
messages::execution::{BatchCancelOrders, CancelAllOrders, CancelOrder, ModifyOrder},
msgbus::{
self, MessagingSwitchboard, TypedIntoHandler,
stubs::{
TypedIntoMessageSavingHandler, TypedMessageSavingHandler,
get_typed_into_message_saving_handler, get_typed_message_saving_handler,
},
},
};
use nautilus_core::{UUID4, UnixNanos};
use nautilus_execution::{
matching_engine::{config::OrderMatchingEngineConfig, engine::OrderMatchingEngine},
models::{
fee::{CappedOptionFeeModel, FeeModelAny, FixedFeeModel},
fill::{BestPriceFillModel, DefaultFillModel, FillModelAny, FillModelHandle},
},
};
use nautilus_model::{
data::{
Bar, BarType, BookOrder, InstrumentClose, OptionGreeks, QuoteTick, TradeTick,
stubs::OrderBookDeltaTestBuilder,
},
enums::{
AccountType, AggressorSide, AssetClass, BookAction, BookType, ContingencyType,
InstrumentCloseType, LiquiditySide, MarketStatus, MarketStatusAction, OmsType, OptionKind,
OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType,
},
events::{
OrderEmulated, OrderEventAny, OrderEventType, OrderFilled, OrderRejected, OrderReleased,
order::spec::{OrderEmulatedSpec, OrderFilledSpec, OrderRejectedSpec, OrderReleasedSpec},
},
identifiers::{
AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId, Symbol, TradeId,
TraderId, VenueOrderId, stubs::account_id,
},
instruments::{
CryptoOption, CryptoPerpetual, Equity, IndexInstrument, Instrument, InstrumentAny,
OptionContract,
stubs::{binary_option, crypto_perpetual_ethusdt, equity_aapl, futures_contract_es},
},
orders::{
Order, OrderAny, OrderTestBuilder,
stubs::{TestOrderEventStubs, TestOrderStubs},
},
position::Position,
stubs::TestDefault,
types::{Currency, Money, Price, Quantity},
};
use rstest::{fixture, rstest};
use rust_decimal_macros::dec;
use ustr::Ustr;
#[fixture]
pub fn test_clock() -> Rc<RefCell<TestClock>> {
Rc::new(RefCell::new(TestClock::new()))
}
#[expect(clippy::too_many_arguments)]
fn build_order_filled(
trader_id: TraderId,
strategy_id: StrategyId,
instrument_id: InstrumentId,
client_order_id: ClientOrderId,
venue_order_id: VenueOrderId,
account_id: AccountId,
trade_id: TradeId,
order_side: OrderSide,
order_type: OrderType,
last_qty: Quantity,
last_px: Price,
currency: Currency,
liquidity_side: LiquiditySide,
position_id: Option<PositionId>,
commission: Option<Money>,
) -> OrderFilled {
OrderFilledSpec::builder()
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument_id)
.client_order_id(client_order_id)
.venue_order_id(venue_order_id)
.account_id(account_id)
.trade_id(trade_id)
.order_side(order_side)
.order_type(order_type)
.last_qty(last_qty)
.last_px(last_px)
.currency(currency)
.liquidity_side(liquidity_side)
.maybe_position_id(position_id)
.maybe_commission(commission)
.build()
}
fn build_order_emulated(
trader_id: TraderId,
strategy_id: StrategyId,
instrument_id: InstrumentId,
client_order_id: ClientOrderId,
) -> OrderEmulated {
OrderEmulatedSpec::builder()
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument_id)
.client_order_id(client_order_id)
.build()
}
fn build_order_released(
trader_id: TraderId,
strategy_id: StrategyId,
instrument_id: InstrumentId,
client_order_id: ClientOrderId,
released_price: Price,
) -> OrderReleased {
OrderReleasedSpec::builder()
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument_id)
.client_order_id(client_order_id)
.released_price(released_price)
.build()
}
#[fixture]
fn order_event_handler() -> TypedIntoMessageSavingHandler<OrderEventAny> {
let (handler, saving_handler) = get_typed_into_message_saving_handler::<OrderEventAny>(Some(
Ustr::from("ExecEngine.process"),
));
msgbus::register_order_event_endpoint(MessagingSwitchboard::exec_engine_process(), handler);
saving_handler
}
#[fixture]
pub fn instrument_eth_usdt(crypto_perpetual_ethusdt: CryptoPerpetual) -> InstrumentAny {
InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt)
}
#[fixture]
pub fn market_order_buy(instrument_eth_usdt: InstrumentAny) -> OrderAny {
OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build()
}
#[fixture]
pub fn market_order_fill(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
market_order_buy: OrderAny,
) -> OrderFilled {
build_order_filled(
market_order_buy.trader_id(),
market_order_buy.strategy_id(),
market_order_buy.instrument_id(),
market_order_buy.client_order_id(),
VenueOrderId::new("BINANCE-1"),
account_id,
TradeId::new("1"),
market_order_buy.order_side(),
market_order_buy.order_type(),
Quantity::from("1"),
Price::from("1000.000"),
instrument_eth_usdt.quote_currency(),
LiquiditySide::Taker,
Some(PositionId::new("P-1")),
None,
)
}
#[fixture]
pub fn market_order_sell(instrument_eth_usdt: InstrumentAny) -> OrderAny {
OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build()
}
#[fixture]
fn instrument_es() -> InstrumentAny {
let activation = UnixNanos::from(
Utc.with_ymd_and_hms(2022, 4, 8, 0, 0, 0)
.unwrap()
.timestamp_nanos_opt()
.unwrap() as u64,
);
let expiration = UnixNanos::from(
Utc.with_ymd_and_hms(2100, 7, 8, 0, 0, 0)
.unwrap()
.timestamp_nanos_opt()
.unwrap() as u64,
);
InstrumentAny::FuturesContract(futures_contract_es(Some(activation), Some(expiration)))
}
#[fixture]
fn engine_config() -> OrderMatchingEngineConfig {
OrderMatchingEngineConfig {
support_contingent_orders: true,
use_reduce_only: true,
..Default::default()
}
}
fn get_order_matching_engine(
instrument: InstrumentAny,
clock: Option<Rc<RefCell<TestClock>>>,
cache: Option<Rc<RefCell<Cache>>>,
account_type: Option<AccountType>,
config: Option<OrderMatchingEngineConfig>,
) -> OrderMatchingEngine {
let clock = clock.unwrap_or(Rc::new(RefCell::new(TestClock::new())));
let cache = cache.unwrap_or(Rc::new(RefCell::new(Cache::default())));
let config = config.unwrap_or_default();
OrderMatchingEngine::new(
instrument,
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
account_type.unwrap_or(AccountType::Cash),
clock,
cache,
config,
)
}
fn get_order_matching_engine_l2(
instrument: InstrumentAny,
clock: Option<Rc<RefCell<TestClock>>>,
cache: Option<Rc<RefCell<Cache>>>,
account_type: Option<AccountType>,
config: Option<OrderMatchingEngineConfig>,
) -> OrderMatchingEngine {
let clock = clock.unwrap_or(Rc::new(RefCell::new(TestClock::new())));
let cache = cache.unwrap_or(Rc::new(RefCell::new(Cache::default())));
let config = config.unwrap_or_default();
OrderMatchingEngine::new(
instrument,
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L2_MBP,
OmsType::Netting,
account_type.unwrap_or(AccountType::Cash),
clock,
cache,
config,
)
}
fn crypto_perpetual_with_price_precision(
instrument: InstrumentAny,
price_precision: u8,
price_increment: &str,
) -> InstrumentAny {
match instrument {
InstrumentAny::CryptoPerpetual(mut crypto_perp) => {
crypto_perp.price_precision = price_precision;
crypto_perp.price_increment = Price::from(price_increment);
InstrumentAny::CryptoPerpetual(crypto_perp)
}
_ => panic!("Test fixture expected CryptoPerpetual instrument"),
}
}
fn crypto_perpetual_with_size_precision(
instrument: InstrumentAny,
size_precision: u8,
size_increment: &str,
) -> InstrumentAny {
match instrument {
InstrumentAny::CryptoPerpetual(mut crypto_perp) => {
crypto_perp.size_precision = size_precision;
crypto_perp.size_increment = Quantity::from(size_increment);
InstrumentAny::CryptoPerpetual(crypto_perp)
}
_ => panic!("Test fixture expected CryptoPerpetual instrument"),
}
}
fn order_event_handler_with_cache(
cache: Rc<RefCell<Cache>>,
) -> TypedIntoMessageSavingHandler<OrderEventAny> {
use nautilus_common::msgbus::typed_handler::TypedIntoHandler;
let messages: Rc<RefCell<Vec<OrderEventAny>>> = Rc::new(RefCell::new(Vec::new()));
let messages_for_handler = messages.clone();
msgbus::register_order_event_endpoint(
MessagingSwitchboard::exec_engine_process(),
TypedIntoHandler::from(move |event: OrderEventAny| {
if let Ok(mut cache_ref) = cache.try_borrow_mut() {
let _ = cache_ref.update_order(&event);
}
messages_for_handler.borrow_mut().push(event);
}),
);
TypedIntoMessageSavingHandler::new_with_messages(
Some(Ustr::from("ExecEngine.process")),
messages,
)
}
fn get_order_event_handler_messages(
event_handler: &TypedIntoMessageSavingHandler<OrderEventAny>,
) -> Vec<OrderEventAny> {
event_handler.get_messages()
}
fn clear_order_event_handler_messages(
event_handler: &TypedIntoMessageSavingHandler<OrderEventAny>,
) {
event_handler.clear();
}
#[rstest]
fn test_process_order_when_instrument_already_expired(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
mut market_order_buy: OrderAny,
test_clock: Rc<RefCell<TestClock>>,
) {
let instrument = InstrumentAny::FuturesContract(futures_contract_es(None, None));
test_clock
.borrow_mut()
.set_time(UnixNanos::from(1704067200000000000));
let mut engine = get_order_matching_engine(instrument, Some(test_clock), None, None, None);
engine.process_order(&mut market_order_buy, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first_message = saved_messages.first().unwrap();
assert_eq!(first_message.event_type(), OrderEventType::Rejected);
assert_eq!(
first_message.message().unwrap(),
Ustr::from("Contract ESZ21.GLBX has expired, expiration 1639699200000000000")
);
}
#[rstest]
fn test_process_order_when_instrument_not_active(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
mut market_order_buy: OrderAny,
) {
let activation = UnixNanos::from(
Utc.with_ymd_and_hms(2222, 4, 8, 0, 0, 0)
.unwrap()
.timestamp_nanos_opt()
.unwrap() as u64,
);
let expiration = UnixNanos::from(
Utc.with_ymd_and_hms(2223, 7, 8, 0, 0, 0)
.unwrap()
.timestamp_nanos_opt()
.unwrap() as u64,
);
let instrument =
InstrumentAny::FuturesContract(futures_contract_es(Some(activation), Some(expiration)));
let mut engine = get_order_matching_engine(instrument, None, None, None, None);
engine.process_order(&mut market_order_buy, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first_message = saved_messages.first().unwrap();
assert_eq!(first_message.event_type(), OrderEventType::Rejected);
assert_eq!(
first_message.message().unwrap(),
Ustr::from("Contract ESZ21.GLBX is not yet active, activation 7960723200000000000")
);
}
#[rstest]
fn test_process_order_when_invalid_quantity_precision(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let mut market_order_invalid_precision = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1"))
.submit(true)
.build();
let mut engine = get_order_matching_engine(instrument_eth_usdt, None, None, None, None);
engine.process_order(&mut market_order_invalid_precision, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first_message = saved_messages.first().unwrap();
assert_eq!(first_message.event_type(), OrderEventType::Rejected);
assert_eq!(
first_message.message().unwrap(),
Ustr::from(
"Invalid order quantity precision for order O-19700101-000000-001-001-1, was 0 when ETHUSDT-PERP.BINANCE size precision is 3"
)
);
}
#[rstest]
fn test_process_order_when_invalid_display_qty_precision(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1490.00"))
.quantity(Quantity::from("1.000"))
.display_qty(Quantity::from("0.0001"))
.submit(true)
.build();
let mut engine = get_order_matching_engine(instrument_eth_usdt, None, None, None, None);
engine.process_order(&mut limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first_message = saved_messages.first().unwrap();
assert_eq!(first_message.event_type(), OrderEventType::Rejected);
assert_eq!(
first_message.message().unwrap(),
Ustr::from(
"Invalid order display quantity precision for order O-19700101-000000-001-001-1, was 4 when ETHUSDT-PERP.BINANCE size precision is 3"
)
);
}
#[rstest]
fn test_process_order_when_invalid_price_precision(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_es: InstrumentAny,
test_clock: Rc<RefCell<TestClock>>,
) {
test_clock
.borrow_mut()
.set_time(UnixNanos::from(1704067200000000000));
let mut engine =
get_order_matching_engine(instrument_es.clone(), Some(test_clock), None, None, None);
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_es.id())
.side(OrderSide::Sell)
.price(Price::from("100.12333")) .quantity(Quantity::from("1"))
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first_message = saved_messages.first().unwrap();
assert_eq!(first_message.event_type(), OrderEventType::Rejected);
assert_eq!(
first_message.message().unwrap(),
Ustr::from(
"Invalid order price precision for order O-19700101-000000-001-001-1, was 5 when ESZ21.GLBX price precision is 2"
)
);
}
#[rstest]
fn test_process_order_when_invalid_trigger_price_precision(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_es: InstrumentAny,
test_clock: Rc<RefCell<TestClock>>,
) {
test_clock
.borrow_mut()
.set_time(UnixNanos::from(1704067200000000000));
let mut engine =
get_order_matching_engine(instrument_es.clone(), Some(test_clock), None, None, None);
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_es.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("100.12333")) .quantity(Quantity::from("1"))
.submit(true)
.build();
engine.process_order(&mut stop_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first_message = saved_messages.first().unwrap();
assert_eq!(first_message.event_type(), OrderEventType::Rejected);
assert_eq!(
first_message.message().unwrap(),
Ustr::from(
"Invalid order trigger price precision for order O-19700101-000000-001-001-1, was 5 when ESZ21.GLBX price precision is 2"
)
);
}
#[rstest]
fn test_process_order_when_shorting_equity_without_margin_account(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
equity_aapl: Equity,
) {
let instrument = InstrumentAny::Equity(equity_aapl);
let mut market_order_sell = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Sell)
.quantity(Quantity::from("1"))
.submit(true)
.build();
let mut engine = get_order_matching_engine(instrument, None, None, None, None);
engine.process_order(&mut market_order_sell, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first_message = saved_messages.first().unwrap();
assert_eq!(first_message.event_type(), OrderEventType::Rejected);
assert_eq!(
first_message.message().unwrap(),
Ustr::from(
"Short selling not permitted on a CASH account with position None and \
order MarketOrder(SELL 1 AAPL.XNAS @ MARKET GTC, status=SUBMITTED, \
client_order_id=O-19700101-000000-001-001-1, venue_order_id=None, position_id=None, \
exec_algorithm_id=None, exec_spawn_id=None, tags=None)"
)
);
}
#[rstest]
fn test_process_order_when_invalid_reduce_only(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
engine_config: OrderMatchingEngineConfig,
) {
let mut engine = get_order_matching_engine(
instrument_eth_usdt.clone(),
None,
None,
None,
Some(engine_config),
);
let mut market_order_reduce = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.reduce_only(true)
.submit(true)
.build();
engine.process_order(&mut market_order_reduce, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first_message = saved_messages.first().unwrap();
assert_eq!(first_message.event_type(), OrderEventType::Rejected);
assert_eq!(
first_message.message().unwrap(),
Ustr::from(
"Reduce-only order O-19700101-000000-001-001-1 (MARKET-BUY) would have increased position"
)
);
}
#[rstest]
#[case::paused(MarketStatusAction::Pause, MarketStatus::Paused)]
#[case::suspended(MarketStatusAction::Suspend, MarketStatus::Suspended)]
#[case::closed(MarketStatusAction::Close, MarketStatus::Closed)]
fn test_process_order_rejects_when_market_not_open_and_accepts_after_reopen(
#[case] close_action: MarketStatusAction,
#[case] expected_status: MarketStatus,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
engine.process_status(close_action);
assert_eq!(engine.market_status, expected_status);
let rejected_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut rejected_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(rejected_client_order_id)
.submit(true)
.build();
engine.process_order(&mut rejected_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let rejected = match saved_messages.first().unwrap() {
OrderEventAny::Rejected(rejected) => rejected,
event => panic!("Expected OrderRejected event, was {event:?}"),
};
assert_eq!(rejected.client_order_id, rejected_client_order_id);
assert!(rejected.reason.as_str().contains(expected_status.as_ref()));
assert!(!engine.order_exists(rejected_client_order_id));
clear_order_event_handler_messages(&order_event_handler);
engine.process_status(MarketStatusAction::Trading);
assert_eq!(engine.market_status, MarketStatus::Open);
let accepted_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut accepted_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(accepted_client_order_id)
.submit(true)
.build();
engine.process_order(&mut accepted_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let accepted = match saved_messages.first().unwrap() {
OrderEventAny::Accepted(accepted) => accepted,
event => panic!("Expected OrderAccepted event, was {event:?}"),
};
assert_eq!(accepted.client_order_id, accepted_client_order_id);
assert!(engine.order_exists(accepted_client_order_id));
}
#[rstest]
fn test_market_status_pause_blocks_matching_until_trading_resumes(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let resting_bid_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut resting_bid = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1499.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(resting_bid_id)
.submit(true)
.build();
engine_l2.process_order(&mut resting_bid, account_id);
clear_order_event_handler_messages(&order_event_handler);
engine_l2.process_status(MarketStatusAction::Pause);
assert_eq!(engine_l2.market_status, MarketStatus::Paused);
let matching_ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1498.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&matching_ask).unwrap();
let paused_messages = get_order_event_handler_messages(&order_event_handler);
assert!(
paused_messages
.iter()
.all(|event| !matches!(event, OrderEventAny::Filled(_))),
"paused market must not fill resting orders, was {paused_messages:?}",
);
assert!(engine_l2.order_exists(resting_bid_id));
engine_l2.process_status(MarketStatusAction::Trading);
engine_l2.iterate(UnixNanos::from(2_u64), AggressorSide::NoAggressor);
let resumed_messages = get_order_event_handler_messages(&order_event_handler);
let fill = resumed_messages
.iter()
.find_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.expect("resting order should fill after market reopens");
assert_eq!(fill.client_order_id, resting_bid_id);
}
#[rstest]
fn test_process_order_when_invalid_contingent_orders(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_es: InstrumentAny,
engine_config: OrderMatchingEngineConfig,
test_clock: Rc<RefCell<TestClock>>,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
test_clock
.borrow_mut()
.set_time(UnixNanos::from(1704067200000000000));
let mut engine = get_order_matching_engine(
instrument_es.clone(),
Some(test_clock),
Some(cache.clone()),
None,
Some(engine_config),
);
let entry_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let stop_loss_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut entry_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_es.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(1))
.contingency_type(ContingencyType::Oto)
.client_order_id(entry_client_order_id)
.submit(true)
.build();
let rejected_event = OrderRejected::default();
entry_order
.apply(OrderEventAny::Rejected(rejected_event))
.unwrap();
let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_es.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("0.95"))
.quantity(Quantity::from(1))
.contingency_type(ContingencyType::Oto)
.client_order_id(stop_loss_client_order_id)
.parent_order_id(entry_client_order_id)
.submit(true)
.build();
let mut accepted_stop_order = TestOrderStubs::make_accepted_order(&stop_order);
cache
.as_ref()
.borrow_mut()
.add_order(entry_order.clone(), None, None, false)
.unwrap();
engine.process_order(&mut accepted_stop_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first_message = saved_messages.first().unwrap();
assert_eq!(first_message.event_type(), OrderEventType::Rejected);
assert_eq!(
first_message.message().unwrap(),
Ustr::from(format!("Rejected OTO order from {entry_client_order_id}").as_str())
);
}
#[rstest]
fn test_process_order_when_closed_linked_order(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_es: InstrumentAny,
engine_config: OrderMatchingEngineConfig,
test_clock: Rc<RefCell<TestClock>>,
) {
test_clock
.borrow_mut()
.set_time(UnixNanos::from(1704067200000000000));
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = get_order_matching_engine(
instrument_es.clone(),
Some(test_clock),
Some(cache.clone()),
None,
Some(engine_config),
);
let stop_loss_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-2");
let take_profit_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-3");
let mut stop_loss_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_es.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("0.95"))
.quantity(Quantity::from(1))
.contingency_type(ContingencyType::Oco)
.client_order_id(stop_loss_client_order_id)
.linked_order_ids(vec![take_profit_client_order_id])
.submit(true)
.build();
let take_profit_order = OrderTestBuilder::new(OrderType::MarketIfTouched)
.instrument_id(instrument_es.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("1.1"))
.quantity(Quantity::from(1))
.contingency_type(ContingencyType::Oco)
.client_order_id(take_profit_client_order_id)
.linked_order_ids(vec![stop_loss_client_order_id])
.submit(true)
.build();
let rejected_event: OrderRejected = OrderRejectedSpec::builder()
.client_order_id(stop_loss_client_order_id)
.reason(Ustr::from("Rejected"))
.build();
stop_loss_order
.apply(OrderEventAny::Rejected(rejected_event))
.unwrap();
let mut accepted_take_profit = TestOrderStubs::make_accepted_order(&take_profit_order);
cache
.as_ref()
.borrow_mut()
.add_order(stop_loss_order.clone(), None, None, false)
.unwrap();
let _stop_loss_closed_after = stop_loss_order.is_closed();
engine.process_order(&mut accepted_take_profit, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first_message = saved_messages.first().unwrap();
assert_eq!(first_message.event_type(), OrderEventType::Rejected);
assert_eq!(
first_message.message().unwrap(),
Ustr::from(format!("Contingent order {stop_loss_client_order_id} already closed").as_str())
);
}
#[rstest]
fn test_process_bracket_order_list_does_not_double_submit_children(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
engine_config: OrderMatchingEngineConfig,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
Some(engine_config),
);
let ask = BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("10.000"),
1,
);
engine
.process_order_book_delta(
&OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(ask)
.build(),
)
.unwrap();
let entry_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let sl_id = ClientOrderId::from("O-19700101-000000-001-001-2");
let tp_id = ClientOrderId::from("O-19700101-000000-001-001-3");
let mut entry = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(entry_id)
.contingency_type(ContingencyType::Oto)
.linked_order_ids(vec![sl_id, tp_id])
.submit(true)
.build();
let mut sl = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("100.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(sl_id)
.parent_order_id(entry_id)
.contingency_type(ContingencyType::Oco)
.linked_order_ids(vec![tp_id])
.submit(true)
.build();
let mut tp = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.price(Price::from("3000.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(tp_id)
.parent_order_id(entry_id)
.contingency_type(ContingencyType::Oco)
.linked_order_ids(vec![sl_id])
.submit(true)
.build();
{
let mut cache_mut = cache.borrow_mut();
cache_mut
.add_order(entry.clone(), None, None, false)
.unwrap();
cache_mut.add_order(sl.clone(), None, None, false).unwrap();
cache_mut.add_order(tp.clone(), None, None, false).unwrap();
}
engine.process_order(&mut entry, account_id);
engine.process_order(&mut sl, account_id);
engine.process_order(&mut tp, account_id);
let events = get_order_event_handler_messages(&order_event_handler);
let already_exists = events
.iter()
.filter(|e| {
e.event_type() == OrderEventType::Rejected
&& e.message()
.is_some_and(|m| m.as_str().contains("already exists"))
})
.count();
assert_eq!(
already_exists, 0,
"No bracket child should be rejected with 'Order already exists'; events: {events:?}",
);
let filled = events
.iter()
.filter(|e| e.event_type() == OrderEventType::Filled)
.count();
let accepted = events
.iter()
.filter(|e| e.event_type() == OrderEventType::Accepted)
.count();
assert_eq!(filled, 1, "entry should fill exactly once");
assert_eq!(
accepted, 2,
"SL and TP should each be accepted exactly once"
);
}
#[rstest]
fn test_process_market_order_no_market_rejected(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
mut market_order_buy: OrderAny,
mut market_order_sell: OrderAny,
) {
let mut engine = get_order_matching_engine(instrument_eth_usdt, None, None, None, None);
engine.process_order(&mut market_order_buy, account_id);
engine.process_order(&mut market_order_sell, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let first = saved_messages.first().unwrap();
let second = saved_messages.get(1).unwrap();
assert_eq!(first.event_type(), OrderEventType::Rejected);
assert_eq!(second.event_type(), OrderEventType::Rejected);
assert_eq!(
first.message().unwrap(),
Ustr::from("No market for ETHUSDT-PERP.BINANCE")
);
assert_eq!(
second.message().unwrap(),
Ustr::from("No market for ETHUSDT-PERP.BINANCE")
);
}
#[rstest]
#[case::at_the_open(TimeInForce::AtTheOpen, "AT_THE_OPEN")]
#[case::at_the_close(TimeInForce::AtTheClose, "AT_THE_CLOSE")]
fn test_process_market_order_at_the_open_or_close_rejected(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] time_in_force: TimeInForce,
#[case] tif_str: &str,
) {
let mut engine = get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, None);
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.time_in_force(time_in_force)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first = saved_messages.first().unwrap();
assert_eq!(first.event_type(), OrderEventType::Rejected);
assert_eq!(
first.message().unwrap(),
Ustr::from(format!("time in force {tif_str} is not currently supported").as_str())
);
}
#[rstest]
#[case::at_the_open(TimeInForce::AtTheOpen, "AT_THE_OPEN")]
#[case::at_the_close(TimeInForce::AtTheClose, "AT_THE_CLOSE")]
fn test_process_limit_order_at_the_open_or_close_rejected(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] time_in_force: TimeInForce,
#[case] tif_str: &str,
) {
let mut engine = get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, None);
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1500.00"))
.quantity(Quantity::from("1.000"))
.time_in_force(time_in_force)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first = saved_messages.first().unwrap();
assert_eq!(first.event_type(), OrderEventType::Rejected);
assert_eq!(
first.message().unwrap(),
Ustr::from(format!("time in force {tif_str} is not currently supported").as_str())
);
}
#[rstest]
fn test_bid_ask_initialized(instrument_es: InstrumentAny) {
let mut engine_l2 = get_order_matching_engine_l2(instrument_es.clone(), None, None, None, None);
let book_order_buy = BookOrder::new(
OrderSide::Buy,
Price::from("100.00"),
Quantity::from("1"),
0,
);
let book_order_sell = BookOrder::new(
OrderSide::Sell,
Price::from("101.00"),
Quantity::from("1"),
0,
);
let orderbook_delta_buy = OrderBookDeltaTestBuilder::new(instrument_es.id())
.book_action(BookAction::Add)
.book_order(book_order_buy)
.build();
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_es.id())
.book_action(BookAction::Add)
.book_order(book_order_sell)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_buy)
.unwrap();
assert_eq!(engine_l2.get_core().bid, Some(Price::from("100.00")));
assert_eq!(engine_l2.get_core().ask, None);
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
assert_eq!(engine_l2.get_core().bid, Some(Price::from("100.00")));
assert_eq!(engine_l2.get_core().ask, Some(Price::from("101.00")));
}
#[rstest]
fn test_not_enough_quantity_filled_fok_order(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("2.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.time_in_force(TimeInForce::Fok)
.submit(true)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let first_message = saved_messages.first().unwrap();
assert_eq!(first_message.event_type(), OrderEventType::Canceled);
let canceled = match first_message {
OrderEventAny::Canceled(canceled) => canceled,
_ => panic!("Expected OrderCanceled event in first message"),
};
assert_eq!(canceled.client_order_id, market_order.client_order_id());
}
#[rstest]
fn test_valid_market_buy(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let book_order_1 = BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
);
let book_order_2 = BookOrder::new(
OrderSide::Sell,
Price::from("1510.00"),
Quantity::from("1.000"),
1,
);
let orderbook_delta_sell_1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(book_order_1)
.build();
let orderbook_delta_sell_2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(book_order_2)
.build();
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("2.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell_1)
.unwrap();
engine_l2
.process_order_book_delta(&orderbook_delta_sell_2)
.unwrap();
engine_l2.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let event1 = saved_messages.first().unwrap();
let fill1 = match event1 {
OrderEventAny::Filled(fill) => fill,
_ => panic!("Expected OrderFilled event in first message"),
};
let event2 = saved_messages.get(1).unwrap();
let fill2 = match event2 {
OrderEventAny::Filled(fill) => fill,
_ => panic!("Expected OrderFilled event in second message"),
};
assert_eq!(fill1.last_px, Price::from("1500.00"));
assert_eq!(fill1.last_qty, Quantity::from("1.000"));
assert_eq!(fill2.last_px, Price::from("1510.00"));
assert_eq!(fill2.last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_market_fill_caps_pending_cached_quantity_before_commission(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.500"),
1,
))
.build();
engine_l2.process_order_book_delta(&ask).unwrap();
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("2.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-CAP"))
.submit(true)
.build();
engine_l2.process_order(&mut market_order, account_id);
engine_l2.fill_market_order(market_order.client_order_id());
let fills: Vec<OrderFilled> = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.collect();
assert_eq!(fills.len(), 2);
assert_eq!(fills[0].last_qty, Quantity::from("1.500"));
assert_eq!(fills[1].last_qty, Quantity::from("0.500"));
let commission = fills[1].commission.expect("expected commission");
let expected_commission = fills[1].last_qty.as_decimal()
* fills[1].last_px.as_decimal()
* instrument_eth_usdt.taker_fee();
assert_eq!(commission.currency, instrument_eth_usdt.quote_currency());
assert_eq!(commission.as_decimal(), expected_commission);
}
#[rstest]
fn test_market_order_with_acks_generates_accepted_then_filled(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
use_market_order_acks: true,
..Default::default()
};
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine.process_order_book_delta(&orderbook_delta).unwrap();
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let accepted = match saved_messages.first().unwrap() {
OrderEventAny::Accepted(a) => a,
other => panic!("Expected OrderAccepted, was {other:?}"),
};
assert_eq!(accepted.client_order_id, market_order.client_order_id());
let filled = match saved_messages.get(1).unwrap() {
OrderEventAny::Filled(f) => f,
other => panic!("Expected OrderFilled, was {other:?}"),
};
assert_eq!(filled.client_order_id, market_order.client_order_id());
assert_eq!(filled.last_px, Price::from("1500.00"));
assert_eq!(filled.last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_market_order_with_protection_and_acks_generates_accepted_then_filled(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig::builder()
.use_market_order_acks(true)
.price_protection_points(600u32)
.build();
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine.process_order_book_delta(&orderbook_delta).unwrap();
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let accepted = match saved_messages.first().unwrap() {
OrderEventAny::Accepted(a) => a,
other => panic!("Expected OrderAccepted, was {other:?}"),
};
assert_eq!(accepted.client_order_id, market_order.client_order_id());
let filled = match saved_messages.get(1).unwrap() {
OrderEventAny::Filled(f) => f,
other => panic!("Expected OrderFilled, was {other:?}"),
};
assert_eq!(filled.client_order_id, market_order.client_order_id());
}
#[rstest]
fn test_process_limit_post_only_order_that_would_be_a_taker(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let mut post_only_limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1501.00"))
.quantity(Quantity::from("1.000"))
.post_only(true)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut post_only_limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let first_message = saved_messages.first().unwrap();
let rejected = match first_message {
OrderEventAny::Rejected(rejected) => rejected,
_ => panic!("Expected OrderRejected event in first message"),
};
assert_eq!(saved_messages.len(), 1);
assert_eq!(first_message.event_type(), OrderEventType::Rejected);
assert_eq!(
rejected.reason,
Ustr::from(
"POST_ONLY LIMIT BUY order limit px of 1501.00 would have been a TAKER: bid=None, ask=1500.00"
)
);
assert!(
rejected.due_post_only,
"due_post_only should be set for post-only rejections"
);
}
#[rstest]
fn test_process_limit_order_not_matched_and_canceled_fok_order(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.time_in_force(TimeInForce::Fok)
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
let event2 = saved_messages.get(1).unwrap();
let rejected = match event2 {
OrderEventAny::Canceled(canceled) => canceled,
_ => panic!("Expected OrderCanceled event in second message"),
};
assert_eq!(saved_messages.len(), 2);
assert_eq!(accepted.client_order_id, client_order_id);
assert_eq!(rejected.client_order_id, client_order_id);
}
#[rstest]
#[case(TimeInForce::Ioc)]
#[case(TimeInForce::Fok)]
fn test_process_limit_order_without_immediate_match_cancels(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
#[case] time_in_force: TimeInForce,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.time_in_force(time_in_force)
.client_order_id(client_order_id)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let accepted = match saved_messages.first().unwrap() {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
let canceled = match saved_messages.get(1).unwrap() {
OrderEventAny::Canceled(canceled) => canceled,
_ => panic!("Expected OrderCanceled event in second message"),
};
assert_eq!(saved_messages.len(), 2);
assert_eq!(accepted.client_order_id, client_order_id);
assert_eq!(canceled.client_order_id, client_order_id);
assert_eq!(canceled.venue_order_id, Some(accepted.venue_order_id));
}
#[rstest]
fn test_accept_order_released_dispatches_and_registers(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.emulation_trigger(TriggerType::Default)
.client_order_id(client_order_id)
.build();
let trader_id = limit_order.trader_id();
let strategy_id = limit_order.strategy_id();
let instrument_id = limit_order.instrument_id();
limit_order
.apply(OrderEventAny::Emulated(build_order_emulated(
trader_id,
strategy_id,
instrument_id,
client_order_id,
)))
.unwrap();
limit_order
.apply(OrderEventAny::Released(build_order_released(
trader_id,
strategy_id,
instrument_id,
client_order_id,
Price::from("1495.00"),
)))
.unwrap();
assert_eq!(limit_order.status(), OrderStatus::Released);
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let accepted_count = saved_messages
.iter()
.filter(|e| matches!(e, OrderEventAny::Accepted(_)))
.count();
assert_eq!(
accepted_count, 1,
"Released order should still dispatch OrderAccepted",
);
assert!(
engine_l2.order_exists(client_order_id),
"Released order should register with the matching core",
);
}
#[rstest]
fn test_process_limit_order_matched_immediate_fill(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1501.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
let event2 = saved_messages.get(1).unwrap();
let fill = match event2 {
OrderEventAny::Filled(fill) => fill,
_ => panic!("Expected OrderFilled event in second message"),
};
assert_eq!(saved_messages.len(), 2);
assert_eq!(accepted.client_order_id, client_order_id);
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_px, Price::from("1500.00"));
assert_eq!(fill.last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_process_stop_market_order_triggered_rejected(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let engine_config = OrderMatchingEngineConfig {
reject_stop_orders: true,
..Default::default()
};
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
None,
None,
Some(engine_config),
);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut stop_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let event = saved_messages.first().unwrap();
let rejected = match event {
OrderEventAny::Rejected(rejected) => rejected,
_ => panic!("Expected OrderRejected event in first message"),
};
assert_eq!(saved_messages.len(), 1);
assert_eq!(rejected.client_order_id, client_order_id);
assert_eq!(
rejected.reason,
Ustr::from(
"STOP_MARKET BUY order stop px of 1495.00 was in the market: bid=None, ask=1500.00, but rejected because of configuration"
)
);
}
#[rstest]
fn test_process_stop_market_order_valid_trigger_filled(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
reject_stop_orders: false,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut stop_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill = saved_messages.first().unwrap();
let fill = match fill {
OrderEventAny::Filled(fill) => fill,
_ => panic!("Expected OrderFilled event in first message"),
};
assert_eq!(saved_messages.len(), 1);
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_px, Price::from("1500.00"));
assert_eq!(fill.last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_process_stop_market_order_valid_not_triggered_accepted(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1505.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut stop_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let event = saved_messages.first().unwrap();
let accepted = match event {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(saved_messages.len(), 1);
assert_eq!(accepted.client_order_id, client_order_id);
}
#[rstest]
fn test_process_stop_limit_order_triggered_not_filled(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
reject_stop_orders: false,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_order = OrderTestBuilder::new(OrderType::StopLimit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1495.00"))
.price(Price::from("1490.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut stop_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
let event2 = saved_messages.get(1).unwrap();
let triggered = match event2 {
OrderEventAny::Triggered(triggered) => triggered,
_ => panic!("Expected OrderTriggered event in second message"),
};
assert_eq!(saved_messages.len(), 2);
assert_eq!(accepted.client_order_id, client_order_id);
assert_eq!(triggered.client_order_id, client_order_id);
let resting = engine_l2
.get_core()
.get_order(client_order_id)
.copied()
.expect("triggered stop-limit should rest in the core");
assert_eq!(resting.trigger_price, None);
assert_eq!(resting.limit_price, Some(Price::from("1490.00")));
clear_order_event_handler_messages(&order_event_handler);
let matching_ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1489.00"),
Quantity::from("1.000"),
2,
))
.build();
engine_l2.process_order_book_delta(&matching_ask).unwrap();
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill = saved_messages
.iter()
.find_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.expect("triggered stop-limit should fill as a limit after the ask crosses");
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_process_stop_limit_order_triggered_filled(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
reject_stop_orders: false,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_order = OrderTestBuilder::new(OrderType::StopLimit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1495.00"))
.price(Price::from("1502.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut stop_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 3);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let order_triggered = match event2 {
OrderEventAny::Triggered(triggered) => triggered,
_ => panic!("Expected OrderTriggered event in second message"),
};
assert_eq!(order_triggered.client_order_id, client_order_id);
let event3 = saved_messages.get(2).unwrap();
let fill = match event3 {
OrderEventAny::Filled(fill) => fill,
_ => panic!("Expected OrderFilled event in third message"),
};
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_px, Price::from("1500.00"));
assert_eq!(fill.last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_passive_stop_limit_trigger_emits_triggered_before_fill(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
reject_stop_orders: false,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("2.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&ask).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_limit = OrderTestBuilder::new(OrderType::StopLimit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1505.00"))
.price(Price::from("1506.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut stop_limit, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1505.00"),
Quantity::from("1.000"),
AggressorSide::Buyer,
TradeId::new("trigger-1"),
UnixNanos::from(2u64),
UnixNanos::from(2u64),
);
engine_l2.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let triggered = match saved_messages.first().unwrap() {
OrderEventAny::Triggered(triggered) => triggered,
other => panic!("Expected OrderTriggered event first, was {other:?}"),
};
let filled = match saved_messages.get(1).unwrap() {
OrderEventAny::Filled(filled) => filled,
other => panic!("Expected OrderFilled event second, was {other:?}"),
};
assert_eq!(triggered.client_order_id, client_order_id);
assert_eq!(filled.client_order_id, client_order_id);
assert_eq!(filled.liquidity_side, LiquiditySide::Taker);
}
#[rstest]
fn test_passive_post_only_stop_limit_rejected_when_triggered_as_taker(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
reject_stop_orders: false,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("2.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&ask).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_limit = OrderTestBuilder::new(OrderType::StopLimit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1505.00"))
.price(Price::from("1506.00"))
.quantity(Quantity::from("1.000"))
.post_only(true)
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut stop_limit, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1505.00"),
Quantity::from("1.000"),
AggressorSide::Buyer,
TradeId::new("trigger-1"),
UnixNanos::from(2u64),
UnixNanos::from(2u64),
);
engine_l2.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let triggered = match saved_messages.first().unwrap() {
OrderEventAny::Triggered(triggered) => triggered,
other => panic!("Expected OrderTriggered event first, was {other:?}"),
};
let rejected = match saved_messages.get(1).unwrap() {
OrderEventAny::Rejected(rejected) => rejected,
other => panic!("Expected OrderRejected event second, was {other:?}"),
};
assert_eq!(triggered.client_order_id, client_order_id);
assert_eq!(rejected.client_order_id, client_order_id);
assert!(rejected.due_post_only);
assert!(!engine_l2.order_exists(client_order_id));
}
#[rstest]
#[case::stop_limit(OrderType::StopLimit, "1495.00")]
#[case::limit_if_touched(OrderType::LimitIfTouched, "1500.00")]
fn test_immediate_post_only_limit_style_trigger_rejected_as_taker(
#[case] order_type: OrderType,
#[case] trigger_price: &str,
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
reject_stop_orders: false,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("2.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&ask).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut order = OrderTestBuilder::new(order_type)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from(trigger_price))
.price(Price::from("1505.00"))
.quantity(Quantity::from("1.000"))
.post_only(true)
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 3);
let accepted = match saved_messages.first().unwrap() {
OrderEventAny::Accepted(accepted) => accepted,
event => panic!("Expected OrderAccepted event first, was {event:?}"),
};
let triggered = match saved_messages.get(1).unwrap() {
OrderEventAny::Triggered(triggered) => triggered,
event => panic!("Expected OrderTriggered event second, was {event:?}"),
};
let rejected = match saved_messages.get(2).unwrap() {
OrderEventAny::Rejected(rejected) => rejected,
event => panic!("Expected OrderRejected event third, was {event:?}"),
};
assert_eq!(accepted.client_order_id, client_order_id);
assert_eq!(triggered.client_order_id, client_order_id);
assert_eq!(rejected.client_order_id, client_order_id);
assert!(rejected.due_post_only);
assert!(!engine_l2.order_exists(client_order_id));
}
#[rstest]
fn test_passive_stop_limit_rekeys_to_limit_after_trigger(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
reject_stop_orders: false,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("2.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&ask).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_limit = OrderTestBuilder::new(OrderType::StopLimit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1505.00"))
.price(Price::from("1502.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut stop_limit, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1505.00"),
Quantity::from("1.000"),
AggressorSide::Buyer,
TradeId::new("trigger-1"),
UnixNanos::from(2u64),
UnixNanos::from(2u64),
);
engine_l2.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
assert!(matches!(
saved_messages.first().unwrap(),
OrderEventAny::Triggered(_)
));
assert!(engine_l2.order_exists(client_order_id));
clear_order_event_handler_messages(&order_event_handler);
let bid = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1499.00"),
Quantity::from("1.000"),
2,
))
.ts_init(UnixNanos::from(3u64))
.build();
engine_l2.process_order_book_delta(&bid).unwrap();
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let filled = match saved_messages.first().unwrap() {
OrderEventAny::Filled(filled) => filled,
other => panic!("Expected OrderFilled event after re-key, was {other:?}"),
};
assert_eq!(filled.client_order_id, client_order_id);
assert_eq!(filled.liquidity_side, LiquiditySide::Maker);
}
#[rstest]
fn test_process_cancel_command_valid(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
let cancel_command = CancelOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
engine_l2.process_order(&mut limit_order, account_id);
engine_l2.process_cancel(&cancel_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let canceled = match event2 {
OrderEventAny::Canceled(canceled) => canceled,
_ => panic!("Expected OrderCanceled event in second message"),
};
assert_eq!(canceled.client_order_id, client_order_id);
}
#[rstest]
fn test_process_cancel_command_order_not_found(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let account_id = AccountId::from("ACCOUNT-001");
let cancel_command = CancelOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_cancel(&cancel_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let event = saved_messages.first().unwrap();
let rejected = match event {
OrderEventAny::CancelRejected(rejected) => rejected,
_ => panic!("Expected OrderRejected event in first message"),
};
assert_eq!(rejected.client_order_id, client_order_id);
assert_eq!(
rejected.reason,
Ustr::from(format!("Order {client_order_id} not found").as_str())
);
}
#[rstest]
fn test_process_cancel_all_command(instrument_eth_usdt: InstrumentAny, account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
None,
);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id_1 = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order_1 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_1)
.submit(true)
.build();
cache
.borrow_mut()
.add_order(limit_order_1.clone(), None, None, false)
.unwrap();
engine_l2.process_order(&mut limit_order_1, account_id);
let client_order_id_2 = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut limit_order_2 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1496.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_2)
.submit(true)
.build();
cache
.borrow_mut()
.add_order(limit_order_2.clone(), None, None, false)
.unwrap();
engine_l2.process_order(&mut limit_order_2, account_id);
let client_order_id_3 = ClientOrderId::from("O-19700101-000000-001-001-3");
let mut limit_order_3 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("BTCUSDT-PERP.BINANCE")) .side(OrderSide::Buy)
.price(Price::from("1497.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_3)
.submit(true)
.build();
cache
.borrow_mut()
.add_order(limit_order_3.clone(), None, None, false)
.unwrap();
engine_l2.process_order(&mut limit_order_3, account_id);
let cancel_all_command = CancelAllOrders::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
OrderSide::Buy,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_cancel_all(&cancel_all_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 5);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id_1);
let event2 = saved_messages.get(1).unwrap();
let accepted = match event2 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in second message"),
};
assert_eq!(accepted.client_order_id, client_order_id_2);
let event3 = saved_messages.get(2).unwrap();
let accepted = match event3 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in third message"),
};
assert_eq!(accepted.client_order_id, client_order_id_3);
let event4 = saved_messages.get(3).unwrap();
let canceled1 = match event4 {
OrderEventAny::Canceled(canceled) => canceled,
_ => panic!("Expected OrderCanceled event in fourth message"),
};
let event5 = saved_messages.get(4).unwrap();
let canceled2 = match event5 {
OrderEventAny::Canceled(canceled) => canceled,
_ => panic!("Expected OrderCanceled event in fifth message"),
};
let ids = [canceled1.client_order_id, canceled2.client_order_id];
assert!(ids.contains(&client_order_id_1));
assert_eq!(canceled1.instrument_id, instrument_eth_usdt.id());
assert!(ids.contains(&client_order_id_2));
assert_eq!(canceled2.instrument_id, instrument_eth_usdt.id());
}
#[rstest]
fn test_process_batch_cancel_command(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, Some(cache), None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id_1 = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order_1 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_1)
.submit(true)
.build();
let client_order_id_2 = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut limit_order_2 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1496.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_2)
.submit(true)
.build();
engine_l2.process_order(&mut limit_order_1, account_id);
engine_l2.process_order(&mut limit_order_2, account_id);
let cancel_1 = CancelOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id_1,
Some(VenueOrderId::from("V1")),
UUID4::new(),
UnixNanos::default(),
None,
None, );
let cancel_2 = CancelOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id_2,
Some(VenueOrderId::from("V2")),
UUID4::new(),
UnixNanos::default(),
None,
None, );
let batch_cancel_command = BatchCancelOrders::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
vec![cancel_1, cancel_2],
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_batch_cancel(&batch_cancel_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 4);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id_1);
let event2 = saved_messages.get(1).unwrap();
let accepted = match event2 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in second message"),
};
assert_eq!(accepted.client_order_id, client_order_id_2);
let event3 = saved_messages.get(2).unwrap();
let canceled1 = match event3 {
OrderEventAny::Canceled(canceled) => canceled,
_ => panic!("Expected OrderCanceled event in third message"),
};
let event4 = saved_messages.get(3).unwrap();
let _canceled2 = match event4 {
OrderEventAny::Canceled(canceled) => canceled,
_ => panic!("Expected OrderCanceled event in fourth message"),
};
assert_eq!(canceled1.client_order_id, client_order_id_1);
}
#[rstest]
fn test_process_cancel_skips_already_canceled_order(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
None,
);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit_order, account_id);
let cached_order = cache.borrow().order(&client_order_id).unwrap().clone();
let canceled_event =
TestOrderEventStubs::canceled(&cached_order, account_id, Some(VenueOrderId::from("V1")));
cache
.borrow_mut()
.order_mut(&client_order_id)
.unwrap()
.apply(canceled_event)
.unwrap();
clear_order_event_handler_messages(&order_event_handler);
let cancel_command = CancelOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
UUID4::new(),
UnixNanos::default(),
None,
None, );
assert!(
engine_l2.order_exists(client_order_id),
"matching core should still hold the order before process_cancel runs",
);
engine_l2.process_cancel(&cancel_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert!(
saved_messages.is_empty(),
"expected no events for cancel of already-terminal order, found {saved_messages:?}",
);
assert!(
!engine_l2.order_exists(client_order_id),
"stale matching-core entry should be purged when the gate fires",
);
}
#[rstest]
fn test_iterate_purges_already_canceled_order_from_core(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
None,
);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit_order, account_id);
let cached_order = cache.borrow().order(&client_order_id).unwrap().clone();
let canceled_event =
TestOrderEventStubs::canceled(&cached_order, account_id, Some(VenueOrderId::from("V1")));
cache
.borrow_mut()
.order_mut(&client_order_id)
.unwrap()
.apply(canceled_event)
.unwrap();
clear_order_event_handler_messages(&order_event_handler);
assert!(
engine_l2.order_exists(client_order_id),
"matching core should still hold the order before iterate runs",
);
engine_l2.iterate(UnixNanos::from(1), AggressorSide::NoAggressor);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert!(
saved_messages.is_empty(),
"expected no events while purging stale closed order, found {saved_messages:?}",
);
assert!(
!engine_l2.order_exists(client_order_id),
"stale closed order should be purged during matching iteration",
);
}
#[rstest]
fn test_process_cancel_all_skips_orders_closed_by_contingent_cascade(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
engine_config: OrderMatchingEngineConfig,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
Some(engine_config),
);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id_1 = ClientOrderId::from("O-19700101-000000-001-001-1");
let client_order_id_2 = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut limit_order_1 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_1)
.contingency_type(ContingencyType::Ouo)
.linked_order_ids(vec![client_order_id_2])
.submit(true)
.build();
let mut limit_order_2 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1496.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_2)
.contingency_type(ContingencyType::Ouo)
.linked_order_ids(vec![client_order_id_1])
.submit(true)
.build();
cache
.borrow_mut()
.add_order(limit_order_1.clone(), None, None, false)
.unwrap();
cache
.borrow_mut()
.add_order(limit_order_2.clone(), None, None, false)
.unwrap();
engine_l2.process_order(&mut limit_order_1, account_id);
engine_l2.process_order(&mut limit_order_2, account_id);
clear_order_event_handler_messages(&order_event_handler);
let cancel_all_command = CancelAllOrders::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
OrderSide::Buy,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_cancel_all(&cancel_all_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let canceled_ids: Vec<_> = saved_messages
.iter()
.filter_map(|e| match e {
OrderEventAny::Canceled(c) => Some(c.client_order_id),
_ => None,
})
.collect();
assert_eq!(
canceled_ids.len(),
2,
"expected 2 canceled events (one per leg), found {saved_messages:?}",
);
assert!(canceled_ids.contains(&client_order_id_1));
assert!(canceled_ids.contains(&client_order_id_2));
}
#[rstest]
fn test_expire_order(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let engine_config = OrderMatchingEngineConfig {
support_gtd_orders: true,
..Default::default()
};
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
None,
None,
Some(engine_config),
);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let expire_time = DateTime::parse_from_rfc3339("2019-10-23T10:32:49.669Z")
.unwrap()
.with_timezone(&Utc)
.timestamp_nanos_opt()
.unwrap();
let tick_time = DateTime::parse_from_rfc3339("2025-10-23T10:32:50.000Z")
.unwrap()
.with_timezone(&Utc)
.timestamp_nanos_opt()
.unwrap();
let mut limit_order_expire = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.expire_time(UnixNanos::from(expire_time as u64))
.client_order_id(client_order_id)
.submit(true)
.build();
let tick = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1500.00"),
Quantity::from("1.000"),
AggressorSide::Buyer,
TradeId::new("1"),
UnixNanos::from(tick_time as u64),
UnixNanos::from(tick_time as u64),
);
engine_l2.process_order(&mut limit_order_expire, account_id);
engine_l2.process_trade_tick(&tick);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let expired = match event2 {
OrderEventAny::Expired(expired) => expired,
_ => panic!("Expected OrderExpired event in second message"),
};
assert_eq!(expired.client_order_id, client_order_id);
}
#[rstest]
fn test_process_modify_order_rejected_not_found(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let modify_order_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
None,
None,
None,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_order_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let event = saved_messages.first().unwrap();
let rejected = match event {
OrderEventAny::ModifyRejected(rejected) => rejected,
_ => panic!("Expected OrderRejected event in first message"),
};
assert_eq!(rejected.client_order_id, client_order_id);
}
#[rstest]
fn test_rejected_post_only_modify_preserves_fifo_priority(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let id_first = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_first = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(id_first)
.post_only(true)
.submit(true)
.build();
engine_l2.process_order(&mut limit_first, account_id);
let id_second = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut limit_second = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(id_second)
.post_only(true)
.submit(true)
.build();
engine_l2.process_order(&mut limit_second, account_id);
let modify_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
id_first,
Some(VenueOrderId::from("V1")),
None,
Some(Price::from("1500.00")),
None,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let rejected_count = saved_messages
.iter()
.filter(|e| matches!(e, OrderEventAny::ModifyRejected(_)))
.count();
assert_eq!(rejected_count, 1, "post-only modify should be rejected");
assert!(
!saved_messages
.iter()
.any(|e| matches!(e, OrderEventAny::Updated(_))),
"rejected modify must not dispatch OrderUpdated, was {saved_messages:?}",
);
let bids = engine_l2.get_core().get_orders_bid();
assert_eq!(bids.len(), 2, "both orders should remain in core");
assert_eq!(
bids[0].client_order_id, id_first,
"first-inserted order must keep FIFO priority after rejected modify",
);
assert_eq!(bids[1].client_order_id, id_second);
}
#[rstest]
fn test_rejected_post_only_modify_preserves_queue_position(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let (mut engine, cache, handler) = get_l1_queue_position_engine(instrument_eth_usdt.clone());
let quote_initial = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Price::from("101.00"),
Quantity::from("10.000"),
Quantity::from("5.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e_initial);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("100.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.post_only(true)
.submit(true)
.build();
cache
.borrow_mut()
.add_order(order.clone(), None, None, false)
.unwrap();
engine.process_order(&mut order, account_id);
clear_order_event_handler_messages(&handler);
let trade_drain = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Quantity::from("10.000"),
AggressorSide::Seller,
TradeId::new("DRAIN"),
UnixNanos::from(1u64),
UnixNanos::from(1u64),
);
engine.process_trade_tick(&trade_drain);
assert!(
get_order_event_handler_messages(&handler)
.iter()
.all(|e| !matches!(e, OrderEventAny::Filled(_))),
"drain trade exactly consumes the queue, must not fill the order yet",
);
let quote_replenish = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Price::from("101.00"),
Quantity::from("10.000"),
Quantity::from("5.000"),
UnixNanos::from(2u64),
UnixNanos::from(2u64),
);
engine.process_quote_tick("e_replenish);
let modify_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
None,
Some(Price::from("101.00")),
None,
UUID4::new(),
UnixNanos::from(3u64),
None,
None, );
engine.process_modify(&modify_command, account_id);
let after_modify = get_order_event_handler_messages(&handler);
assert!(
after_modify
.iter()
.any(|e| matches!(e, OrderEventAny::ModifyRejected(_))),
"post-only modify into ask must be rejected, was {after_modify:?}",
);
clear_order_event_handler_messages(&handler);
let trade_fill = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Quantity::from("1.000"),
AggressorSide::Seller,
TradeId::new("FILL"),
UnixNanos::from(4u64),
UnixNanos::from(4u64),
);
engine.process_trade_tick(&trade_fill);
let messages = get_order_event_handler_messages(&handler);
let fill = messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.unwrap_or_else(|| {
panic!(
"queue_ahead must have stayed at zero across the rejected modify; \
single-unit trade should fill the order, was events {messages:?}",
)
});
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_rejected_stop_market_modify_preserves_fifo_priority(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let id_first = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_first = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1505.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(id_first)
.submit(true)
.build();
engine_l2.process_order(&mut stop_first, account_id);
let id_second = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut stop_second = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1505.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(id_second)
.submit(true)
.build();
engine_l2.process_order(&mut stop_second, account_id);
let modify_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
id_first,
Some(VenueOrderId::from("V1")),
None,
None,
Some(Price::from("1495.00")),
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let rejected_count = saved_messages
.iter()
.filter(|e| matches!(e, OrderEventAny::ModifyRejected(_)))
.count();
assert_eq!(
rejected_count, 1,
"in-market stop modify should be rejected"
);
assert!(
!saved_messages
.iter()
.any(|e| matches!(e, OrderEventAny::Updated(_))),
"rejected stop modify must not dispatch OrderUpdated, was {saved_messages:?}",
);
let bids = engine_l2.get_core().get_orders_bid();
assert_eq!(bids.len(), 2, "both stops should remain in core");
assert_eq!(
bids[0].client_order_id, id_first,
"first-inserted stop must keep FIFO priority after rejected modify",
);
assert_eq!(bids[1].client_order_id, id_second);
}
#[rstest]
fn test_update_limit_order_post_only_matched(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.post_only(true)
.submit(true)
.build();
engine_l2.process_order(&mut limit_order, account_id);
let modify_order_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
None,
Some(Price::from("1500.00")), None,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_order_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let rejected = match event2 {
OrderEventAny::ModifyRejected(rejected) => rejected,
_ => panic!("Expected OrderModifyRejected event in second message"),
};
assert_eq!(rejected.client_order_id, client_order_id);
assert_eq!(
rejected.reason,
Ustr::from(
"POST_ONLY LIMIT BUY order with new limit px of 1500.00 would have been a TAKER: bid=None, ask=1500.00"
)
);
}
#[rstest]
fn test_update_limit_order_valid(instrument_eth_usdt: InstrumentAny, account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
None,
);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
cache
.borrow_mut()
.add_order(limit_order.clone(), None, None, false)
.unwrap();
engine_l2.process_order(&mut limit_order, account_id);
let new_limit_price = Price::from("1500.00");
let modify_order_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
None,
Some(new_limit_price),
None,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_order_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 3);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let updated = match event2 {
OrderEventAny::Updated(updated) => updated,
_ => panic!("Expected OrderUpdated event in second message"),
};
assert_eq!(updated.client_order_id, client_order_id);
assert_eq!(updated.price.unwrap(), new_limit_price);
let event3 = saved_messages.get(2).unwrap();
let fill = match event3 {
OrderEventAny::Filled(fill) => fill,
_ => panic!("Expected OrderFilled event in third message"),
};
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_px, new_limit_price);
assert_eq!(fill.last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_update_stop_market_order_valid(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_market_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1505.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut stop_market_order, account_id);
let new_trigger_price = Price::from("1501.00");
let modify_order_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
None,
None,
Some(new_trigger_price),
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_order_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let updated = match event2 {
OrderEventAny::Updated(updated) => updated,
_ => panic!("Expected OrderUpdated event in second message"),
};
assert_eq!(updated.client_order_id, client_order_id);
assert_eq!(updated.trigger_price.unwrap(), new_trigger_price);
}
#[rstest]
fn test_update_stop_limit_order_valid_update_not_triggered(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_market_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1502.00"))
.trigger_price(Price::from("1505.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut stop_market_order, account_id);
let new_trigger_price = Price::from("1501.00");
let modify_order_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
None,
None,
Some(new_trigger_price),
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_order_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let updated = match event2 {
OrderEventAny::Updated(updated) => updated,
_ => panic!("Expected OrderUpdated event in second message"),
};
assert_eq!(updated.client_order_id, client_order_id);
assert_eq!(updated.trigger_price.unwrap(), new_trigger_price);
}
#[rstest]
fn test_process_market_if_touched_order_already_triggered(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
reject_stop_orders: false,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_market_order = OrderTestBuilder::new(OrderType::MarketIfTouched)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1500.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut stop_market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let event = saved_messages.first().unwrap();
let fill = match event {
OrderEventAny::Filled(fill) => fill,
_ => panic!("Expected OrderFilled event in first message"),
};
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_px, Price::from("1500.00"));
assert_eq!(fill.last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_update_market_if_touched_order_valid(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_market_order = OrderTestBuilder::new(OrderType::MarketIfTouched)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1505.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut stop_market_order, account_id);
let new_trigger_price = Price::from("1501.00");
let modify_order_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
None,
None,
Some(new_trigger_price),
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_order_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let updated = match event2 {
OrderEventAny::Updated(updated) => updated,
_ => panic!("Expected OrderUpdated event in second message"),
};
assert_eq!(updated.client_order_id, client_order_id);
assert_eq!(updated.trigger_price.unwrap(), new_trigger_price);
}
#[rstest]
fn test_process_limit_if_touched_order_immediate_trigger_and_fill(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
reject_stop_orders: false,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_if_touched_order = OrderTestBuilder::new(OrderType::LimitIfTouched)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1500.00"))
.price(Price::from("1505.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit_if_touched_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 3);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let triggered = match event2 {
OrderEventAny::Triggered(triggered) => triggered,
_ => panic!("Expected OrderTriggered event in second message"),
};
assert_eq!(triggered.client_order_id, client_order_id);
let event3 = saved_messages.get(2).unwrap();
let fill = match event3 {
OrderEventAny::Filled(fill) => fill,
_ => panic!("Expected OrderFilled event in third message"),
};
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_px, Price::from("1500.00"));
assert_eq!(fill.last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_update_limit_if_touched_order_valid(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::new("O-19700101-000000-001-001-1");
let mut limit_if_touched_order = OrderTestBuilder::new(OrderType::LimitIfTouched)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1498.00"))
.price(Price::from("1505.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit_if_touched_order, account_id);
let new_trigger_price = Price::from("1499.00");
let modify_order_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
None,
None,
Some(new_trigger_price),
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_order_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let updated = match event2 {
OrderEventAny::Updated(updated) => updated,
_ => panic!("Expected OrderUpdated event in second message"),
};
assert_eq!(updated.client_order_id, client_order_id);
assert_eq!(updated.trigger_price.unwrap(), new_trigger_price);
}
#[rstest]
fn test_process_market_to_limit_orders_not_fully_filled(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut market_to_limit_order = OrderTestBuilder::new(OrderType::MarketToLimit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("2.000"))
.client_order_id(client_order_id)
.build();
let submitted = TestOrderEventStubs::submitted(&market_to_limit_order, account_id);
market_to_limit_order.apply(submitted).unwrap();
engine_l2.process_order(&mut market_to_limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 3);
let event1 = saved_messages.first().unwrap();
let updated = match event1 {
OrderEventAny::Updated(order) => order,
_ => panic!("Expected OrderUpdated event in first message"),
};
assert_eq!(updated.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let fill = match event2 {
OrderEventAny::Filled(order) => order,
_ => panic!("Expected OrderFilled event in second message"),
};
assert_eq!(fill.client_order_id, client_order_id);
let event3 = saved_messages.get(2).unwrap();
let accepted = match event3 {
OrderEventAny::Accepted(order) => order,
_ => panic!("Expected OrderFilled event in third message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_px, Price::from("1500.00"));
assert_eq!(fill.last_qty, Quantity::from("1.000"));
let resting_orders = engine_l2.get_core().get_orders();
assert_eq!(resting_orders.len(), 1);
let first_order = resting_orders.first().unwrap();
assert_eq!(first_order.client_order_id, client_order_id);
}
#[rstest]
fn test_process_trailing_stop_orders_rejeceted_and_valid(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id_trailing_stop_market = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut trailing_stop_market = OrderTestBuilder::new(OrderType::TrailingStopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1498.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_trailing_stop_market)
.trailing_offset(dec!(0.1))
.limit_offset(dec!(0.1))
.trailing_offset_type(TrailingOffsetType::Price)
.submit(true)
.build();
let client_order_id_trailing_stop_limit = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1502.00"))
.price(Price::from("1500.00"))
.quantity(Quantity::from("1.000"))
.trailing_offset(dec!(0.1))
.limit_offset(dec!(0.1))
.trailing_offset_type(TrailingOffsetType::Price)
.client_order_id(client_order_id_trailing_stop_limit)
.submit(true)
.build();
engine_l2.process_order(&mut trailing_stop_market, account_id);
engine_l2.process_order(&mut trailing_stop_limit, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let event1 = saved_messages.first().unwrap();
let rejected = match event1 {
OrderEventAny::Rejected(rejected) => rejected,
_ => panic!("Expected OrderRejected event in first message"),
};
assert_eq!(
rejected.client_order_id,
client_order_id_trailing_stop_market
);
let event2 = saved_messages.get(1).unwrap();
let accepted = match event2 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in second message"),
};
assert_eq!(
accepted.client_order_id,
client_order_id_trailing_stop_limit
);
}
#[rstest]
fn test_updating_of_trailing_stop_market_order_with_no_trigger_price_set(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut trailing_stop_market = OrderTestBuilder::new(OrderType::TrailingStopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.trigger_price(Price::from("1505.00"))
.trigger_type(TriggerType::LastPrice)
.trailing_offset(dec!(1))
.limit_offset(dec!(1))
.trailing_offset_type(TrailingOffsetType::Price)
.submit(true)
.build();
engine_l2.process_order(&mut trailing_stop_market, account_id);
let tick = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1480.00"),
Quantity::from("1.000"),
AggressorSide::Seller,
TradeId::from("1"),
UnixNanos::default(),
UnixNanos::default(),
);
engine_l2.process_trade_tick(&tick);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id);
let event2 = saved_messages.get(1).unwrap();
let updated = match event2 {
OrderEventAny::Updated(updated) => updated,
_ => panic!("Expected OrderUpdated event in second message"),
};
assert_eq!(updated.client_order_id, client_order_id);
assert_eq!(updated.trigger_price.unwrap(), Price::from("1481.00"));
}
#[rstest]
#[case(ContingencyType::Oco)]
#[case(ContingencyType::Ouo)]
fn test_updating_of_contingent_orders(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
#[case] contingency_type: ContingencyType,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let engine_config = OrderMatchingEngineConfig {
support_contingent_orders: true,
..Default::default()
};
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
Some(engine_config),
);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id_primary = ClientOrderId::from("O-19700101-000000-001-001-1");
let client_order_id_contingent = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut primary_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_primary)
.contingency_type(contingency_type)
.linked_order_ids(vec![client_order_id_contingent])
.submit(true)
.build();
let mut contingent_stop_market_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("1500.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_contingent)
.linked_order_ids(vec![client_order_id_primary])
.contingency_type(contingency_type)
.submit(true)
.build();
cache
.borrow_mut()
.add_order(primary_order.clone(), None, None, false)
.unwrap();
cache
.borrow_mut()
.add_order(contingent_stop_market_order.clone(), None, None, false)
.unwrap();
engine_l2.process_order(&mut primary_order, account_id);
engine_l2.process_order(&mut contingent_stop_market_order, account_id);
let modify_order_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id_primary,
Some(VenueOrderId::from("V1")),
Some(Quantity::from("2.000")),
None,
None,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_order_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 4);
let event1 = saved_messages.first().unwrap();
let accepted = match event1 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(accepted.client_order_id, client_order_id_primary);
let event2 = saved_messages.get(1).unwrap();
let accepted = match event2 {
OrderEventAny::Accepted(accepted) => accepted,
_ => panic!("Expected OrderAccepted event in second message"),
};
assert_eq!(accepted.client_order_id, client_order_id_contingent);
let event3 = saved_messages.get(2).unwrap();
let updated = match event3 {
OrderEventAny::Updated(updated) => updated,
_ => panic!("Expected OrderUpdated event in third message"),
};
assert_eq!(updated.client_order_id, client_order_id_primary);
assert_eq!(updated.quantity, Quantity::from("2.000"));
let event4 = saved_messages.get(3).unwrap();
let updated = match event4 {
OrderEventAny::Updated(updated) => updated,
_ => panic!("Expected OrderUpdated event in fourth message"),
};
assert_eq!(updated.client_order_id, client_order_id_contingent);
assert_eq!(updated.quantity, Quantity::from("2.000"));
}
#[rstest]
fn test_reduce_only_order_exceeding_position_quantity(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
engine_config: OrderMatchingEngineConfig,
) {
let mut engine = get_order_matching_engine(
instrument_eth_usdt.clone(),
None,
None,
None,
Some(engine_config),
);
let mut buy_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("79.000"))
.submit(true)
.build();
engine.process_order(&mut buy_order, account_id);
let mut reduce_only_sell = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.quantity(Quantity::from("80.000")) .price(Price::from("1500.00"))
.reduce_only(true)
.submit(true)
.build();
engine.process_order(&mut reduce_only_sell, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert!(saved_messages.len() >= 2, "Should have at least 2 events");
let has_quantity_update = saved_messages.iter().any(|event| {
matches!(event, OrderEventAny::Updated(updated) if updated.quantity == Quantity::from("79.000"))
});
assert!(
has_quantity_update || !saved_messages.is_empty(),
"Order should be processed without panic"
);
}
#[rstest]
fn test_reduce_only_stop_market_caps_cumulative_multi_level_fill(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
cache
.borrow_mut()
.add_instrument(instrument_eth_usdt.clone())
.unwrap();
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
None,
);
let position_id = PositionId::new(
format!(
"{}-{}",
instrument_eth_usdt.id(),
StrategyId::test_default()
)
.as_str(),
);
let opening_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("2382.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-OPEN"))
.build();
let opening_fill = build_order_filled(
opening_order.trader_id(),
opening_order.strategy_id(),
opening_order.instrument_id(),
opening_order.client_order_id(),
VenueOrderId::from("V-OPEN"),
account_id,
TradeId::new("E-OPEN"),
opening_order.order_side(),
opening_order.order_type(),
opening_order.quantity(),
Price::from("1495.00"),
instrument_eth_usdt.quote_currency(),
LiquiditySide::Taker,
Some(position_id),
None,
);
let position = Position::new(&instrument_eth_usdt, opening_fill);
cache
.borrow_mut()
.add_position(&position, OmsType::Netting)
.unwrap();
let pre_trigger_bid = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1491.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&pre_trigger_bid)
.unwrap();
for (order_id, price, quantity) in [
(2, "1490.00", "1000.000"),
(3, "1489.00", "1000.000"),
(4, "1488.00", "1000.000"),
] {
let bid = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from(price),
Quantity::from(quantity),
order_id,
))
.build();
engine_l2.process_order_book_delta(&bid).unwrap();
}
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-RO");
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("1490.00"))
.quantity(Quantity::from("7142.000"))
.client_order_id(client_order_id)
.reduce_only(true)
.submit(true)
.build();
engine_l2.process_order(&mut stop_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trigger_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Delete)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1491.00"),
Quantity::from("0.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&trigger_delta).unwrap();
let messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<&OrderFilled> = messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) if fill.client_order_id == client_order_id => Some(fill),
_ => None,
})
.collect();
let updated = messages
.iter()
.find_map(|event| match event {
OrderEventAny::Updated(updated) if updated.client_order_id == client_order_id => {
Some(updated)
}
_ => None,
})
.expect("Expected reduce-only order quantity update");
assert_eq!(fills.len(), 3);
assert_eq!(fills[0].last_qty, Quantity::from("1000.000"));
assert_eq!(fills[1].last_qty, Quantity::from("1000.000"));
assert_eq!(fills[2].last_qty, Quantity::from("382.000"));
assert_eq!(updated.quantity, Quantity::from("2382.000"));
let cache_ref = cache.borrow();
let order = cache_ref
.order(&client_order_id)
.expect("Expected reduce-only order in cache");
assert_eq!(order.quantity(), Quantity::from("2382.000"));
assert_eq!(order.filled_qty(), Quantity::from("2382.000"));
assert_eq!(order.status(), OrderStatus::Filled);
}
#[rstest]
fn test_process_market_orders_with_protection_rejeceted_and_valid(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig::builder()
.trade_execution(false)
.price_protection_points(600u32)
.build();
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let _ = engine_l2.process_order_book_delta(&orderbook_delta_sell);
let client_order_id_market_buy = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut market_buy_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_market_buy)
.submit(true)
.build();
let client_order_id_market_sell = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut market_sell_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_market_sell)
.submit(true)
.build();
engine_l2.process_order(&mut market_sell_order, account_id);
engine_l2.process_order(&mut market_buy_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let rejected = match saved_messages.first().unwrap() {
OrderEventAny::Rejected(rejected) => rejected,
_ => panic!("Expected OrderRejected event in first message"),
};
assert_eq!(rejected.client_order_id, client_order_id_market_sell);
assert_eq!(rejected.reason, "No market for ETHUSDT-PERP.BINANCE");
let filled = match saved_messages.get(1).unwrap() {
OrderEventAny::Filled(filled) => filled,
_ => panic!("Expected Filled event in second message"),
};
assert_eq!(filled.client_order_id, client_order_id_market_buy);
}
#[rstest]
fn test_process_stop_orders_with_protection_both_accepted(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig::builder()
.trade_execution(false)
.price_protection_points(600u32)
.build();
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let _ = engine_l2.process_order_book_delta(&orderbook_delta_sell);
let client_order_id_market_buy = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut market_buy_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.trigger_price(Price::new(1501.0, 2))
.client_order_id(client_order_id_market_buy)
.submit(true)
.build();
let client_order_id_market_sell = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut market_sell_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.quantity(Quantity::from("1.000"))
.trigger_price(Price::new(1499.0, 2))
.client_order_id(client_order_id_market_sell)
.submit(true)
.build();
engine_l2.process_order(&mut market_sell_order, account_id);
engine_l2.process_order(&mut market_buy_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let accepted1 = match saved_messages.first().unwrap() {
OrderEventAny::Accepted(accepted) => accepted,
other => panic!("Expected OrderAccepted event in first message, was {other:?}"),
};
assert_eq!(accepted1.client_order_id, client_order_id_market_sell);
let accepted2 = match saved_messages.get(1).unwrap() {
OrderEventAny::Accepted(accepted) => accepted,
other => panic!("Expected OrderAccepted event in second message, was {other:?}"),
};
assert_eq!(accepted2.client_order_id, client_order_id_market_buy);
}
#[rstest]
fn test_protection_filtered_fills_do_not_consume_liquidity(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
liquidity_consumption: true,
price_protection_points: Some(100), ..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1000.00"),
Quantity::from("100.000"),
0,
))
.build();
let _ = engine_l2.process_order_book_delta(&bid_delta);
let ask_delta_1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1001.00"),
Quantity::from("2.000"),
1,
))
.build();
let _ = engine_l2.process_order_book_delta(&ask_delta_1);
let ask_delta_2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1002.00"),
Quantity::from("5.000"),
2,
))
.build();
let _ = engine_l2.process_order_book_delta(&ask_delta_2);
let ask_delta_3 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1003.00"),
Quantity::from("5.000"),
3,
))
.build();
let _ = engine_l2.process_order_book_delta(&ask_delta_3);
let mut order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("10.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine_l2.process_order(&mut order1, account_id);
let messages = get_order_event_handler_messages(&order_event_handler);
let fills1: Vec<_> = messages
.iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
let total_filled_1: f64 = fills1.iter().map(|f| f.last_qty.as_f64()).sum();
assert_eq!(
total_filled_1, 7.0,
"Order1 should fill 7 units (2 at 1001 + 5 at 1002)"
);
clear_order_event_handler_messages(&order_event_handler);
let mut order2 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.quantity(Quantity::from("5.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build();
engine_l2.process_order(&mut order2, account_id);
let messages = get_order_event_handler_messages(&order_event_handler);
let fill2 = messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("Expected fill for order2");
assert_eq!(fill2.last_qty, Quantity::from("5.000"));
assert_eq!(fill2.last_px, Price::from("1000.00"));
}
#[rstest]
fn test_process_mark_bar_skipped_without_panic(instrument_eth_usdt: InstrumentAny) {
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine = get_order_matching_engine(instrument_eth_usdt, None, None, None, Some(config));
let bar_type = BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-MARK-EXTERNAL");
let mark_bar = Bar {
bar_type,
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("100000.000"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
engine.process_bar(&mark_bar);
assert!(
engine.get_core().last.is_none(),
"Mark price bar should be skipped and not update market state"
);
}
#[rstest]
fn test_process_mark_bar_does_not_replace_selected_execution_bar(
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine = get_order_matching_engine(instrument_eth_usdt, None, None, None, Some(config));
let hourly_bar_type = BarType::from("ETHUSDT-PERP.BINANCE-1-HOUR-LAST-EXTERNAL");
let first_hourly_bar = Bar {
bar_type: hourly_bar_type,
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("100000.000"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
let mark_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-MARK-EXTERNAL"),
open: Price::from("1400.00"),
high: Price::from("1410.00"),
low: Price::from("1390.00"),
close: Price::from("1405.00"),
volume: Quantity::from("100000.000"),
ts_event: UnixNanos::from(2_000_000_000),
ts_init: UnixNanos::from(2_000_000_000),
};
let second_hourly_bar = Bar {
bar_type: hourly_bar_type,
open: Price::from("1600.00"),
high: Price::from("1610.00"),
low: Price::from("1590.00"),
close: Price::from("1605.00"),
volume: Quantity::from("100000.000"),
ts_event: UnixNanos::from(3_000_000_000),
ts_init: UnixNanos::from(3_000_000_000),
};
engine.process_bar(&first_hourly_bar);
engine.process_bar(&mark_bar);
engine.process_bar(&second_hourly_bar);
assert_eq!(engine.get_core().last, Some(Price::from("1605.00")));
}
#[rstest]
fn test_process_monthly_bar_not_skipped(instrument_eth_usdt: InstrumentAny) {
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine = get_order_matching_engine(instrument_eth_usdt, None, None, None, Some(config));
let bar_type = BarType::from("ETHUSDT-PERP.BINANCE-1-MONTH-LAST-EXTERNAL");
let monthly_bar = Bar {
bar_type,
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("100000.000"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
engine.process_bar(&monthly_bar);
assert!(
engine.get_core().last.is_some(),
"Monthly bar should be processed and update market state"
);
}
#[rstest]
fn test_process_yearly_bar_not_skipped(instrument_eth_usdt: InstrumentAny) {
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine = get_order_matching_engine(instrument_eth_usdt, None, None, None, Some(config));
let bar_type = BarType::from("ETHUSDT-PERP.BINANCE-1-YEAR-LAST-EXTERNAL");
let yearly_bar = Bar {
bar_type,
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("100000.000"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
engine.process_bar(&yearly_bar);
assert!(
engine.get_core().last.is_some(),
"Yearly bar should be processed and update market state"
);
}
#[rstest]
fn test_process_trade_bar_fills_order_with_volume_not_divisible_by_four(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let instrument = crypto_perpetual_with_size_precision(instrument_eth_usdt, 2, "0.01");
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument.clone(), None, Some(cache), None, Some(config));
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1490.00"))
.quantity(Quantity::from("0.01"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("0.05"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
engine.process_bar(&bar);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<&OrderFilled> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.collect();
assert_eq!(fills.len(), 1);
assert_eq!(fills[0].client_order_id, client_order_id);
assert_eq!(fills[0].last_qty, Quantity::from("0.01"));
assert_eq!(fills[0].last_px, Price::from("1490.00"));
assert_eq!(
engine.get_core().last,
Some(Price::from("1505.00")),
"Bar should advance market state through to the close tick",
);
}
#[rstest]
fn test_process_trade_bar_with_units_less_than_four_does_not_overfill(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let instrument = crypto_perpetual_with_size_precision(instrument_eth_usdt, 0, "1");
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument.clone(), None, Some(cache), None, Some(config));
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1510.00"))
.quantity(Quantity::from("6"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("3"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
engine.process_bar(&bar);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<&OrderFilled> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.collect();
let total_qty = fills
.iter()
.map(|fill| fill.last_qty.as_decimal())
.sum::<rust_decimal::Decimal>();
assert_eq!(total_qty, dec!(3));
assert!(
fills
.iter()
.all(|fill| fill.client_order_id == client_order_id)
);
assert_eq!(
engine.get_core().last,
Some(Price::from("1505.00")),
"Low-volume bar should still advance market state through to the close tick",
);
}
#[rstest]
fn test_process_trade_bar_with_two_units_fills_orders_at_high_and_low(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let instrument = crypto_perpetual_with_size_precision(instrument_eth_usdt, 0, "1");
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument.clone(), None, Some(cache), None, Some(config));
let sell_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let buy_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut sell_limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Sell)
.price(Price::from("1510.00"))
.quantity(Quantity::from("1"))
.client_order_id(sell_client_order_id)
.submit(true)
.build();
let mut buy_limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1490.00"))
.quantity(Quantity::from("1"))
.client_order_id(buy_client_order_id)
.submit(true)
.build();
engine.process_order(&mut sell_limit_order, account_id);
engine.process_order(&mut buy_limit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("2"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
engine.process_bar(&bar);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<&OrderFilled> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.collect();
let sell_fill = fills
.iter()
.find(|fill| fill.client_order_id == sell_client_order_id)
.expect("expected sell order fill at bar high");
let buy_fill = fills
.iter()
.find(|fill| fill.client_order_id == buy_client_order_id)
.expect("expected buy order fill at bar low");
assert_eq!(fills.len(), 2);
assert_eq!(sell_fill.last_qty, Quantity::from("1"));
assert_eq!(sell_fill.last_px, Price::from("1510.00"));
assert_eq!(buy_fill.last_qty, Quantity::from("1"));
assert_eq!(buy_fill.last_px, Price::from("1490.00"));
}
#[rstest]
fn test_process_trade_bar_with_three_units_fills_order_at_high(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let instrument = crypto_perpetual_with_size_precision(instrument_eth_usdt, 0, "1");
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument.clone(), None, Some(cache), None, Some(config));
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Sell)
.price(Price::from("1508.00"))
.quantity(Quantity::from("1"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("3"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
engine.process_bar(&bar);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<&OrderFilled> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.collect();
assert_eq!(fills.len(), 1);
assert_eq!(fills[0].client_order_id, client_order_id);
assert_eq!(fills[0].last_qty, Quantity::from("1"));
assert_eq!(fills[0].last_px, Price::from("1508.00"));
}
#[rstest]
fn test_process_quote_bar_with_volume_not_divisible_by_four(instrument_eth_usdt: InstrumentAny) {
let instrument = crypto_perpetual_with_size_precision(instrument_eth_usdt, 2, "0.01");
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine = get_order_matching_engine(instrument, None, None, None, Some(config));
let ts = UnixNanos::from(1_000_000_000);
let bid_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-BID-EXTERNAL"),
open: Price::from("1499.00"),
high: Price::from("1500.00"),
low: Price::from("1498.00"),
close: Price::from("1499.50"),
volume: Quantity::from("0.07"),
ts_event: ts,
ts_init: ts,
};
let ask_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-ASK-EXTERNAL"),
open: Price::from("1500.00"),
high: Price::from("1501.00"),
low: Price::from("1499.00"),
close: Price::from("1500.50"),
volume: Quantity::from("0.09"),
ts_event: ts,
ts_init: ts,
};
engine.process_bar(&bid_bar);
engine.process_bar(&ask_bar);
}
#[rstest]
fn test_process_quote_bar_with_one_unit_updates_close(instrument_eth_usdt: InstrumentAny) {
let instrument = crypto_perpetual_with_size_precision(instrument_eth_usdt, 0, "1");
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine = get_order_matching_engine(instrument, None, None, None, Some(config));
let ts = UnixNanos::from(1_000_000_000);
let bid_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-BID-EXTERNAL"),
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("1"),
ts_event: ts,
ts_init: ts,
};
let ask_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-ASK-EXTERNAL"),
open: Price::from("1501.00"),
high: Price::from("1511.00"),
low: Price::from("1491.00"),
close: Price::from("1506.00"),
volume: Quantity::from("1"),
ts_event: ts,
ts_init: ts,
};
engine.process_bar(&bid_bar);
engine.process_bar(&ask_bar);
assert_eq!(engine.get_core().bid, Some(Price::from("1505.00")));
assert_eq!(engine.get_core().ask, Some(Price::from("1506.00")));
}
#[rstest]
fn test_process_quote_bar_with_asymmetric_units_updates_positive_sides(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let instrument = crypto_perpetual_with_size_precision(instrument_eth_usdt, 0, "1");
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument.clone(), None, Some(cache), None, Some(config));
let sell_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let buy_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut sell_limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Sell)
.price(Price::from("1510.00"))
.quantity(Quantity::from("1"))
.client_order_id(sell_client_order_id)
.submit(true)
.build();
let mut buy_limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1506.00"))
.quantity(Quantity::from("1"))
.client_order_id(buy_client_order_id)
.submit(true)
.build();
engine.process_order(&mut sell_limit_order, account_id);
engine.process_order(&mut buy_limit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let ts = UnixNanos::from(1_000_000_000);
let bid_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-BID-EXTERNAL"),
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("3"),
ts_event: ts,
ts_init: ts,
};
let ask_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-ASK-EXTERNAL"),
open: Price::from("1501.00"),
high: Price::from("1511.00"),
low: Price::from("1491.00"),
close: Price::from("1506.00"),
volume: Quantity::from("1"),
ts_event: ts,
ts_init: ts,
};
engine.process_bar(&bid_bar);
engine.process_bar(&ask_bar);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<&OrderFilled> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.collect();
let sell_fill = fills
.iter()
.find(|fill| fill.client_order_id == sell_client_order_id)
.expect("expected sell order fill at bar high bid");
let buy_fill = fills
.iter()
.find(|fill| fill.client_order_id == buy_client_order_id)
.expect("expected buy order fill at bar close ask");
assert_eq!(fills.len(), 2);
assert_eq!(sell_fill.last_qty, Quantity::from("1"));
assert_eq!(sell_fill.last_px, Price::from("1510.00"));
assert_eq!(buy_fill.last_qty, Quantity::from("1"));
assert_eq!(buy_fill.last_px, Price::from("1506.00"));
assert_eq!(engine.get_core().bid, Some(Price::from("1490.00")));
assert_eq!(engine.get_core().ask, Some(Price::from("1506.00")));
}
#[rstest]
fn test_process_quote_bar_with_zero_ask_volume_clears_previous_ask(
instrument_eth_usdt: InstrumentAny,
) {
let instrument = crypto_perpetual_with_size_precision(instrument_eth_usdt, 0, "1");
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine = get_order_matching_engine(instrument, None, None, None, Some(config));
let first_ts = UnixNanos::from(1_000_000_000);
let first_bid_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-BID-EXTERNAL"),
open: Price::from("1498.00"),
high: Price::from("1500.00"),
low: Price::from("1497.00"),
close: Price::from("1499.00"),
volume: Quantity::from("1"),
ts_event: first_ts,
ts_init: first_ts,
};
let first_ask_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-ASK-EXTERNAL"),
open: Price::from("1499.00"),
high: Price::from("1501.00"),
low: Price::from("1498.00"),
close: Price::from("1500.00"),
volume: Quantity::from("1"),
ts_event: first_ts,
ts_init: first_ts,
};
engine.process_bar(&first_bid_bar);
engine.process_bar(&first_ask_bar);
assert_eq!(engine.best_bid_price(), Some(Price::from("1499.00")));
assert_eq!(engine.best_ask_price(), Some(Price::from("1500.00")));
let second_ts = UnixNanos::from(2_000_000_000);
let second_bid_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-BID-EXTERNAL"),
open: Price::from("1600.00"),
high: Price::from("1610.00"),
low: Price::from("1590.00"),
close: Price::from("1605.00"),
volume: Quantity::from("3"),
ts_event: second_ts,
ts_init: second_ts,
};
let second_ask_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-ASK-EXTERNAL"),
open: Price::from("1601.00"),
high: Price::from("1611.00"),
low: Price::from("1591.00"),
close: Price::from("1606.00"),
volume: Quantity::from("0"),
ts_event: second_ts,
ts_init: second_ts,
};
engine.process_bar(&second_bid_bar);
engine.process_bar(&second_ask_bar);
assert_eq!(engine.best_bid_price(), Some(Price::from("1590.00")));
assert_eq!(engine.best_ask_price(), None);
assert_eq!(engine.get_core().bid, Some(Price::from("1590.00")));
assert_eq!(engine.get_core().ask, None);
}
#[rstest]
fn test_process_quote_bar_with_zero_close_side_does_not_cross_book(
instrument_eth_usdt: InstrumentAny,
) {
let instrument = crypto_perpetual_with_size_precision(instrument_eth_usdt, 0, "1");
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine = get_order_matching_engine(instrument, None, None, None, Some(config));
let ts = UnixNanos::from(1_000_000_000);
let bid_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-BID-EXTERNAL"),
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("8"),
ts_event: ts,
ts_init: ts,
};
let ask_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-ASK-EXTERNAL"),
open: Price::from("1501.00"),
high: Price::from("1511.00"),
low: Price::from("1491.00"),
close: Price::from("1506.00"),
volume: Quantity::from("3"),
ts_event: ts,
ts_init: ts,
};
engine.process_bar(&bid_bar);
engine.process_bar(&ask_bar);
assert_eq!(engine.best_bid_price(), Some(Price::from("1505.00")));
assert_eq!(engine.best_ask_price(), None);
assert_eq!(engine.get_core().bid, Some(Price::from("1505.00")));
assert_eq!(engine.get_core().ask, None);
}
#[rstest]
fn test_process_quote_bar_with_two_units_fills_orders_at_high_and_low(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let instrument = crypto_perpetual_with_size_precision(instrument_eth_usdt, 0, "1");
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument.clone(), None, Some(cache), None, Some(config));
let sell_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let buy_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut sell_limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Sell)
.price(Price::from("1510.00"))
.quantity(Quantity::from("1"))
.client_order_id(sell_client_order_id)
.submit(true)
.build();
let mut buy_limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1491.00"))
.quantity(Quantity::from("1"))
.client_order_id(buy_client_order_id)
.submit(true)
.build();
engine.process_order(&mut sell_limit_order, account_id);
engine.process_order(&mut buy_limit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let ts = UnixNanos::from(1_000_000_000);
let bid_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-BID-EXTERNAL"),
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1505.00"),
volume: Quantity::from("2"),
ts_event: ts,
ts_init: ts,
};
let ask_bar = Bar {
bar_type: BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-ASK-EXTERNAL"),
open: Price::from("1501.00"),
high: Price::from("1511.00"),
low: Price::from("1491.00"),
close: Price::from("1506.00"),
volume: Quantity::from("2"),
ts_event: ts,
ts_init: ts,
};
engine.process_bar(&bid_bar);
engine.process_bar(&ask_bar);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<&OrderFilled> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.collect();
let sell_fill = fills
.iter()
.find(|fill| fill.client_order_id == sell_client_order_id)
.expect("expected sell order fill at bar high bid");
let buy_fill = fills
.iter()
.find(|fill| fill.client_order_id == buy_client_order_id)
.expect("expected buy order fill at bar low ask");
assert_eq!(fills.len(), 2);
assert_eq!(sell_fill.last_qty, Quantity::from("1"));
assert_eq!(sell_fill.last_px, Price::from("1510.00"));
assert_eq!(buy_fill.last_qty, Quantity::from("1"));
assert_eq!(buy_fill.last_px, Price::from("1491.00"));
assert_eq!(engine.get_core().bid, Some(Price::from("1490.00")));
assert_eq!(engine.get_core().ask, Some(Price::from("1491.00")));
}
#[rstest]
fn test_modify_partially_filled_order_quantity_below_filled_rejected(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("0.500"), 1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1500.00")) .quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2);
let event1 = saved_messages.first().unwrap();
assert!(
matches!(event1, OrderEventAny::Accepted(_)),
"Expected OrderAccepted"
);
let event2 = saved_messages.get(1).unwrap();
let fill = match event2 {
OrderEventAny::Filled(filled) => filled,
_ => panic!("Expected OrderFilled event"),
};
assert_eq!(fill.last_qty, Quantity::from("0.500"));
clear_order_event_handler_messages(&order_event_handler);
let modify_order_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
Some(Quantity::from("0.400")), None,
None,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_order_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 1);
let event = saved_messages.first().unwrap();
let rejected = match event {
OrderEventAny::ModifyRejected(rejected) => rejected,
_ => panic!("Expected OrderModifyRejected event, was {event:?}"),
};
assert_eq!(rejected.client_order_id, client_order_id);
assert!(rejected.reason.contains("below filled quantity"));
}
#[rstest]
#[case("0.600", None, Some("0.400"))]
#[case("1.000", None, None)]
#[case("0.600", Some("0.400"), None)]
#[case("0.600", Some("0.600"), None)]
fn test_ouo_sibling_adjusted_after_resolving_order_fill(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
#[case] fill_qty: &str,
#[case] sibling_filled_qty: Option<&str>,
#[case] expected_sibling_quantity: Option<&str>,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let engine_config = OrderMatchingEngineConfig {
support_contingent_orders: true,
..Default::default()
};
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
Some(engine_config),
);
let client_order_id_resolving = ClientOrderId::from("O-19700101-000000-001-001-1");
let client_order_id_sibling = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut resolving_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1500.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_resolving)
.contingency_type(ContingencyType::Ouo)
.linked_order_ids(vec![client_order_id_sibling])
.submit(true)
.build();
let mut sibling_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1495.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_sibling)
.contingency_type(ContingencyType::Ouo)
.linked_order_ids(vec![client_order_id_resolving])
.submit(true)
.build();
cache
.borrow_mut()
.add_order(resolving_order.clone(), None, None, false)
.unwrap();
cache
.borrow_mut()
.add_order(sibling_order.clone(), None, None, false)
.unwrap();
engine_l2.process_order(&mut resolving_order, account_id);
engine_l2.process_order(&mut sibling_order, account_id);
if let Some(sibling_filled_qty) = sibling_filled_qty {
let sibling_order = cache
.borrow()
.order(&client_order_id_sibling)
.map(|order| order.clone())
.unwrap();
let sibling_filled = build_order_filled(
sibling_order.trader_id(),
sibling_order.strategy_id(),
sibling_order.instrument_id(),
sibling_order.client_order_id(),
sibling_order.venue_order_id().unwrap(),
sibling_order.account_id().unwrap(),
TradeId::from("T-SIBLING-001"),
sibling_order.order_side(),
sibling_order.order_type(),
Quantity::from(sibling_filled_qty),
sibling_order.price().unwrap(),
instrument_eth_usdt.quote_currency(),
LiquiditySide::Maker,
None,
None,
);
cache
.borrow_mut()
.update_order(&OrderEventAny::Filled(sibling_filled))
.unwrap();
}
clear_order_event_handler_messages(&order_event_handler);
let fill_qty = Quantity::from(fill_qty);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
fill_qty,
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert_eq!(saved_messages.len(), 2, "events: {saved_messages:?}");
let fill = match saved_messages.first().unwrap() {
OrderEventAny::Filled(filled) => filled,
event => panic!("Expected OrderFilled event, was {event:?}"),
};
assert_eq!(fill.client_order_id, client_order_id_resolving);
assert_eq!(fill.last_qty, fill_qty);
match expected_sibling_quantity {
Some(quantity) => {
let updated = match saved_messages.get(1).unwrap() {
OrderEventAny::Updated(updated) => updated,
event => panic!("Expected OrderUpdated event, was {event:?}"),
};
assert_eq!(updated.client_order_id, client_order_id_sibling);
assert_eq!(updated.quantity, Quantity::from(quantity));
}
None => {
let canceled = match saved_messages.get(1).unwrap() {
OrderEventAny::Canceled(canceled) => canceled,
event => panic!("Expected OrderCanceled event, was {event:?}"),
};
assert_eq!(canceled.client_order_id, client_order_id_sibling);
}
}
}
#[rstest]
#[case("0.600", None, None, true)]
#[case("1.000", Some("1495.00"), Some("0.400"), false)]
fn test_ouo_child_cancelled_after_parent_modify(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
#[case] modify_quantity: &str,
#[case] modify_price: Option<&str>,
#[case] contingent_filled_qty: Option<&str>,
#[case] expect_primary_cancel: bool,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let engine_config = OrderMatchingEngineConfig {
support_contingent_orders: true,
..Default::default()
};
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
Some(engine_config),
);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("0.600"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let orderbook_delta_buy = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1400.00"),
Quantity::from("1.000"),
2,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_buy)
.unwrap();
let client_order_id_primary = ClientOrderId::from("O-19700101-000000-001-001-1");
let client_order_id_contingent = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut primary_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1500.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_primary)
.contingency_type(ContingencyType::Ouo)
.linked_order_ids(vec![client_order_id_contingent])
.submit(true)
.build();
let mut contingent_stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("1380.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id_contingent)
.linked_order_ids(vec![client_order_id_primary])
.contingency_type(ContingencyType::Ouo)
.submit(true)
.build();
cache
.borrow_mut()
.add_order(primary_order.clone(), None, None, false)
.unwrap();
cache
.borrow_mut()
.add_order(contingent_stop_order.clone(), None, None, false)
.unwrap();
engine_l2.process_order(&mut primary_order, account_id);
engine_l2.process_order(&mut contingent_stop_order, account_id);
if let Some(contingent_filled_qty) = contingent_filled_qty {
let contingent_stop_order = cache
.borrow()
.order(&client_order_id_contingent)
.map(|order| order.clone())
.unwrap();
let contingent_filled = build_order_filled(
contingent_stop_order.trader_id(),
contingent_stop_order.strategy_id(),
contingent_stop_order.instrument_id(),
contingent_stop_order.client_order_id(),
contingent_stop_order.venue_order_id().unwrap(),
contingent_stop_order.account_id().unwrap(),
TradeId::from("T-CONTINGENT-001"),
contingent_stop_order.order_side(),
contingent_stop_order.order_type(),
Quantity::from(contingent_filled_qty),
contingent_stop_order.trigger_price().unwrap(),
instrument_eth_usdt.quote_currency(),
LiquiditySide::Maker,
None,
None,
);
cache
.borrow_mut()
.update_order(&OrderEventAny::Filled(contingent_filled))
.unwrap();
}
clear_order_event_handler_messages(&order_event_handler);
let modify_order_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id_primary,
Some(VenueOrderId::from("V1")),
Some(Quantity::from(modify_quantity)),
modify_price.map(Price::from),
None,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_order_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let expected_message_count = if expect_primary_cancel { 3 } else { 2 };
assert_eq!(
saved_messages.len(),
expected_message_count,
"events: {saved_messages:?}",
);
let event1 = saved_messages.first().unwrap();
let updated_primary = match event1 {
OrderEventAny::Updated(updated) => updated,
_ => panic!("Expected OrderUpdated event for primary"),
};
assert_eq!(updated_primary.client_order_id, client_order_id_primary);
assert_eq!(updated_primary.quantity, Quantity::from(modify_quantity));
let event2 = saved_messages.get(1).unwrap();
let cancelled_child = match event2 {
OrderEventAny::Canceled(cancelled) => cancelled,
_ => panic!("Expected OrderCanceled event for contingent, was {event2:?}"),
};
assert_eq!(cancelled_child.client_order_id, client_order_id_contingent);
if expect_primary_cancel {
let event3 = saved_messages.get(2).unwrap();
let cancelled_primary = match event3 {
OrderEventAny::Canceled(cancelled) => cancelled,
_ => panic!("Expected OrderCanceled event for primary, was {event3:?}"),
};
assert_eq!(cancelled_primary.client_order_id, client_order_id_primary);
}
}
#[rstest]
#[case(OrderSide::Buy)]
#[case(OrderSide::Sell)]
fn test_liquidity_consumption_tracks_fills_at_price_level(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
) {
let config = OrderMatchingEngineConfig {
liquidity_consumption: true,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta_buy = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("900.00"),
Quantity::from("100.000"),
100,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_buy)
.unwrap();
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("100.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let mut order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("30.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine_l2.process_order(&mut order1, account_id);
let mut order2 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("50.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build();
engine_l2.process_order(&mut order2, account_id);
let mut order3 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("50.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-3"))
.submit(true)
.build();
engine_l2.process_order(&mut order3, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let filled_events: Vec<_> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.collect();
assert_eq!(filled_events.len(), 3);
assert_eq!(filled_events[0].last_qty, Quantity::from("30.000"));
assert_eq!(filled_events[1].last_qty, Quantity::from("50.000"));
assert_eq!(filled_events[2].last_qty, Quantity::from("20.000"));
}
#[rstest]
#[case(OrderSide::Buy, OrderSide::Sell)]
#[case(OrderSide::Sell, OrderSide::Buy)]
fn test_liquidity_consumption_resets_on_fresh_data(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
#[case] book_side: OrderSide,
) {
let config = OrderMatchingEngineConfig {
liquidity_consumption: true,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let liquidity_price = if book_side == OrderSide::Sell {
Price::from("1000.00")
} else {
Price::from("900.00")
};
let opposite_price = if book_side == OrderSide::Sell {
Price::from("900.00")
} else {
Price::from("1000.00")
};
let orderbook_delta_opposite = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
if book_side == OrderSide::Sell {
OrderSide::Buy
} else {
OrderSide::Sell
},
opposite_price,
Quantity::from("100.000"),
100,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_opposite)
.unwrap();
let orderbook_delta_target = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
book_side,
liquidity_price,
Quantity::from("50.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_target)
.unwrap();
let mut order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("50.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine_l2.process_order(&mut order1, account_id);
let orderbook_delta_update = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Update)
.book_order(BookOrder::new(
book_side,
liquidity_price,
Quantity::from("80.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_update)
.unwrap();
let mut order2 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("60.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build();
engine_l2.process_order(&mut order2, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let filled_events: Vec<_> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.collect();
assert_eq!(filled_events.len(), 2);
assert_eq!(filled_events[0].last_qty, Quantity::from("50.000"));
assert_eq!(filled_events[1].last_qty, Quantity::from("60.000"));
}
#[rstest]
#[case(OrderSide::Buy)]
#[case(OrderSide::Sell)]
fn test_liquidity_consumption_off_allows_repeated_fills(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
) {
let config = OrderMatchingEngineConfig {
liquidity_consumption: false,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta_buy = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("900.00"),
Quantity::from("50.000"),
100,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_buy)
.unwrap();
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("50.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let mut order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("50.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine_l2.process_order(&mut order1, account_id);
let mut order2 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("50.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build();
engine_l2.process_order(&mut order2, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let filled_events: Vec<_> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.collect();
assert_eq!(filled_events.len(), 2);
assert_eq!(filled_events[0].last_qty, Quantity::from("50.000"));
assert_eq!(filled_events[1].last_qty, Quantity::from("50.000"));
}
#[rstest]
#[case(OrderSide::Buy, OrderSide::Sell)]
#[case(OrderSide::Sell, OrderSide::Buy)]
fn test_liquidity_consumption_unchanged_levels_stay_depleted(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
#[case] book_side: OrderSide,
) {
let config = OrderMatchingEngineConfig {
liquidity_consumption: true,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let (prices, opposite_price, best_price) = if order_side == OrderSide::Buy {
(
[
Price::from("1000.00"),
Price::from("1001.00"),
Price::from("1002.00"),
],
Price::from("900.00"),
Price::from("1000.00"),
)
} else {
(
[
Price::from("1002.00"),
Price::from("1001.00"),
Price::from("1000.00"),
],
Price::from("1100.00"),
Price::from("1002.00"),
)
};
let opposite_side_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
order_side,
opposite_price,
Quantity::from("1000.000"),
100,
))
.build();
engine_l2
.process_order_book_delta(&opposite_side_delta)
.unwrap();
let delta1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
book_side,
prices[0],
Quantity::from("200.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&delta1).unwrap();
let delta2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
book_side,
prices[1],
Quantity::from("812.000"),
2,
))
.build();
engine_l2.process_order_book_delta(&delta2).unwrap();
let delta3 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
book_side,
prices[2],
Quantity::from("1012.000"),
3,
))
.build();
engine_l2.process_order_book_delta(&delta3).unwrap();
let mut order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("2024.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine_l2.process_order(&mut order1, account_id);
let delete_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Delete)
.book_order(BookOrder::new(
book_side,
best_price,
Quantity::from("0.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&delete_delta).unwrap();
let mut order2 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("100.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build();
engine_l2.process_order(&mut order2, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let filled_events: Vec<_> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.collect();
let order1_fills: Vec<_> = filled_events
.iter()
.filter(|f| f.client_order_id.as_str().ends_with("-1"))
.collect();
let order2_fill_count = filled_events
.iter()
.filter(|f| f.client_order_id.as_str().ends_with("-2"))
.count();
assert_eq!(
order1_fills.len(),
3,
"First order should fill at all 3 levels"
);
let total_fill_qty: f64 = order1_fills.iter().map(|f| f.last_qty.as_f64()).sum();
assert!(
(total_fill_qty - 2024.0).abs() < 0.001,
"First order should consume total 2024 units"
);
assert_eq!(
order2_fill_count, 0,
"Second order should not fill - unchanged levels should stay depleted"
);
}
#[rstest]
fn test_stop_limit_triggered_not_filled_single_accept(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
reject_stop_orders: false,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_order = OrderTestBuilder::new(OrderType::StopLimit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1495.00"))
.price(Price::from("1490.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut stop_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let accepted_count = saved_messages
.iter()
.filter(|e| matches!(e, OrderEventAny::Accepted(_)))
.count();
assert_eq!(
accepted_count, 1,
"Expected exactly 1 Accepted event, was {accepted_count} (double-accept bug)"
);
assert_eq!(
saved_messages.len(),
2,
"Expected Accepted + Triggered events"
);
let orders = engine_l2.get_core().get_orders();
let matching_order_count = orders
.iter()
.filter(|o| o.client_order_id == client_order_id)
.count();
assert_eq!(
matching_order_count, 1,
"Order should exist exactly once in core"
);
}
#[rstest]
fn test_modify_limit_order_price_persists_to_core(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, Some(cache), None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1490.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit_order, account_id);
let order_in_core = engine_l2.get_core().get_order(client_order_id).unwrap();
assert_eq!(order_in_core.limit_price, Some(Price::from("1490.00")));
clear_order_event_handler_messages(&order_event_handler);
let modify_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
Some(Quantity::from("1.000")),
Some(Price::from("1495.00")),
None,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_command, account_id);
let order_in_core = engine_l2.get_core().get_order(client_order_id).unwrap();
assert_eq!(
order_in_core.limit_price,
Some(Price::from("1495.00")),
"Modified price should persist to core"
);
}
#[rstest]
fn test_rejected_modify_does_not_change_book_priority(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1490.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit_order, account_id);
let initial_order_count = engine_l2.get_core().get_orders().len();
clear_order_event_handler_messages(&order_event_handler);
let modify_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
Some(Quantity::from("1.000")),
Some(Price::from("1495.12345")), None,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_command, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let rejected_count = saved_messages
.iter()
.filter(|e| matches!(e, OrderEventAny::ModifyRejected(_)))
.count();
assert_eq!(rejected_count, 1, "Modify should be rejected");
let order_in_core = engine_l2.get_core().get_order(client_order_id).unwrap();
assert_eq!(
order_in_core.limit_price,
Some(Price::from("1490.00")),
"Order price should be unchanged after rejected modify"
);
let final_order_count = engine_l2.get_core().get_orders().len();
assert_eq!(
initial_order_count, final_order_count,
"Order count should be unchanged"
);
}
#[rstest]
fn test_netting_mode_fills_have_no_position_id(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2
.process_order_book_delta(&orderbook_delta_sell)
.unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill_event = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.expect("Expected a fill event");
assert!(
fill_event.position_id.is_none(),
"Netting mode fills should have position_id = None"
);
}
#[rstest]
fn test_market_if_touched_buy_fills_at_trigger_price(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let bar_type = BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL");
let init_bar = Bar {
bar_type,
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1500.00"),
volume: Quantity::from("100.000"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
engine.process_bar(&init_bar);
clear_order_event_handler_messages(&order_event_handler);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut mit_order = OrderTestBuilder::new(OrderType::MarketIfTouched)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1480.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut mit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let _trigger_bar = Bar {
bar_type,
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1470.00"),
close: Price::from("1490.00"),
volume: Quantity::from("100.000"),
ts_event: UnixNanos::from(2_000_000_000),
ts_init: UnixNanos::from(2_000_000_000),
};
engine.set_fill_at_market(false);
engine.trigger_stop_order(client_order_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill_event = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.expect("Expected a fill event");
assert_eq!(
fill_event.last_px,
Price::from("1480.00"),
"BUY MIT should fill at trigger price 1480.00, not bar low 1470.00"
);
}
#[rstest]
fn test_market_if_touched_sell_fills_at_trigger_price(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let bar_type = BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL");
let init_bar = Bar {
bar_type,
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1500.00"),
volume: Quantity::from("100.000"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
engine.process_bar(&init_bar);
clear_order_event_handler_messages(&order_event_handler);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut mit_order = OrderTestBuilder::new(OrderType::MarketIfTouched)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("1520.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut mit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let _trigger_bar = Bar {
bar_type,
open: Price::from("1500.00"),
high: Price::from("1530.00"),
low: Price::from("1490.00"),
close: Price::from("1510.00"),
volume: Quantity::from("100.000"),
ts_event: UnixNanos::from(2_000_000_000),
ts_init: UnixNanos::from(2_000_000_000),
};
engine.set_fill_at_market(false);
engine.trigger_stop_order(client_order_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill_event = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.expect("Expected a fill event");
assert_eq!(
fill_event.last_px,
Price::from("1520.00"),
"SELL MIT should fill at trigger price 1520.00, not bar high 1530.00"
);
}
#[rstest]
fn test_market_if_touched_buy_fills_at_trigger_price_with_liquidity_consumption(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
bar_execution: true,
liquidity_consumption: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let bar_type = BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL");
let init_bar = Bar {
bar_type,
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1500.00"),
volume: Quantity::from("100.000"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
engine.process_bar(&init_bar);
clear_order_event_handler_messages(&order_event_handler);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut mit_order = OrderTestBuilder::new(OrderType::MarketIfTouched)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1480.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut mit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
engine.set_fill_at_market(false);
engine.trigger_stop_order(client_order_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill_event = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.expect("Expected a fill event");
assert_eq!(
fill_event.last_px,
Price::from("1480.00"),
"BUY MIT should fill at trigger price with liquidity consumption enabled"
);
}
#[rstest]
fn test_market_if_touched_sell_fills_at_trigger_price_with_liquidity_consumption(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
bar_execution: true,
liquidity_consumption: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let bar_type = BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL");
let init_bar = Bar {
bar_type,
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1490.00"),
close: Price::from("1500.00"),
volume: Quantity::from("100.000"),
ts_event: UnixNanos::from(1_000_000_000),
ts_init: UnixNanos::from(1_000_000_000),
};
engine.process_bar(&init_bar);
clear_order_event_handler_messages(&order_event_handler);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut mit_order = OrderTestBuilder::new(OrderType::MarketIfTouched)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("1520.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut mit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
engine.set_fill_at_market(false);
engine.trigger_stop_order(client_order_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill_event = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.expect("Expected a fill event");
assert_eq!(
fill_event.last_px,
Price::from("1520.00"),
"SELL MIT should fill at trigger price with liquidity consumption enabled"
);
}
#[rstest]
#[case(OrderSide::Buy)]
#[case(OrderSide::Sell)]
fn test_liquidity_consumption_tracks_fills_at_multiple_price_levels(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
) {
let config = OrderMatchingEngineConfig {
liquidity_consumption: true,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
if order_side == OrderSide::Buy {
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("900.00"),
Quantity::from("1000.000"),
100,
))
.build();
engine_l2.process_order_book_delta(&bid_delta).unwrap();
let ask1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("999.00"),
Quantity::from("50.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&ask1).unwrap();
let ask2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("50.000"),
2,
))
.build();
engine_l2.process_order_book_delta(&ask2).unwrap();
let ask3 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1001.00"),
Quantity::from("100.000"),
3,
))
.build();
engine_l2.process_order_book_delta(&ask3).unwrap();
} else {
let ask_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1100.00"),
Quantity::from("1000.000"),
100,
))
.build();
engine_l2.process_order_book_delta(&ask_delta).unwrap();
let bid1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1001.00"),
Quantity::from("50.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&bid1).unwrap();
let bid2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1000.00"),
Quantity::from("50.000"),
2,
))
.build();
engine_l2.process_order_book_delta(&bid2).unwrap();
let bid3 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("999.00"),
Quantity::from("100.000"),
3,
))
.build();
engine_l2.process_order_book_delta(&bid3).unwrap();
}
let limit_price = Price::from("1000.00");
let mut order1 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.price(limit_price)
.quantity(Quantity::from("100.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine_l2.process_order(&mut order1, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let order1_fills: Vec<_> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) if fill.client_order_id.as_str().ends_with("-1") => {
Some(fill)
}
_ => None,
})
.collect();
let total_order1: f64 = order1_fills.iter().map(|f| f.last_qty.as_f64()).sum();
assert!(
(total_order1 - 100.0).abs() < 0.001,
"First order should fill 100, was {total_order1}"
);
clear_order_event_handler_messages(&order_event_handler);
let mut order2 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.price(limit_price)
.quantity(Quantity::from("50.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build();
engine_l2.process_order(&mut order2, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let order2_fills: Vec<_> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) if fill.client_order_id.as_str().ends_with("-2") => {
Some(fill)
}
_ => None,
})
.collect();
let total_order2: f64 = order2_fills.iter().map(|f| f.last_qty.as_f64()).sum();
assert!(
total_order2 < 0.001,
"Second order should NOT fill - both price levels should be consumed. \
Got fill of {total_order2}. If this fails, consumption was incorrectly \
tracked at wrong price levels."
);
}
#[rstest]
#[case(OrderSide::Buy)]
#[case(OrderSide::Sell)]
fn test_fok_order_canceled_when_liquidity_consumption_exhausts_fills(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
) {
let config = OrderMatchingEngineConfig {
liquidity_consumption: true,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
if order_side == OrderSide::Buy {
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("900.00"),
Quantity::from("1000.000"),
100,
))
.build();
engine_l2.process_order_book_delta(&bid_delta).unwrap();
let ask_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("50.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&ask_delta).unwrap();
} else {
let ask_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1100.00"),
Quantity::from("1000.000"),
100,
))
.build();
engine_l2.process_order_book_delta(&ask_delta).unwrap();
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1000.00"),
Quantity::from("50.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&bid_delta).unwrap();
}
let mut order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("50.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine_l2.process_order(&mut order1, account_id);
clear_order_event_handler_messages(&order_event_handler);
let limit_price = Price::from("1000.00");
let mut fok_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.price(limit_price)
.quantity(Quantity::from("30.000"))
.time_in_force(TimeInForce::Fok)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build();
engine_l2.process_order(&mut fok_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let canceled_count = saved_messages
.iter()
.filter(|event| matches!(event, OrderEventAny::Canceled(_)))
.count();
assert_eq!(
canceled_count, 1,
"FOK order should be canceled when liquidity is exhausted"
);
}
#[rstest]
#[case(OrderSide::Buy)]
#[case(OrderSide::Sell)]
fn test_ioc_order_canceled_when_liquidity_consumption_exhausts_fills(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
) {
let config = OrderMatchingEngineConfig {
liquidity_consumption: true,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
if order_side == OrderSide::Buy {
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("900.00"),
Quantity::from("1000.000"),
100,
))
.build();
engine_l2.process_order_book_delta(&bid_delta).unwrap();
let ask_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("50.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&ask_delta).unwrap();
} else {
let ask_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1100.00"),
Quantity::from("1000.000"),
100,
))
.build();
engine_l2.process_order_book_delta(&ask_delta).unwrap();
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1000.00"),
Quantity::from("50.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&bid_delta).unwrap();
}
let mut order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("50.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine_l2.process_order(&mut order1, account_id);
clear_order_event_handler_messages(&order_event_handler);
let limit_price = Price::from("1000.00");
let mut ioc_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.price(limit_price)
.quantity(Quantity::from("30.000"))
.time_in_force(TimeInForce::Ioc)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build();
engine_l2.process_order(&mut ioc_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let canceled_count = saved_messages
.iter()
.filter(|event| matches!(event, OrderEventAny::Canceled(_)))
.count();
assert_eq!(
canceled_count, 1,
"IOC order should be canceled when liquidity is exhausted"
);
}
#[rstest]
#[case(OrderSide::Buy)]
#[case(OrderSide::Sell)]
fn test_gtc_order_not_canceled_when_liquidity_consumption_exhausts_fills(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
) {
let config = OrderMatchingEngineConfig {
liquidity_consumption: true,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
if order_side == OrderSide::Buy {
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("900.00"),
Quantity::from("1000.000"),
100,
))
.build();
engine_l2.process_order_book_delta(&bid_delta).unwrap();
let ask_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("50.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&ask_delta).unwrap();
} else {
let ask_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1100.00"),
Quantity::from("1000.000"),
100,
))
.build();
engine_l2.process_order_book_delta(&ask_delta).unwrap();
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1000.00"),
Quantity::from("50.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&bid_delta).unwrap();
}
let mut order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("50.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine_l2.process_order(&mut order1, account_id);
clear_order_event_handler_messages(&order_event_handler);
let limit_price = Price::from("1000.00");
let mut gtc_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.price(limit_price)
.quantity(Quantity::from("30.000"))
.time_in_force(TimeInForce::Gtc)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build();
engine_l2.process_order(&mut gtc_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let canceled_count = saved_messages
.iter()
.filter(|event| matches!(event, OrderEventAny::Canceled(_)))
.count();
let filled_count = saved_messages
.iter()
.filter(|event| matches!(event, OrderEventAny::Filled(_)))
.count();
assert_eq!(
canceled_count, 0,
"GTC order should NOT be canceled when liquidity is exhausted"
);
assert_eq!(
filled_count, 0,
"GTC order should NOT be filled when liquidity is exhausted"
);
}
#[rstest]
#[case(OrderSide::Buy, AggressorSide::Seller)]
#[case(OrderSide::Sell, AggressorSide::Buyer)]
fn test_trade_execution_fill_model_at_limit_with_prob_zero_does_not_fill(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
#[case] aggressor_side: AggressorSide,
) {
let fill_model = FillModelAny::Default(DefaultFillModel::new(0.0, 0.0, Some(42)).unwrap());
let config = OrderMatchingEngineConfig {
trade_execution: true,
..Default::default()
};
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = OrderMatchingEngine::new(
instrument_eth_usdt.clone(),
1,
fill_model.into(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
config,
);
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Price::from("1010.00"),
Quantity::from("100.000"),
Quantity::from("100.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
let limit_price = Price::from("1005.00");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.price(limit_price)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
limit_price,
Quantity::from("10.000"),
aggressor_side,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let filled_count = saved_messages
.iter()
.filter(|event| matches!(event, OrderEventAny::Filled(_)))
.count();
assert_eq!(
filled_count, 0,
"Order should NOT fill when trade at limit price with prob_fill_on_limit=0.0"
);
}
#[rstest]
#[case(OrderSide::Buy, AggressorSide::Seller)]
#[case(OrderSide::Sell, AggressorSide::Buyer)]
fn test_trade_execution_fill_model_at_limit_with_prob_one_fills(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
#[case] aggressor_side: AggressorSide,
) {
let fill_model = FillModelAny::Default(DefaultFillModel::new(1.0, 0.0, Some(42)).unwrap());
let config = OrderMatchingEngineConfig {
trade_execution: true,
..Default::default()
};
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = OrderMatchingEngine::new(
instrument_eth_usdt.clone(),
1,
fill_model.into(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
config,
);
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Price::from("1010.00"),
Quantity::from("100.000"),
Quantity::from("100.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
let limit_price = Price::from("1005.00");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.price(limit_price)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
limit_price,
Quantity::from("10.000"),
aggressor_side,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let filled_events: Vec<_> = saved_messages
.iter()
.filter(|event| matches!(event, OrderEventAny::Filled(_)))
.collect();
assert_eq!(
filled_events.len(),
1,
"Order should fill when trade at limit price with prob_fill_on_limit=1.0"
);
if let OrderEventAny::Filled(filled) = &filled_events[0] {
assert_eq!(filled.last_px, limit_price);
}
}
#[rstest]
#[case(OrderSide::Buy, AggressorSide::Seller, "1005.00", "1000.00")]
#[case(OrderSide::Sell, AggressorSide::Buyer, "1005.00", "1010.00")]
fn test_trade_execution_crossing_limit_fills_regardless_of_fill_model(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
#[case] aggressor_side: AggressorSide,
#[case] limit_price_str: &str,
#[case] trade_price_str: &str,
) {
let fill_model = FillModelAny::Default(DefaultFillModel::new(0.0, 0.0, Some(42)).unwrap());
let config = OrderMatchingEngineConfig {
trade_execution: true,
..Default::default()
};
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = OrderMatchingEngine::new(
instrument_eth_usdt.clone(),
1,
fill_model.into(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
config,
);
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("990.00"),
Price::from("1020.00"),
Quantity::from("100.000"),
Quantity::from("100.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
let limit_price = Price::from(limit_price_str);
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.price(limit_price)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade_price = Price::from(trade_price_str);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
trade_price,
Quantity::from("10.000"),
aggressor_side,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let filled_events: Vec<_> = saved_messages
.iter()
.filter(|event| matches!(event, OrderEventAny::Filled(_)))
.collect();
assert_eq!(
filled_events.len(),
1,
"Order should fill when trade crosses limit regardless of fill model"
);
if let OrderEventAny::Filled(filled) = &filled_events[0] {
assert_eq!(filled.last_px, limit_price);
}
}
#[rstest]
fn test_no_aggressor_trade_fills_resting_limit_at_trade_price(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
trade_execution: true,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("95.00"),
Quantity::from("10.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&bid_delta).unwrap();
let ask_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("105.00"),
Quantity::from("10.000"),
2,
))
.build();
engine_l2.process_order_book_delta(&ask_delta).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("100.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Quantity::from("1.000"),
AggressorSide::NoAggressor,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine_l2.process_trade_tick(&trade);
let fills: Vec<_> = get_order_event_handler_messages(&order_event_handler)
.iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(*f),
_ => None,
})
.collect();
assert_eq!(
fills.len(),
1,
"no-aggressor trade at the limit price must fill the resting BUY",
);
assert_eq!(fills[0].client_order_id, client_order_id);
}
#[rstest]
#[case(OrderSide::Buy, AggressorSide::Seller)]
#[case(OrderSide::Sell, AggressorSide::Buyer)]
fn test_trade_execution_fill_model_rejection_still_applies_liquidity_consumption(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
#[case] aggressor_side: AggressorSide,
) {
let fill_model = FillModelAny::Default(DefaultFillModel::new(0.0, 0.0, Some(42)).unwrap());
let config = OrderMatchingEngineConfig {
trade_execution: true,
liquidity_consumption: true,
..Default::default()
};
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = OrderMatchingEngine::new(
instrument_eth_usdt.clone(),
1,
fill_model.into(),
FeeModelAny::default().into(),
BookType::L2_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
config,
);
if order_side == OrderSide::Buy {
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("900.00"),
Quantity::from("1000.000"),
100,
))
.build();
engine.process_order_book_delta(&bid_delta).unwrap();
let ask_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1005.00"),
Quantity::from("50.000"),
1,
))
.build();
engine.process_order_book_delta(&ask_delta).unwrap();
} else {
let ask_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1100.00"),
Quantity::from("1000.000"),
100,
))
.build();
engine.process_order_book_delta(&ask_delta).unwrap();
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1015.00"),
Quantity::from("50.000"),
1,
))
.build();
engine.process_order_book_delta(&bid_delta).unwrap();
}
let mut order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.quantity(Quantity::from("50.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut order1, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let filled_count = saved_messages
.iter()
.filter(|event| matches!(event, OrderEventAny::Filled(_)))
.count();
assert_eq!(filled_count, 1);
clear_order_event_handler_messages(&order_event_handler);
let limit_price = Price::from("1010.00");
let mut order2 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.price(limit_price)
.quantity(Quantity::from("30.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build();
engine.process_order(&mut order2, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
limit_price,
Quantity::from("100.000"),
aggressor_side,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let filled_count = saved_messages
.iter()
.filter(|event| matches!(event, OrderEventAny::Filled(_)))
.count();
assert_eq!(
filled_count, 0,
"Order should NOT fill when fill model rejects at limit price"
);
}
#[rstest]
fn test_bar_execution_fills_stop_order(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1499.00"),
Price::from("1501.00"),
Quantity::from("1.000"),
Quantity::from("1.000"),
UnixNanos::from(1u64),
UnixNanos::from(1u64),
);
engine.process_quote_tick("e);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("1490.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut stop_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let bar_type = BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL");
let bar = Bar {
bar_type,
open: Price::from("1500.00"),
high: Price::from("1510.00"),
low: Price::from("1480.00"),
close: Price::from("1495.00"),
volume: Quantity::from("100.000"),
ts_event: UnixNanos::from(2u64),
ts_init: UnixNanos::from(2u64),
};
engine.process_bar(&bar);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("Expected stop order to fill during bar execution");
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_px, Price::from("1490.00"));
}
#[rstest]
fn test_bar_adaptive_ordering_fills_low_side_first(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
bar_execution: true,
bar_adaptive_high_low_ordering: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1500.00"),
Price::from("1500.00"),
Quantity::from("1.000"),
Quantity::from("1.000"),
UnixNanos::from(1u64),
UnixNanos::from(1u64),
);
engine.process_quote_tick("e);
let buy_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut buy_stop = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1510.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(buy_id)
.submit(true)
.build();
engine.process_order(&mut buy_stop, account_id);
let sell_id = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut sell_stop = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("1490.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(sell_id)
.submit(true)
.build();
engine.process_order(&mut sell_stop, account_id);
clear_order_event_handler_messages(&order_event_handler);
let bar_type = BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL");
let bar = Bar {
bar_type,
open: Price::from("1498.00"),
high: Price::from("1520.00"),
low: Price::from("1480.00"),
close: Price::from("1500.00"),
volume: Quantity::from("100.000"),
ts_event: UnixNanos::from(2u64),
ts_init: UnixNanos::from(2u64),
};
engine.process_bar(&bar);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<&OrderFilled> = saved_messages
.iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
assert_eq!(fills.len(), 2, "Both stops should fill");
assert_eq!(
fills[0].client_order_id, sell_id,
"SELL should fill first (low processed before high)"
);
assert_eq!(fills[1].client_order_id, buy_id, "BUY should fill second");
}
#[ignore]
#[rstest]
fn test_trailing_stop_market_updated_then_triggered(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("10.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&delta).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut trailing_stop = OrderTestBuilder::new(OrderType::TrailingStopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.trigger_price(Price::from("1510.00"))
.trigger_type(TriggerType::LastPrice)
.trailing_offset(dec!(5))
.trailing_offset_type(TrailingOffsetType::Price)
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut trailing_stop, account_id);
let tick1 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1480.00"),
Quantity::from("1.000"),
AggressorSide::Seller,
TradeId::new("1"),
UnixNanos::from(1u64),
UnixNanos::from(1u64),
);
engine_l2.process_trade_tick(&tick1);
let tick2 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1490.00"),
Quantity::from("1.000"),
AggressorSide::Buyer,
TradeId::new("2"),
UnixNanos::from(2u64),
UnixNanos::from(2u64),
);
engine_l2.process_trade_tick(&tick2);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let accepted_count = saved_messages
.iter()
.filter(|e| matches!(e, OrderEventAny::Accepted(_)))
.count();
let updated_count = saved_messages
.iter()
.filter(|e| matches!(e, OrderEventAny::Updated(_)))
.count();
let fill = saved_messages.iter().find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
});
assert_eq!(accepted_count, 1, "Should have 1 accepted event");
assert!(updated_count >= 1, "Should have at least 1 trailing update");
assert!(
fill.is_some(),
"Trailing stop should have triggered and filled"
);
assert_eq!(fill.unwrap().client_order_id, client_order_id);
}
#[rstest]
fn test_gtd_order_partially_filled_then_expired(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
support_gtd_orders: true,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("0.500"),
1,
))
.build();
engine_l2.process_order_book_delta(&delta).unwrap();
let expire_ns: u64 = 1_500_000_000_000_000_000;
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1500.00"))
.quantity(Quantity::from("1.000"))
.time_in_force(TimeInForce::Gtd)
.expire_time(UnixNanos::from(expire_ns))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill_count = saved_messages
.iter()
.filter(|e| matches!(e, OrderEventAny::Filled(_)))
.count();
assert_eq!(fill_count, 1, "Should have 1 partial fill");
let fill = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.unwrap();
assert_eq!(fill.last_qty, Quantity::from("0.500"));
clear_order_event_handler_messages(&order_event_handler);
let delete_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Delete)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("0.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&delete_delta).unwrap();
clear_order_event_handler_messages(&order_event_handler);
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1490.00"),
Price::from("1510.00"),
Quantity::from("1.000"),
Quantity::from("1.000"),
UnixNanos::from(expire_ns + 1),
UnixNanos::from(expire_ns + 1),
);
engine_l2.process_quote_tick("e);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let expired = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Expired(exp) => Some(exp),
_ => None,
})
.expect("Expected OrderExpired event for GTD order");
assert_eq!(expired.client_order_id, client_order_id);
}
#[rstest]
fn test_gtd_order_at_boundary_tick_fills_before_expiry(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
support_gtd_orders: true,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let initial_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1505.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&initial_delta).unwrap();
let expire_ns: u64 = 1_500_000_000_000_000_000;
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1500.00"))
.quantity(Quantity::from("1.000"))
.time_in_force(TimeInForce::Gtd)
.expire_time(UnixNanos::from(expire_ns))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let crossing_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
2,
))
.ts_init(UnixNanos::from(expire_ns))
.build();
engine_l2.process_order_book_delta(&crossing_delta).unwrap();
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("Expected OrderFilled event at boundary tick");
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_qty, Quantity::from("1.000"));
let expired_count = saved_messages
.iter()
.filter(|e| matches!(e, OrderEventAny::Expired(_)))
.count();
assert_eq!(expired_count, 0, "Order should not expire after filling");
}
#[rstest]
fn test_price_protection_exact_boundary_fills(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig::builder()
.price_protection_points(100u32)
.build();
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let delta1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
let delta2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1501.00"),
Quantity::from("1.000"),
2,
))
.build();
engine_l2.process_order_book_delta(&delta1).unwrap();
engine_l2.process_order_book_delta(&delta2).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("2.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<&OrderFilled> = saved_messages
.iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
assert_eq!(fills.len(), 2, "Both levels at/within boundary should fill");
assert_eq!(fills[0].last_px, Price::from("1500.00"));
assert_eq!(fills[1].last_px, Price::from("1501.00"));
}
#[rstest]
fn test_settlement_price_used_on_contract_expiration(
instrument_es: InstrumentAny,
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let clock = Rc::new(RefCell::new(TestClock::new()));
clock
.borrow_mut()
.set_time(UnixNanos::from(1_680_000_000_000_000_000u64));
let config = OrderMatchingEngineConfig {
use_position_ids: true,
..Default::default()
};
let fee_model =
FeeModelAny::Fixed(FixedFeeModel::new(Money::new(0.0, Currency::USD()), None).unwrap());
let mut engine = OrderMatchingEngine::new(
instrument_es.clone(),
1,
FillModelHandle::default(),
fee_model.into(),
BookType::L2_MBP,
OmsType::Netting,
AccountType::Margin,
clock,
cache.clone(),
config,
);
let delta = OrderBookDeltaTestBuilder::new(instrument_es.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("4501.00"),
Quantity::from(10),
1,
))
.build();
engine.process_order_book_delta(&delta).unwrap();
let mut buy_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_es.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(1))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
cache
.borrow_mut()
.add_order(buy_order.clone(), None, None, false)
.unwrap();
engine.process_order(&mut buy_order, account_id);
let messages = get_order_event_handler_messages(&order_event_handler);
let mut fill = messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(*f),
_ => None,
})
.expect("Expected a fill event from the market order");
fill.position_id = Some(PositionId::new("P-001"));
let position = Position::new(&instrument_es, fill);
let strategy_id = position.strategy_id;
cache
.borrow_mut()
.add_position(&position, OmsType::Netting)
.unwrap();
clear_order_event_handler_messages(&order_event_handler);
let topic = format!("events.order.{strategy_id}");
let (published_handler, published_events): (_, TypedMessageSavingHandler<OrderEventAny>) =
get_typed_message_saving_handler(None);
msgbus::subscribe_order_events(topic.clone().into(), published_handler.clone(), None);
let settlement = Price::from("4600.00");
engine.set_settlement_price(settlement);
let close = InstrumentClose::new(
instrument_es.id(),
Price::from("4550.00"),
InstrumentCloseType::ContractExpired,
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_instrument_close(close);
msgbus::unsubscribe_order_events(topic.into(), &published_handler);
let published_messages = published_events.get_messages();
assert_eq!(published_messages.len(), 1);
assert!(matches!(
published_messages.first(),
Some(OrderEventAny::Initialized(init))
if init.client_order_id.as_str().starts_with("EXPIRATION-")
));
let messages = get_order_event_handler_messages(&order_event_handler);
let expiration_fill = messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(*f),
_ => None,
})
.expect("Expected a fill event from contract expiration");
assert_eq!(expiration_fill.last_px, settlement);
}
#[rstest]
fn test_trade_tick_seeds_liquidity_consumption_for_stop_market_fill(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
trade_execution: true,
liquidity_consumption: true,
..Default::default()
};
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("900.00"),
Quantity::from("100.000"),
100,
))
.build();
engine.process_order_book_delta(&bid_delta).unwrap();
let ask1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("10.000"),
1,
))
.build();
engine.process_order_book_delta(&ask1).unwrap();
let ask2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1001.00"),
Quantity::from("10.000"),
2,
))
.build();
engine.process_order_book_delta(&ask2).unwrap();
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1001.00"))
.quantity(Quantity::from("5.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut stop_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1001.00"),
Quantity::from("8.000"),
AggressorSide::Buyer,
TradeId::new("1"),
UnixNanos::from(2u64),
UnixNanos::from(2u64),
);
engine.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<_> = saved_messages
.iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
assert!(
!fills.is_empty(),
"Stop order should have triggered and filled"
);
let total_qty: f64 = fills.iter().map(|f| f.last_qty.as_f64()).sum();
assert!(
(total_qty - 5.0).abs() < 0.001,
"Total fill qty should be 5.0, was {total_qty}"
);
let got_fill_at_1001 = fills.iter().any(|f| f.last_px == Price::from("1001.00"));
assert!(
got_fill_at_1001,
"Expected fill at 1001.00 due to consumed liquidity at 1000.00, \
but all fills were at 1000.00. Trade consumption was not applied."
);
}
#[rstest]
fn test_trade_tick_seeds_consumption_with_stale_entry_reconciles(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
trade_execution: true,
liquidity_consumption: true,
..Default::default()
};
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("900.00"),
Quantity::from("100.000"),
100,
))
.build();
engine.process_order_book_delta(&bid_delta).unwrap();
let ask1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("10.000"),
1,
))
.build();
engine.process_order_book_delta(&ask1).unwrap();
let ask2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1001.00"),
Quantity::from("20.000"),
2,
))
.build();
engine.process_order_book_delta(&ask2).unwrap();
let mut order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("3.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut order1, account_id);
let ask_update = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Update)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("8.000"),
1,
))
.build();
engine.process_order_book_delta(&ask_update).unwrap();
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1001.00"))
.quantity(Quantity::from("5.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
.submit(true)
.build();
engine.process_order(&mut stop_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1001.00"),
Quantity::from("6.000"),
AggressorSide::Buyer,
TradeId::new("1"),
UnixNanos::from(2u64),
UnixNanos::from(2u64),
);
engine.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<_> = saved_messages
.iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
assert!(
!fills.is_empty(),
"Stop order should have triggered and filled"
);
let got_fill_at_1001 = fills.iter().any(|f| f.last_px == Price::from("1001.00"));
assert!(
got_fill_at_1001,
"Expected fill at 1001.00 due to consumed liquidity at 1000.00 with stale entry. \
Stale original_size was not reconciled, causing consumption to be discarded."
);
}
#[rstest]
fn test_trade_tick_skips_seeding_when_book_already_updated(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
trade_execution: true,
liquidity_consumption: true,
..Default::default()
};
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("900.00"),
Quantity::from("100.000"),
100,
))
.build();
engine.process_order_book_delta(&bid_delta).unwrap();
let ask1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("10.000"),
1,
))
.build();
engine.process_order_book_delta(&ask1).unwrap();
let ask2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1001.00"),
Quantity::from("20.000"),
2,
))
.build();
engine.process_order_book_delta(&ask2).unwrap();
let ask_update = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Update)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("2.000"),
1,
))
.ts_event(UnixNanos::from(3))
.build();
engine.process_order_book_delta(&ask_update).unwrap();
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Quantity::from("8.000"),
AggressorSide::Buyer,
TradeId::new("1"),
UnixNanos::from(2u64),
UnixNanos::from(2u64),
);
engine.process_trade_tick(&trade);
clear_order_event_handler_messages(&order_event_handler);
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("5.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<_> = saved_messages
.iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
let got_fill_at_1000 = fills.iter().any(|f| f.last_px == Price::from("1000.00"));
assert!(
got_fill_at_1000,
"Should fill at 1000.00 from actual book (seeding should be skipped for \
already-updated book). If all fills are at 1001.00, trade seeding double-counted."
);
}
#[rstest]
fn test_trade_tick_seeds_consumption_when_book_ts_equals_trade_ts(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
trade_execution: true,
liquidity_consumption: true,
..Default::default()
};
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let bid_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("900.00"),
Quantity::from("100.000"),
100,
))
.build();
engine.process_order_book_delta(&bid_delta).unwrap();
let ask1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("10.000"),
1,
))
.build();
engine.process_order_book_delta(&ask1).unwrap();
let ask2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1001.00"),
Quantity::from("10.000"),
2,
))
.build();
engine.process_order_book_delta(&ask2).unwrap();
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1001.00"))
.quantity(Quantity::from("5.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut stop_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1001.00"),
Quantity::from("8.000"),
AggressorSide::Buyer,
TradeId::new("1"),
UnixNanos::from(1u64),
UnixNanos::from(1u64),
);
engine.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<_> = saved_messages
.iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
assert!(
!fills.is_empty(),
"Stop order should have triggered and filled"
);
let got_fill_at_1001 = fills.iter().any(|f| f.last_px == Price::from("1001.00"));
assert!(
got_fill_at_1001,
"Expected fill at 1001.00, seeding should proceed when book.ts_last == trade.ts_event. \
If all fills at 1000.00, the guard incorrectly skipped seeding on equal timestamps."
);
}
#[rstest]
fn test_trade_tick_seeds_consumption_for_seller_side(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
trade_execution: true,
liquidity_consumption: true,
..Default::default()
};
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let ask_delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1100.00"),
Quantity::from("100.000"),
100,
))
.build();
engine.process_order_book_delta(&ask_delta).unwrap();
let bid1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1000.00"),
Quantity::from("10.000"),
1,
))
.build();
engine.process_order_book_delta(&bid1).unwrap();
let bid2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("999.00"),
Quantity::from("10.000"),
2,
))
.build();
engine.process_order_book_delta(&bid2).unwrap();
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.trigger_price(Price::from("999.00"))
.quantity(Quantity::from("5.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut stop_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("999.00"),
Quantity::from("8.000"),
AggressorSide::Seller,
TradeId::new("1"),
UnixNanos::from(2u64),
UnixNanos::from(2u64),
);
engine.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<_> = saved_messages
.iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
assert!(
!fills.is_empty(),
"Stop order should have triggered and filled"
);
let total_qty: f64 = fills.iter().map(|f| f.last_qty.as_f64()).sum();
assert!(
(total_qty - 5.0).abs() < 0.001,
"Total fill qty should be 5.0, was {total_qty}"
);
let got_fill_at_999 = fills.iter().any(|f| f.last_px == Price::from("999.00"));
assert!(
got_fill_at_999,
"Expected fill at 999.00 due to consumed bid liquidity at 1000.00, \
but all fills were at 1000.00. Seller-side trade consumption was not applied."
);
}
#[rstest]
fn test_process_order_rejection_no_refcell_reentrant_panic(
account_id: AccountId,
equity_aapl: Equity,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let instrument = InstrumentAny::Equity(equity_aapl);
let cache_clone = Rc::clone(&cache);
let handler = TypedIntoHandler::from(move |_event: OrderEventAny| {
let _guard = cache_clone.borrow_mut();
});
msgbus::register_order_event_endpoint(MessagingSwitchboard::exec_engine_process(), handler);
let mut engine = get_order_matching_engine(instrument, None, Some(cache), None, None);
let mut sell_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(engine.instrument.id())
.side(OrderSide::Sell)
.quantity(Quantity::from("1"))
.submit(true)
.build();
engine.process_order(&mut sell_order, account_id);
}
fn get_l1_queue_position_engine(
instrument: InstrumentAny,
) -> (
OrderMatchingEngine,
Rc<RefCell<Cache>>,
TypedIntoMessageSavingHandler<OrderEventAny>,
) {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let handler = order_event_handler_with_cache(Rc::clone(&cache));
let config = OrderMatchingEngineConfig {
trade_execution: true,
queue_position: true,
..Default::default()
};
let engine = OrderMatchingEngine::new(
instrument,
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Margin,
clock,
Rc::clone(&cache),
config,
);
(engine, cache, handler)
}
#[rstest]
fn test_l1_queue_position_deep_order_deferred_snapshot(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let (mut engine, _cache, handler) = get_l1_queue_position_engine(instrument_eth_usdt.clone());
let quote0 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("54.59"),
Price::from("54.62"),
Quantity::from("600.000"),
Quantity::from("3617.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e0);
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("54.57"))
.quantity(Quantity::from("1000.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut order, account_id);
clear_order_event_handler_messages(&handler);
let trade1 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("54.59"),
Quantity::from("37.000"),
AggressorSide::Seller,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&trade1);
let quote1 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("54.59"),
Price::from("54.62"),
Quantity::from("563.000"),
Quantity::from("3617.000"),
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_quote_tick("e1);
let trade2 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("54.59"),
Quantity::from("200.000"),
AggressorSide::Seller,
TradeId::new("2"),
UnixNanos::from(3),
UnixNanos::from(3),
);
engine.process_trade_tick(&trade2);
let trade3 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("54.59"),
Quantity::from("363.000"),
AggressorSide::Seller,
TradeId::new("3"),
UnixNanos::from(4),
UnixNanos::from(4),
);
engine.process_trade_tick(&trade3);
let quote2 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("54.57"),
Price::from("54.59"),
Quantity::from("1895.000"),
Quantity::from("200.000"),
UnixNanos::from(5),
UnixNanos::from(5),
);
engine.process_quote_tick("e2);
assert_eq!(
get_order_event_handler_messages(&handler)
.iter()
.filter(|e| matches!(e, OrderEventAny::Filled(_)))
.count(),
0,
"Should not fill when BBO first reaches order level"
);
let trade4 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("54.57"),
Quantity::from("200.000"),
AggressorSide::Seller,
TradeId::new("4"),
UnixNanos::from(6),
UnixNanos::from(6),
);
engine.process_trade_tick(&trade4);
assert_eq!(
get_order_event_handler_messages(&handler)
.iter()
.filter(|e| matches!(e, OrderEventAny::Filled(_)))
.count(),
0,
"Should not fill, queue still has 1695 ahead"
);
}
#[rstest]
fn test_l1_queue_position_at_bbo_trades_decrement_queue(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let (mut engine, _cache, handler) = get_l1_queue_position_engine(instrument_eth_usdt.clone());
let quote0 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("54.59"),
Price::from("54.62"),
Quantity::from("600.000"),
Quantity::from("3617.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e0);
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("54.59"))
.quantity(Quantity::from("100.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut order, account_id);
clear_order_event_handler_messages(&handler);
let trade1 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("54.59"),
Quantity::from("37.000"),
AggressorSide::Seller,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&trade1);
assert!(
get_order_event_handler_messages(&handler)
.iter()
.all(|e| !matches!(e, OrderEventAny::Filled(_))),
"Should not fill after first trade"
);
let quote1 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("54.59"),
Price::from("54.62"),
Quantity::from("563.000"),
Quantity::from("3617.000"),
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_quote_tick("e1);
let trade2 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("54.59"),
Quantity::from("200.000"),
AggressorSide::Seller,
TradeId::new("2"),
UnixNanos::from(3),
UnixNanos::from(3),
);
engine.process_trade_tick(&trade2);
assert!(
get_order_event_handler_messages(&handler)
.iter()
.all(|e| !matches!(e, OrderEventAny::Filled(_))),
"Should not fill after second trade"
);
let trade3 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("54.59"),
Quantity::from("363.000"),
AggressorSide::Seller,
TradeId::new("3"),
UnixNanos::from(4),
UnixNanos::from(4),
);
engine.process_trade_tick(&trade3);
assert!(
get_order_event_handler_messages(&handler)
.iter()
.all(|e| !matches!(e, OrderEventAny::Filled(_))),
"Should not fill when trade exactly depletes queue (zero excess)"
);
let trade4 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("54.59"),
Quantity::from("100.000"),
AggressorSide::Seller,
TradeId::new("4"),
UnixNanos::from(5),
UnixNanos::from(5),
);
engine.process_trade_tick(&trade4);
let fills: Vec<_> = get_order_event_handler_messages(&handler)
.iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(*f),
_ => None,
})
.collect();
assert_eq!(fills.len(), 1, "Should fill after queue fully exhausted");
assert_eq!(fills[0].last_qty, Quantity::from("100.000"));
}
#[rstest]
#[case::buy(OrderSide::Buy, "99.00", "98.00", AggressorSide::Seller)]
#[case::sell(OrderSide::Sell, "101.00", "102.00", AggressorSide::Buyer)]
fn test_l1_queue_position_cross_through_with_queue_ahead_does_not_fill(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] order_side: OrderSide,
#[case] order_price: &str,
#[case] trade_price: &str,
#[case] aggressor_side: AggressorSide,
) {
let (mut engine, _cache, handler) = get_l1_queue_position_engine(instrument_eth_usdt.clone());
let quote0 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("99.00"),
Price::from("101.00"),
Quantity::from("10.000"),
Quantity::from("10.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e0);
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(order_side)
.price(Price::from(order_price))
.quantity(Quantity::from("5.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut order, account_id);
clear_order_event_handler_messages(&handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from(trade_price),
Quantity::from("5.000"),
aggressor_side,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&trade);
assert_eq!(
get_order_event_handler_messages(&handler)
.iter()
.filter(|e| matches!(e, OrderEventAny::Filled(_)))
.count(),
0,
"cross-through trade must not fill while queue_ahead > 0",
);
}
#[rstest]
fn test_l1_queue_position_trade_partial_does_not_fill(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let (mut engine, _cache, handler) = get_l1_queue_position_engine(instrument_eth_usdt.clone());
let quote0 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Price::from("101.00"),
Quantity::from("10.000"),
Quantity::from("10.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e0);
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("100.00"))
.quantity(Quantity::from("5.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut order, account_id);
clear_order_event_handler_messages(&handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Quantity::from("5.000"),
AggressorSide::Seller,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&trade);
assert_eq!(
get_order_event_handler_messages(&handler)
.iter()
.filter(|e| matches!(e, OrderEventAny::Filled(_)))
.count(),
0,
"Should not fill when queue only partially consumed"
);
}
#[rstest]
fn test_l1_queue_position_full_example(account_id: AccountId, instrument_eth_usdt: InstrumentAny) {
let (mut engine, _cache, handler) = get_l1_queue_position_engine(instrument_eth_usdt.clone());
let quote0 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Price::from("101.00"),
Quantity::from("10.000"),
Quantity::from("10.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e0);
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("100.00"))
.quantity(Quantity::from("2.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut order, account_id);
clear_order_event_handler_messages(&handler);
let trade1 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Quantity::from("3.000"),
AggressorSide::Seller,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&trade1);
assert!(
get_order_event_handler_messages(&handler)
.iter()
.all(|e| !matches!(e, OrderEventAny::Filled(_)))
);
let quote1 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Price::from("101.00"),
Quantity::from("8.000"),
Quantity::from("10.000"),
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_quote_tick("e1);
let trade2 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Quantity::from("4.000"),
AggressorSide::Seller,
TradeId::new("2"),
UnixNanos::from(3),
UnixNanos::from(3),
);
engine.process_trade_tick(&trade2);
assert!(
get_order_event_handler_messages(&handler)
.iter()
.all(|e| !matches!(e, OrderEventAny::Filled(_)))
);
let quote2 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Price::from("101.00"),
Quantity::from("15.000"),
Quantity::from("10.000"),
UnixNanos::from(4),
UnixNanos::from(4),
);
engine.process_quote_tick("e2);
let trade3 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Quantity::from("2.000"),
AggressorSide::Seller,
TradeId::new("3"),
UnixNanos::from(5),
UnixNanos::from(5),
);
engine.process_trade_tick(&trade3);
assert!(
get_order_event_handler_messages(&handler)
.iter()
.all(|e| !matches!(e, OrderEventAny::Filled(_)))
);
let trade4 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Quantity::from("3.000"),
AggressorSide::Seller,
TradeId::new("4"),
UnixNanos::from(6),
UnixNanos::from(6),
);
engine.process_trade_tick(&trade4);
let fills: Vec<_> = get_order_event_handler_messages(&handler)
.iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(*f),
_ => None,
})
.collect();
assert_eq!(fills.len(), 1);
assert_eq!(fills[0].last_qty, Quantity::from("2.000"));
}
#[rstest]
fn test_l1_queue_position_bid_price_drop_clears_queue(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let (mut engine, _cache, handler) = get_l1_queue_position_engine(instrument_eth_usdt.clone());
let quote0 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Price::from("101.00"),
Quantity::from("10.000"),
Quantity::from("10.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e0);
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("100.00"))
.quantity(Quantity::from("2.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut order, account_id);
clear_order_event_handler_messages(&handler);
let quote1 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("99.00"),
Price::from("101.00"),
Quantity::from("5.000"),
Quantity::from("10.000"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_quote_tick("e1);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Quantity::from("2.000"),
AggressorSide::Seller,
TradeId::new("1"),
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_trade_tick(&trade);
assert_eq!(
get_order_event_handler_messages(&handler)
.iter()
.filter(|e| matches!(e, OrderEventAny::Filled(_)))
.count(),
1,
);
}
#[rstest]
fn test_l1_queue_position_pending_not_stuck_after_zero_size_quote(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let (mut engine, _cache, handler) = get_l1_queue_position_engine(instrument_eth_usdt.clone());
let quote0 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Price::from("101.00"),
Quantity::from("500.000"),
Quantity::from("500.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e0);
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("99.00"))
.quantity(Quantity::from("5.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut order, account_id);
clear_order_event_handler_messages(&handler);
let quote1 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Price::from("101.00"),
Quantity::from("0.000"),
Quantity::from("500.000"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_quote_tick("e1);
let quote2 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("98.00"),
Price::from("99.00"),
Quantity::from("300.000"),
Quantity::from("200.000"),
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_quote_tick("e2);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("99.00"),
Quantity::from("5.000"),
AggressorSide::Seller,
TradeId::new("1"),
UnixNanos::from(3),
UnixNanos::from(3),
);
engine.process_trade_tick(&trade);
assert_eq!(
get_order_event_handler_messages(&handler)
.iter()
.filter(|e| matches!(e, OrderEventAny::Filled(_)))
.count(),
1,
);
}
#[rstest]
fn test_l1_queue_position_pending_resolved_by_any_side_trade(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let (mut engine, _cache, handler) = get_l1_queue_position_engine(instrument_eth_usdt.clone());
let quote0 = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("100.00"),
Price::from("101.00"),
Quantity::from("500.000"),
Quantity::from("500.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e0);
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("99.00"))
.quantity(Quantity::from("5.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut order, account_id);
clear_order_event_handler_messages(&handler);
let trade1 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("98.00"),
Quantity::from("5.000"),
AggressorSide::Buyer,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&trade1);
assert_eq!(
get_order_event_handler_messages(&handler)
.iter()
.filter(|e| matches!(e, OrderEventAny::Filled(_)))
.count(),
0,
);
let trade2 = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("99.00"),
Quantity::from("5.000"),
AggressorSide::Seller,
TradeId::new("2"),
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_trade_tick(&trade2);
assert_eq!(
get_order_event_handler_messages(&handler)
.iter()
.filter(|e| matches!(e, OrderEventAny::Filled(_)))
.count(),
1,
);
}
#[rstest]
fn test_triggered_stop_market_removed_from_matching_core_after_trade_trigger(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let ask1 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("10.000"),
1,
))
.build();
engine.process_order_book_delta(&ask1).unwrap();
let ask2 = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1001.00"),
Quantity::from("10.000"),
2,
))
.build();
engine.process_order_book_delta(&ask2).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-9");
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1001.00"))
.quantity(Quantity::from("5.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut stop_order, account_id);
assert!(engine.order_exists(client_order_id));
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1001.00"),
Quantity::from("8.000"),
AggressorSide::Buyer,
TradeId::new("repro-1"),
UnixNanos::from(2u64),
UnixNanos::from(2u64),
);
engine.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert!(
saved_messages
.iter()
.any(|event| matches!(event, OrderEventAny::Filled(_))),
"Stop order should have triggered and filled",
);
assert!(
!engine.order_exists(client_order_id),
"Filled stop order should be removed from matching core"
);
}
#[rstest]
fn test_fully_filled_limit_order_removed_from_core(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("5.000"),
1,
))
.build();
engine.process_order_book_delta(&delta).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1501.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert!(
saved_messages
.iter()
.any(|event| matches!(event, OrderEventAny::Filled(_))),
"Limit order should have filled immediately",
);
assert!(
!engine.order_exists(client_order_id),
"Filled limit order should be removed from matching core",
);
assert_eq!(
engine.cached_filled_qty_len(),
1,
"cached_filled_qty should stay until the fill event closes the cached order",
);
}
#[rstest]
fn test_closed_filled_order_purges_cached_fill_qty_after_cache_update(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
None,
);
let delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("5.000"),
1,
))
.build();
engine.process_order_book_delta(&delta).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1501.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
assert_eq!(engine.cached_filled_qty_len(), 1);
let fill_event = get_order_event_handler_messages(&order_event_handler)
.iter()
.find(|event| matches!(event, OrderEventAny::Filled(_)))
.unwrap()
.clone();
cache.borrow_mut().update_order(&fill_event).unwrap();
engine.iterate(UnixNanos::from(1), AggressorSide::NoAggressor);
assert_eq!(
engine.cached_filled_qty_len(),
0,
"closed cached orders should purge filled-quantity guards without core membership",
);
clear_order_event_handler_messages(&order_event_handler);
engine.fill_limit_order(client_order_id);
assert!(
get_order_event_handler_messages(&order_event_handler)
.iter()
.all(|event| !matches!(event, OrderEventAny::Filled(_))),
"closed orders should not fill again after their cached guard is purged",
);
}
#[rstest]
fn test_closed_filled_market_order_does_not_fill_after_cache_update(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
None,
);
let delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("5.000"),
1,
))
.build();
engine.process_order_book_delta(&delta).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
assert_eq!(engine.cached_filled_qty_len(), 1);
let fill_event = get_order_event_handler_messages(&order_event_handler)
.iter()
.find(|event| matches!(event, OrderEventAny::Filled(_)))
.unwrap()
.clone();
cache.borrow_mut().update_order(&fill_event).unwrap();
engine.iterate(UnixNanos::from(1), AggressorSide::NoAggressor);
assert_eq!(
engine.cached_filled_qty_len(),
0,
"closed cached market orders should purge filled-quantity guards without core membership",
);
clear_order_event_handler_messages(&order_event_handler);
engine.fill_market_order(client_order_id);
assert!(
get_order_event_handler_messages(&order_event_handler)
.iter()
.all(|event| !matches!(event, OrderEventAny::Filled(_))),
"closed market orders should not fill again after their cached guard is purged",
);
}
#[rstest]
fn test_triggered_stop_limit_removed_from_core_after_fill(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1000.00"),
Quantity::from("10.000"),
1,
))
.build();
engine.process_order_book_delta(&ask).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut stop_limit = OrderTestBuilder::new(OrderType::StopLimit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1005.00"))
.price(Price::from("1010.00"))
.quantity(Quantity::from("2.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut stop_limit, account_id);
assert!(engine.order_exists(client_order_id));
clear_order_event_handler_messages(&order_event_handler);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1005.00"),
Quantity::from("10.000"),
AggressorSide::Buyer,
TradeId::new("trigger-1"),
UnixNanos::from(2u64),
UnixNanos::from(2u64),
);
engine.process_trade_tick(&trade);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert!(
saved_messages
.iter()
.any(|event| matches!(event, OrderEventAny::Filled(_))),
"Stop limit order should have triggered and filled",
);
assert!(
!engine.order_exists(client_order_id),
"Filled stop limit order should be removed from matching core",
);
}
#[rstest]
fn test_fully_filled_market_to_limit_not_in_core(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let delta = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("5.000"),
1,
))
.build();
engine.process_order_book_delta(&delta).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut mtl_order = OrderTestBuilder::new(OrderType::MarketToLimit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.build();
let submitted = TestOrderEventStubs::submitted(&mtl_order, account_id);
mtl_order.apply(submitted).unwrap();
engine.process_order(&mut mtl_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
assert!(
saved_messages
.iter()
.any(|event| matches!(event, OrderEventAny::Filled(_))),
"MarketToLimit order should have filled",
);
assert!(
!engine.order_exists(client_order_id),
"Fully filled MarketToLimit should not rest in matching core",
);
assert_eq!(
engine.cached_filled_qty_len(),
1,
"cached_filled_qty should stay until the fill event closes the cached order",
);
}
#[rstest]
fn test_l1_ask_tracks_decreasing_seller_trade_prices(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
trade_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("990.00"),
Price::from("1010.00"),
Quantity::from("100.000"),
Quantity::from("100.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
for (i, price_str) in ["1000.00", "1050.00", "900.00"].iter().enumerate() {
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from(*price_str),
Quantity::from("10.000"),
AggressorSide::Seller,
TradeId::new(format!("{}", i + 1)),
UnixNanos::from((i + 1) as u64),
UnixNanos::from((i + 1) as u64),
);
engine.process_trade_tick(&trade);
}
clear_order_event_handler_messages(&order_event_handler);
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("Market BUY should have filled");
assert_eq!(
fill.last_px,
Price::from("900.00"),
"Fill price should match latest trade, not stale ask high-water mark"
);
}
#[rstest]
fn test_l1_bid_tracks_increasing_buyer_trade_prices(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
trade_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("990.00"),
Price::from("1010.00"),
Quantity::from("100.000"),
Quantity::from("100.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
for (i, price_str) in ["1000.00", "950.00", "1100.00"].iter().enumerate() {
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from(*price_str),
Quantity::from("10.000"),
AggressorSide::Buyer,
TradeId::new(format!("{}", i + 1)),
UnixNanos::from((i + 1) as u64),
UnixNanos::from((i + 1) as u64),
);
engine.process_trade_tick(&trade);
}
clear_order_event_handler_messages(&order_event_handler);
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("Market SELL should have filled");
assert_eq!(
fill.last_px,
Price::from("1100.00"),
"Fill price should match latest trade, not stale bid low-water mark"
);
}
#[rstest]
fn test_l1_sequential_trades_alternating_aggressors(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
trade_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("990.00"),
Price::from("1010.00"),
Quantity::from("100.000"),
Quantity::from("100.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
let trades: Vec<(&str, AggressorSide)> = vec![
("1000.00", AggressorSide::Seller),
("1020.00", AggressorSide::Buyer),
("980.00", AggressorSide::Seller),
("950.00", AggressorSide::Buyer),
];
for (i, (price_str, side)) in trades.iter().enumerate() {
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from(*price_str),
Quantity::from("10.000"),
*side,
TradeId::new(format!("{}", i + 1)),
UnixNanos::from((i + 1) as u64),
UnixNanos::from((i + 1) as u64),
);
engine.process_trade_tick(&trade);
}
clear_order_event_handler_messages(&order_event_handler);
let mut buy_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut buy_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("Market BUY should have filled");
assert_eq!(
fill.last_px,
Price::from("950.00"),
"BUY fill price should match latest trade price"
);
}
#[rstest]
fn test_l1_trade_only_no_initial_quote_ask_tracks_price(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let fill_model = FillModelAny::BestPrice(BestPriceFillModel::new(1.0, 0.0, None).unwrap());
let config = OrderMatchingEngineConfig {
trade_execution: true,
..Default::default()
};
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = OrderMatchingEngine::new(
instrument_eth_usdt.clone(),
1,
fill_model.into(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
config,
);
for (i, price_str) in ["100.00", "105.00", "90.00"].iter().enumerate() {
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from(*price_str),
Quantity::from("10.000"),
AggressorSide::Seller,
TradeId::new(format!("{}", i + 1)),
UnixNanos::from((i + 1) as u64),
UnixNanos::from((i + 1) as u64),
);
engine.process_trade_tick(&trade);
}
clear_order_event_handler_messages(&order_event_handler);
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("Market BUY should have filled");
assert_eq!(
fill.last_px,
Price::from("90.00"),
"BestPriceFillModel should use latest trade price, not stale ask"
);
}
#[rstest]
fn test_l1_no_aggressor_trades_track_price(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
trade_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("990.00"),
Price::from("1010.00"),
Quantity::from("100.000"),
Quantity::from("100.000"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
for (i, price_str) in ["1000.00", "1050.00", "900.00"].iter().enumerate() {
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from(*price_str),
Quantity::from("10.000"),
AggressorSide::NoAggressor,
TradeId::new(format!("{}", i + 1)),
UnixNanos::from((i + 1) as u64),
UnixNanos::from((i + 1) as u64),
);
engine.process_trade_tick(&trade);
}
clear_order_event_handler_messages(&order_event_handler);
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fill = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("Market BUY should have filled");
assert_eq!(
fill.last_px,
Price::from("900.00"),
"NoAggressor trades should track price like other aggressor sides"
);
}
#[rstest]
fn test_stale_trade_tick_does_not_mutate_book(instrument_eth_usdt: InstrumentAny) {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = OrderMatchingEngine::new(
instrument_eth_usdt.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig {
trade_execution: true,
..Default::default()
},
);
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Price::from("1000.00"),
Quantity::from("1.000"),
Quantity::from("1.000"),
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_quote_tick("e);
let stale_trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1100.00"),
Quantity::from("1.000"),
AggressorSide::Buyer,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&stale_trade);
assert_eq!(engine.best_bid_price(), Some(Price::from("1000.00")));
assert_eq!(engine.best_ask_price(), Some(Price::from("1000.00")));
}
#[rstest]
fn test_stale_quote_tick_does_not_mutate_book(instrument_eth_usdt: InstrumentAny) {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = OrderMatchingEngine::new(
instrument_eth_usdt.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
let trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Quantity::from("1.000"),
AggressorSide::Buyer,
TradeId::new("1"),
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_trade_tick(&trade);
let stale_quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1100.00"),
Price::from("1200.00"),
Quantity::from("1.000"),
Quantity::from("1.000"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_quote_tick(&stale_quote);
assert_eq!(engine.best_bid_price(), Some(Price::from("1000.00")));
assert_eq!(engine.best_ask_price(), Some(Price::from("1000.00")));
}
#[rstest]
fn test_modify_then_iterate_fills_at_new_limit_price(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
None,
);
let initial_ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("110.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&initial_ask).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("99.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
cache
.borrow_mut()
.add_order(limit_order.clone(), None, None, false)
.unwrap();
engine_l2.process_order(&mut limit_order, account_id);
let modified_limit_price = Price::from("100.00");
let modify_order_command = ModifyOrder::new(
TraderId::test_default(),
Some(ClientId::from("CLIENT-001")),
StrategyId::test_default(),
instrument_eth_usdt.id(),
client_order_id,
Some(VenueOrderId::from("V1")),
None,
Some(modified_limit_price),
None,
UUID4::new(),
UnixNanos::default(),
None,
None, );
engine_l2.process_modify(&modify_order_command, account_id);
let crossing_ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("99.50"),
Quantity::from("1.000"),
2,
))
.build();
engine_l2.process_order_book_delta(&crossing_ask).unwrap();
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let event_types: Vec<OrderEventType> = saved_messages.iter().map(|e| e.event_type()).collect();
assert!(
event_types.contains(&OrderEventType::Filled),
"expected fill at the modified limit price, was events {event_types:?}",
);
let fill = saved_messages
.iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("OrderFilled event present");
assert_eq!(fill.client_order_id, client_order_id);
assert_eq!(fill.last_px, Price::from("100.00"));
assert_eq!(fill.last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_trailing_stop_no_recompute_skips_resync(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, Some(cache), None, None);
let initial_ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&initial_ask).unwrap();
let initial_bid = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1499.00"),
Quantity::from("1.000"),
2,
))
.build();
engine_l2.process_order_book_delta(&initial_bid).unwrap();
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut trailing = OrderTestBuilder::new(OrderType::TrailingStopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.trigger_price(Price::from("1505.00"))
.trigger_type(TriggerType::BidAsk)
.trailing_offset(dec!(1))
.trailing_offset_type(TrailingOffsetType::Price)
.client_order_id(client_order_id)
.submit(true)
.build();
engine_l2.process_order(&mut trailing, account_id);
let new_ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1490.00"),
Quantity::from("1.000"),
3,
))
.build();
engine_l2.process_order_book_delta(&new_ask).unwrap();
let after_recompute = get_order_event_handler_messages(&order_event_handler);
let updated_after_recompute = after_recompute
.iter()
.filter(|e| matches!(e, OrderEventAny::Updated(_)))
.count();
assert!(
updated_after_recompute >= 1,
"trailing recompute should dispatch at least one OrderUpdated, was {updated_after_recompute}",
);
let stop_after_recompute = engine_l2
.get_core()
.get_order(client_order_id)
.copied()
.expect("trailing stop should still be in the core");
clear_order_event_handler_messages(&order_event_handler);
let no_change_ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1495.00"),
Quantity::from("1.000"),
4,
))
.build();
engine_l2.process_order_book_delta(&no_change_ask).unwrap();
let after_no_change = get_order_event_handler_messages(&order_event_handler);
assert!(
!after_no_change
.iter()
.any(|e| matches!(e, OrderEventAny::Updated(_))),
"no-recompute quote must not dispatch OrderUpdated, was {after_no_change:?}",
);
let stop_after_no_change = engine_l2
.get_core()
.get_order(client_order_id)
.copied()
.expect("trailing stop should still be in the core");
assert_eq!(
stop_after_recompute, stop_after_no_change,
"core RestingOrder should be byte-identical when nothing changed",
);
}
#[rstest]
fn test_gtd_expiry_and_trailing_recompute_in_same_iterate(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let config = OrderMatchingEngineConfig {
support_gtd_orders: true,
..Default::default()
};
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, Some(config));
let initial_ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1500.00"),
Quantity::from("1.000"),
1,
))
.build();
engine_l2.process_order_book_delta(&initial_ask).unwrap();
let initial_bid = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("1485.00"),
Quantity::from("1.000"),
2,
))
.build();
engine_l2.process_order_book_delta(&initial_bid).unwrap();
let expire_ns: u64 = 1_500_000_000_000_000_000;
let gtd_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut gtd_limit = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1490.00"))
.quantity(Quantity::from("1.000"))
.time_in_force(TimeInForce::Gtd)
.expire_time(UnixNanos::from(expire_ns))
.client_order_id(gtd_id)
.submit(true)
.build();
engine_l2.process_order(&mut gtd_limit, account_id);
let trail_id = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut trailing = OrderTestBuilder::new(OrderType::TrailingStopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.trigger_price(Price::from("1510.00"))
.trigger_type(TriggerType::BidAsk)
.trailing_offset(dec!(5))
.trailing_offset_type(TrailingOffsetType::Price)
.client_order_id(trail_id)
.submit(true)
.build();
engine_l2.process_order(&mut trailing, account_id);
clear_order_event_handler_messages(&order_event_handler);
let new_ask = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1495.00"),
Quantity::from("1.000"),
3,
))
.ts_init(UnixNanos::from(expire_ns))
.build();
engine_l2.process_order_book_delta(&new_ask).unwrap();
let saved = get_order_event_handler_messages(&order_event_handler);
let expired = saved
.iter()
.find(|e| matches!(e, OrderEventAny::Expired(ev) if ev.client_order_id == gtd_id));
assert!(
expired.is_some(),
"GTD limit should expire at boundary tick within the per-order loop",
);
let trail_update = saved.iter().rev().find_map(|e| match e {
OrderEventAny::Updated(ev) if ev.client_order_id == trail_id => Some(ev),
_ => None,
});
let upd = trail_update
.expect("Trailing-stop should emit OrderUpdated in the same iterate as the GTD expiry");
assert_eq!(
upd.trigger_price.unwrap(),
Price::from("1500.00"),
"Inline trail recompute must use ask(1495) + offset(5)",
);
}
#[rstest]
fn test_fallback_target_restore_preserves_core_last_after_maker_fill(
instrument_eth_usdt: InstrumentAny,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let seed_trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1500.00"),
Quantity::from("1.000"),
AggressorSide::Buyer,
TradeId::from("seed"),
UnixNanos::from(1u64),
UnixNanos::from(1u64),
);
engine_l2.process_trade_tick(&seed_trade);
let pre_fill_last = engine_l2
.get_core()
.last
.expect("trade tick should seed core.last");
assert_eq!(pre_fill_last, Price::from("1500.00"));
let limit_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1490.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(limit_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit, account_id);
let crossing = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1480.00"),
Quantity::from("1.000"),
2,
))
.build();
engine_l2.process_order_book_delta(&crossing).unwrap();
assert_eq!(
engine_l2.get_core().last,
Some(pre_fill_last),
"fallback restore should return core.last to the pre-fill value",
);
}
#[rstest]
fn test_trailing_stop_recompute_after_maker_fill_uses_mutated_core(
instrument_eth_usdt: InstrumentAny,
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let mut engine_l2 =
get_order_matching_engine_l2(instrument_eth_usdt.clone(), None, None, None, None);
let seed_trade = TradeTick::new(
instrument_eth_usdt.id(),
Price::from("1500.00"),
Quantity::from("1.000"),
AggressorSide::Buyer,
TradeId::from("seed"),
UnixNanos::from(1u64),
UnixNanos::from(1u64),
);
engine_l2.process_trade_tick(&seed_trade);
let limit_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1490.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(limit_id)
.submit(true)
.build();
engine_l2.process_order(&mut limit, account_id);
let trail_id = ClientOrderId::from("O-19700101-000000-001-001-2");
let mut trail = OrderTestBuilder::new(OrderType::TrailingStopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.quantity(Quantity::from("1.000"))
.trigger_price(Price::from("1485.00"))
.trigger_type(TriggerType::LastPrice)
.trailing_offset(dec!(3))
.trailing_offset_type(TrailingOffsetType::Price)
.client_order_id(trail_id)
.submit(true)
.build();
engine_l2.process_order(&mut trail, account_id);
clear_order_event_handler_messages(&order_event_handler);
let crossing = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("1480.00"),
Quantity::from("1.000"),
2,
))
.build();
engine_l2.process_order_book_delta(&crossing).unwrap();
let saved = get_order_event_handler_messages(&order_event_handler);
let trail_update = saved
.iter()
.rev()
.find_map(|e| match e {
OrderEventAny::Updated(ev) if ev.client_order_id == trail_id => Some(ev),
_ => None,
})
.expect("trailing-stop should recompute trigger after the Maker fill");
assert_eq!(
trail_update.trigger_price.unwrap(),
Price::from("1487.00"),
"trail recompute must use mutated `core.last` (1490) minus offset(3)",
);
}
#[rstest]
fn test_update_instrument_resets_market_state(instrument_eth_usdt: InstrumentAny) {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = OrderMatchingEngine::new(
instrument_eth_usdt.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Price::from("1001.00"),
Quantity::from("1.000"),
Quantity::from("1.000"),
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_quote_tick("e);
let updated_instrument = match instrument_eth_usdt {
InstrumentAny::CryptoPerpetual(mut crypto_perp) => {
crypto_perp.price_precision = 3;
crypto_perp.price_increment = Price::from("0.001");
InstrumentAny::CryptoPerpetual(crypto_perp)
}
_ => panic!("Test fixture expected CryptoPerpetual instrument"),
};
engine.update_instrument(updated_instrument).unwrap();
assert!(engine.best_bid_price().is_none());
assert!(engine.best_ask_price().is_none());
assert_eq!(engine.get_core().price_increment, Price::from("0.001"));
}
#[rstest]
fn test_update_instrument_without_precision_change_keeps_market_state(
instrument_eth_usdt: InstrumentAny,
) {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = OrderMatchingEngine::new(
instrument_eth_usdt.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Price::from("1001.00"),
Quantity::from("1.000"),
Quantity::from("1.000"),
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_quote_tick("e);
let best_bid_before = engine.best_bid_price();
let best_ask_before = engine.best_ask_price();
let increment_before = engine.get_core().price_increment;
let updated_instrument = match instrument_eth_usdt {
InstrumentAny::CryptoPerpetual(mut crypto_perp) => {
crypto_perp.ts_event = UnixNanos::from(3);
InstrumentAny::CryptoPerpetual(crypto_perp)
}
_ => panic!("Test fixture expected CryptoPerpetual instrument"),
};
engine.update_instrument(updated_instrument).unwrap();
assert_eq!(engine.best_bid_price(), best_bid_before);
assert_eq!(engine.best_ask_price(), best_ask_before);
assert_eq!(engine.get_core().price_increment, increment_before);
}
#[rstest]
fn test_update_instrument_normalizes_tick_compatible_resting_order_fill(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, Some(cache), None, None);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1000.00"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
let updated_instrument = crypto_perpetual_with_price_precision(instrument_eth_usdt, 3, "0.001");
engine
.update_instrument(updated_instrument.clone())
.unwrap();
let trade = TradeTick::new(
updated_instrument.id(),
Price::from("999.000"),
Quantity::from("10.000"),
AggressorSide::Seller,
TradeId::new("1"),
UnixNanos::from(1),
UnixNanos::from(1),
);
engine.process_trade_tick(&trade);
let filled_events: Vec<_> = get_order_event_handler_messages(&order_event_handler)
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(filled) => Some(*filled),
_ => None,
})
.collect();
assert_eq!(filled_events.len(), 1);
assert_eq!(filled_events[0].client_order_id, client_order_id);
assert_eq!(filled_events[0].last_px, Price::from("1000.000"));
assert_eq!(filled_events[0].last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_update_instrument_removes_incompatible_resting_order_from_core(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let initial_instrument = crypto_perpetual_with_price_precision(instrument_eth_usdt, 3, "0.001");
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine = get_order_matching_engine(
initial_instrument.clone(),
None,
Some(cache.clone()),
None,
None,
);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(initial_instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1000.005"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
assert!(engine.order_exists(client_order_id));
clear_order_event_handler_messages(&order_event_handler);
let updated_instrument = crypto_perpetual_with_price_precision(initial_instrument, 2, "0.01");
engine.update_instrument(updated_instrument).unwrap();
assert!(!engine.order_exists(client_order_id));
assert_eq!(
cache.borrow().order(&client_order_id).unwrap().status(),
OrderStatus::Canceled
);
assert!(
get_order_event_handler_messages(&order_event_handler)
.iter()
.any(|event| matches!(event, OrderEventAny::Canceled(canceled) if canceled.client_order_id == client_order_id))
);
}
#[rstest]
fn test_update_instrument_cancels_tick_incompatible_resting_order(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let initial_instrument = crypto_perpetual_with_price_precision(instrument_eth_usdt, 3, "0.001");
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine = get_order_matching_engine(
initial_instrument.clone(),
None,
Some(cache.clone()),
None,
None,
);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(initial_instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1000.005"))
.quantity(Quantity::from("1.000"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
assert!(engine.order_exists(client_order_id));
clear_order_event_handler_messages(&order_event_handler);
let updated_instrument = crypto_perpetual_with_price_precision(initial_instrument, 3, "0.01");
engine.update_instrument(updated_instrument).unwrap();
assert!(!engine.order_exists(client_order_id));
assert_eq!(
cache.borrow().order(&client_order_id).unwrap().status(),
OrderStatus::Canceled
);
assert!(
get_order_event_handler_messages(&order_event_handler)
.iter()
.any(|event| matches!(event, OrderEventAny::Canceled(canceled) if canceled.client_order_id == client_order_id))
);
}
#[rstest]
fn test_update_instrument_cancels_quantity_incompatible_resting_order(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let mut engine = get_order_matching_engine(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
None,
);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
let mut limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.price(Price::from("1000.00"))
.quantity(Quantity::from("1.001"))
.client_order_id(client_order_id)
.submit(true)
.build();
engine.process_order(&mut limit_order, account_id);
assert!(engine.order_exists(client_order_id));
clear_order_event_handler_messages(&order_event_handler);
let updated_instrument = crypto_perpetual_with_size_precision(instrument_eth_usdt, 2, "0.01");
engine.update_instrument(updated_instrument).unwrap();
assert!(!engine.order_exists(client_order_id));
assert_eq!(
cache.borrow().order(&client_order_id).unwrap().status(),
OrderStatus::Canceled
);
assert!(
get_order_event_handler_messages(&order_event_handler)
.iter()
.any(|event| matches!(event, OrderEventAny::Canceled(canceled) if canceled.client_order_id == client_order_id))
);
}
#[rstest]
fn test_process_bar_drops_precision_mismatch_after_instrument_update(
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
bar_execution: true,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let updated_instrument = crypto_perpetual_with_price_precision(instrument_eth_usdt, 3, "0.001");
engine.update_instrument(updated_instrument).unwrap();
let bar_type = BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL");
let stale_bar = Bar {
bar_type,
open: Price::from("1000.00"),
high: Price::from("1001.00"),
low: Price::from("999.00"),
close: Price::from("1000.50"),
volume: Quantity::from("100.000"),
ts_event: UnixNanos::from(1),
ts_init: UnixNanos::from(1),
};
engine.process_bar(&stale_bar);
assert!(engine.get_core().last.is_none());
}
fn option_contract(
underlying: &str,
venue: &str,
expiration_ns: UnixNanos,
kind: OptionKind,
) -> OptionContract {
let symbol = match kind {
OptionKind::Call => "AAPL211217C00150000",
OptionKind::Put => "AAPL211217P00150000",
};
OptionContract::new(
InstrumentId::from(format!("{symbol}.{venue}").as_str()),
Symbol::from(symbol),
AssetClass::Equity,
Some(Ustr::from(venue)),
Ustr::from(underlying),
kind,
Price::from("149.00"),
Currency::USD(),
UnixNanos::from(0),
expiration_ns,
2,
Price::from("0.01"),
Quantity::from(1),
Quantity::from(1),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
UnixNanos::default(),
UnixNanos::default(),
)
}
fn crypto_option_call_btc(venue: &str, expiration_ns: UnixNanos, strike: Price) -> CryptoOption {
CryptoOption::new(
InstrumentId::from(format!("BTC-OPT-CALL.{venue}").as_str()),
Symbol::from("BTC-OPT-CALL"),
Currency::from("BTC"),
Currency::from("USD"),
Currency::from("BTC"),
false,
OptionKind::Call,
strike,
UnixNanos::from(0),
expiration_ns,
2,
1,
Price::from("0.01"),
Quantity::from("0.1"),
Some(Quantity::from(1)),
Some(Quantity::from(1)),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
UnixNanos::default(),
UnixNanos::default(),
)
}
fn underlying_index(venue: &str) -> IndexInstrument {
IndexInstrument::new(
InstrumentId::from(format!("AAPL.{venue}").as_str()),
Symbol::from("AAPL"),
Currency::USD(),
2,
0,
Price::from("0.01"),
Quantity::from(1),
None,
None,
UnixNanos::default(),
UnixNanos::default(),
)
}
fn underlying_equity(venue: &str) -> Equity {
Equity::new(
InstrumentId::from(format!("AAPL.{venue}").as_str()),
Symbol::from("AAPL"),
None,
Currency::USD(),
2,
Price::from("0.01"),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
UnixNanos::default(),
UnixNanos::default(),
)
}
fn open_long_option_position(
cache: &Rc<RefCell<Cache>>,
instrument: &InstrumentAny,
account_id: AccountId,
quantity: Quantity,
open_price: Price,
) -> Position {
let trader_id = TraderId::from("TRADER-001");
let strategy_id = StrategyId::from("S-001");
let client_order_id = ClientOrderId::from("OPT-OPEN-1");
let venue_order_id = VenueOrderId::from("OPT-OPEN-1");
let position_id = PositionId::from("P-OPT-1");
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(quantity)
.client_order_id(client_order_id)
.submit(true)
.build();
cache
.borrow_mut()
.add_order(order, Some(position_id), None, false)
.unwrap();
let fill = build_order_filled(
trader_id,
strategy_id,
instrument.id(),
client_order_id,
venue_order_id,
account_id,
TradeId::from("OPT-OPEN-1"),
OrderSide::Buy,
OrderType::Market,
quantity,
open_price,
instrument.quote_currency(),
LiquiditySide::Taker,
Some(position_id),
Some(Money::new(0.0, instrument.quote_currency())),
);
let position = Position::new(instrument, fill);
cache
.borrow_mut()
.add_position(&position, OmsType::Netting)
.unwrap();
position
}
#[rstest]
fn test_option_cash_settlement_at_intrinsic_value(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let venue = "OPRA";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::OptionContract(option_contract(
"AAPL",
venue,
expiration_ns,
OptionKind::Call,
));
let underlying = InstrumentAny::IndexInstrument(underlying_index(venue));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
cache
.borrow_mut()
.add_instrument(underlying.clone())
.unwrap();
cache
.borrow_mut()
.add_trade(TradeTick::new(
underlying.id(),
Price::from("160.00"),
Quantity::from(1),
AggressorSide::NoAggressor,
TradeId::from("U-1"),
UnixNanos::from(1),
UnixNanos::from(1),
))
.unwrap();
let position = open_long_option_position(
&cache,
&option,
account_id,
Quantity::from(1),
Price::from("5.00"),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
clock.borrow_mut().set_time(expiration_ns);
let mut engine = OrderMatchingEngine::new(
option.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
engine.iterate(expiration_ns, AggressorSide::NoAggressor);
let fills: Vec<OrderFilled> = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
let settlement_fill = fills
.iter()
.find(|f| {
f.client_order_id
.as_str()
.starts_with(&format!("{venue}-LEG-CASH-"))
})
.expect("Expected cash settlement fill");
assert_eq!(settlement_fill.instrument_id, option.id());
assert_eq!(settlement_fill.order_side, OrderSide::Sell);
assert_eq!(settlement_fill.last_qty, position.quantity);
assert_eq!(settlement_fill.last_px, Price::from("11.00"));
assert_eq!(settlement_fill.position_id, Some(position.id));
}
#[rstest]
fn test_option_physical_settlement_delivers_underlying(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let venue = "OPRA";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::OptionContract(option_contract(
"AAPL",
venue,
expiration_ns,
OptionKind::Call,
));
let underlying = InstrumentAny::Equity(underlying_equity(venue));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
cache
.borrow_mut()
.add_instrument(underlying.clone())
.unwrap();
cache
.borrow_mut()
.add_trade(TradeTick::new(
underlying.id(),
Price::from("160.00"),
Quantity::from(1),
AggressorSide::NoAggressor,
TradeId::from("U-1"),
UnixNanos::from(1),
UnixNanos::from(1),
))
.unwrap();
let position = open_long_option_position(
&cache,
&option,
account_id,
Quantity::from(1),
Price::from("5.00"),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
clock.borrow_mut().set_time(expiration_ns);
let mut engine = OrderMatchingEngine::new(
option.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
engine.iterate(expiration_ns, AggressorSide::NoAggressor);
let fills: Vec<OrderFilled> = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
let close_fill = fills
.iter()
.find(|f| {
f.client_order_id
.as_str()
.starts_with(&format!("{venue}-LEG-EX-"))
&& f.client_order_id.as_str().ends_with("-CLOSE")
})
.expect("Expected option close fill");
let underlying_fill = fills
.iter()
.find(|f| {
f.client_order_id
.as_str()
.starts_with(&format!("{venue}-LEG-EX-"))
&& f.client_order_id.as_str().ends_with("-OPEN")
})
.expect("Expected underlying open fill");
assert_eq!(close_fill.instrument_id, option.id());
assert_eq!(close_fill.order_side, OrderSide::Sell);
assert_eq!(close_fill.last_qty, position.quantity);
assert_eq!(close_fill.last_px, Price::from("0.00"));
assert_eq!(close_fill.position_id, Some(position.id));
assert_eq!(underlying_fill.instrument_id, underlying.id());
assert_eq!(underlying_fill.order_side, OrderSide::Buy);
assert_eq!(underlying_fill.last_px, Price::from("149.00"));
assert_eq!(underlying_fill.position_id, None);
}
#[rstest]
fn test_marketable_resting_limit_at_expiration_boundary_fills_before_close(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let activation = UnixNanos::from(
Utc.with_ymd_and_hms(2021, 9, 10, 0, 0, 0)
.unwrap()
.timestamp_nanos_opt()
.unwrap() as u64,
);
let expiration_ns = UnixNanos::from(
Utc.with_ymd_and_hms(2099, 12, 17, 0, 0, 0)
.unwrap()
.timestamp_nanos_opt()
.unwrap() as u64,
);
let instrument =
InstrumentAny::FuturesContract(futures_contract_es(Some(activation), Some(expiration_ns)));
let clock = Rc::new(RefCell::new(TestClock::new()));
clock
.borrow_mut()
.set_time(UnixNanos::from(activation.as_u64() + 1));
let mut engine = OrderMatchingEngine::new(
instrument.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L2_MBP,
OmsType::Netting,
AccountType::Margin,
clock,
cache.clone(),
OrderMatchingEngineConfig {
use_position_ids: true,
..Default::default()
},
);
let opening_ask = OrderBookDeltaTestBuilder::new(instrument.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("4501.00"),
Quantity::from(10),
1,
))
.build();
engine.process_order_book_delta(&opening_ask).unwrap();
let mut buy_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(1))
.client_order_id(ClientOrderId::from("OPEN-1"))
.submit(true)
.build();
cache
.borrow_mut()
.add_order(buy_order.clone(), None, None, false)
.unwrap();
engine.process_order(&mut buy_order, account_id);
let mut opening_fill = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("Expected an opening fill");
opening_fill.position_id = Some(PositionId::from("P-001"));
let position = Position::new(&instrument, opening_fill);
cache
.borrow_mut()
.add_position(&position, OmsType::Netting)
.unwrap();
clear_order_event_handler_messages(&order_event_handler);
let close_client_order_id = ClientOrderId::from("CLOSE-LIMIT-1");
let mut close_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Sell)
.price(Price::from("4502.00"))
.quantity(Quantity::from(1))
.client_order_id(close_client_order_id)
.submit(true)
.build();
engine.process_order(&mut close_order, account_id);
let crossing_bid = OrderBookDeltaTestBuilder::new(instrument.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("4503.00"),
Quantity::from(1),
2,
))
.ts_init(expiration_ns)
.build();
engine.process_order_book_delta(&crossing_bid).unwrap();
let messages = get_order_event_handler_messages(&order_event_handler);
let limit_fill_idx = messages
.iter()
.position(|e| {
matches!(
e,
OrderEventAny::Filled(f) if f.client_order_id == close_client_order_id
)
})
.expect("Expected resting limit to fill at boundary tick");
let limit_fill = match &messages[limit_fill_idx] {
OrderEventAny::Filled(f) => f,
_ => unreachable!(),
};
assert_eq!(limit_fill.last_qty, Quantity::from(1));
let expiration_event_idx = messages.iter().position(|e| {
matches!(
e,
OrderEventAny::Filled(f) if f.client_order_id.as_str().starts_with("EXPIRATION-")
)
});
if let Some(exp_idx) = expiration_event_idx {
assert!(
limit_fill_idx < exp_idx,
"Resting limit must fill before EXPIRATION close fires"
);
}
}
fn run_otm_expiry_case(kind: OptionKind, spot: Price, account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let venue = "OPRA";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::OptionContract(option_contract("AAPL", venue, expiration_ns, kind));
let underlying = InstrumentAny::Equity(underlying_equity(venue));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
cache
.borrow_mut()
.add_instrument(underlying.clone())
.unwrap();
cache
.borrow_mut()
.add_trade(TradeTick::new(
underlying.id(),
spot,
Quantity::from(1),
AggressorSide::NoAggressor,
TradeId::from("U-1"),
UnixNanos::from(1),
UnixNanos::from(1),
))
.unwrap();
let position = open_long_option_position(
&cache,
&option,
account_id,
Quantity::from(1),
Price::from("5.00"),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
clock.borrow_mut().set_time(expiration_ns);
let mut engine = OrderMatchingEngine::new(
option.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
engine.iterate(expiration_ns, AggressorSide::NoAggressor);
let fills: Vec<OrderFilled> = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
let otm_fill = fills
.iter()
.find(|f| {
f.client_order_id
.as_str()
.starts_with(&format!("{venue}-LEG-OTM-"))
})
.expect("Expected OTM expiry fill");
assert_eq!(otm_fill.instrument_id, option.id());
assert_eq!(otm_fill.order_side, OrderSide::Sell);
assert_eq!(otm_fill.last_qty, position.quantity);
assert_eq!(otm_fill.last_px, Price::from("0.00"));
assert_eq!(otm_fill.position_id, Some(position.id));
assert!(
!fills
.iter()
.any(|f| f.client_order_id.as_str().contains("-LEG-CASH-")
|| f.client_order_id.as_str().contains("-LEG-EX-")),
"OTM path must not emit cash or physical settlement fills"
);
}
#[rstest]
#[case::call_otm(OptionKind::Call, Price::from("140.00"))]
#[case::put_otm(OptionKind::Put, Price::from("160.00"))]
fn test_option_otm_expiry_closes_at_zero(
#[case] kind: OptionKind,
#[case] spot: Price,
account_id: AccountId,
) {
run_otm_expiry_case(kind, spot, account_id);
}
#[rstest]
fn test_option_cash_settlement_put_pays_strike_minus_spot(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let venue = "OPRA";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::OptionContract(option_contract(
"AAPL",
venue,
expiration_ns,
OptionKind::Put,
));
let underlying = InstrumentAny::IndexInstrument(underlying_index(venue));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
cache
.borrow_mut()
.add_instrument(underlying.clone())
.unwrap();
cache
.borrow_mut()
.add_trade(TradeTick::new(
underlying.id(),
Price::from("140.00"),
Quantity::from(1),
AggressorSide::NoAggressor,
TradeId::from("U-1"),
UnixNanos::from(1),
UnixNanos::from(1),
))
.unwrap();
let position = open_long_option_position(
&cache,
&option,
account_id,
Quantity::from(1),
Price::from("5.00"),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
clock.borrow_mut().set_time(expiration_ns);
let mut engine = OrderMatchingEngine::new(
option.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
engine.iterate(expiration_ns, AggressorSide::NoAggressor);
let settlement_fill = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.find_map(|e| match e {
OrderEventAny::Filled(f)
if f.client_order_id
.as_str()
.starts_with(&format!("{venue}-LEG-CASH-")) =>
{
Some(f)
}
_ => None,
})
.expect("Expected cash settlement fill");
assert_eq!(settlement_fill.instrument_id, option.id());
assert_eq!(settlement_fill.order_side, OrderSide::Sell);
assert_eq!(settlement_fill.last_qty, position.quantity);
assert_eq!(settlement_fill.last_px, Price::from("9.00"));
assert_eq!(settlement_fill.position_id, Some(position.id));
}
#[rstest]
fn test_option_physical_settlement_put_flips_underlying_side(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let venue = "OPRA";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::OptionContract(option_contract(
"AAPL",
venue,
expiration_ns,
OptionKind::Put,
));
let underlying = InstrumentAny::Equity(underlying_equity(venue));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
cache
.borrow_mut()
.add_instrument(underlying.clone())
.unwrap();
cache
.borrow_mut()
.add_trade(TradeTick::new(
underlying.id(),
Price::from("140.00"),
Quantity::from(1),
AggressorSide::NoAggressor,
TradeId::from("U-1"),
UnixNanos::from(1),
UnixNanos::from(1),
))
.unwrap();
let position = open_long_option_position(
&cache,
&option,
account_id,
Quantity::from(1),
Price::from("5.00"),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
clock.borrow_mut().set_time(expiration_ns);
let mut engine = OrderMatchingEngine::new(
option,
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
engine.iterate(expiration_ns, AggressorSide::NoAggressor);
let fills: Vec<OrderFilled> = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.filter_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
let underlying_fill = fills
.iter()
.find(|f| {
f.client_order_id
.as_str()
.starts_with(&format!("{venue}-LEG-EX-"))
&& f.client_order_id.as_str().ends_with("-OPEN")
})
.expect("Expected underlying open fill");
assert_eq!(underlying_fill.instrument_id, underlying.id());
assert_eq!(underlying_fill.order_side, OrderSide::Sell);
assert_eq!(underlying_fill.last_px, Price::from("149.00"));
assert_eq!(underlying_fill.last_qty, position.quantity);
assert_eq!(underlying_fill.position_id, None);
}
#[rstest]
fn test_check_instrument_expiration_fallback_uses_book(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let activation = UnixNanos::from(
Utc.with_ymd_and_hms(2021, 9, 10, 0, 0, 0)
.unwrap()
.timestamp_nanos_opt()
.unwrap() as u64,
);
let expiration_ns = UnixNanos::from(
Utc.with_ymd_and_hms(2099, 12, 17, 0, 0, 0)
.unwrap()
.timestamp_nanos_opt()
.unwrap() as u64,
);
let instrument =
InstrumentAny::FuturesContract(futures_contract_es(Some(activation), Some(expiration_ns)));
let clock = Rc::new(RefCell::new(TestClock::new()));
clock
.borrow_mut()
.set_time(UnixNanos::from(activation.as_u64() + 1));
let fee_model =
FeeModelAny::Fixed(FixedFeeModel::new(Money::new(0.0, Currency::USD()), None).unwrap());
let mut engine = OrderMatchingEngine::new(
instrument.clone(),
1,
FillModelHandle::default(),
fee_model.into(),
BookType::L2_MBP,
OmsType::Netting,
AccountType::Margin,
clock,
cache.clone(),
OrderMatchingEngineConfig {
use_position_ids: true,
..Default::default()
},
);
let opening_ask = OrderBookDeltaTestBuilder::new(instrument.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("4501.00"),
Quantity::from(10),
1,
))
.build();
engine.process_order_book_delta(&opening_ask).unwrap();
let opening_bid = OrderBookDeltaTestBuilder::new(instrument.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("4499.00"),
Quantity::from(10),
2,
))
.build();
engine.process_order_book_delta(&opening_bid).unwrap();
let mut buy_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(1))
.client_order_id(ClientOrderId::from("OPEN-1"))
.submit(true)
.build();
cache
.borrow_mut()
.add_order(buy_order.clone(), None, None, false)
.unwrap();
engine.process_order(&mut buy_order, account_id);
let mut opening_fill = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("Expected an opening fill");
opening_fill.position_id = Some(PositionId::from("P-001"));
let position = Position::new(&instrument, opening_fill);
cache
.borrow_mut()
.add_position(&position, OmsType::Netting)
.unwrap();
clear_order_event_handler_messages(&order_event_handler);
let trigger_delta = OrderBookDeltaTestBuilder::new(instrument.id())
.book_action(BookAction::Update)
.book_order(BookOrder::new(
OrderSide::Buy,
Price::from("4499.00"),
Quantity::from(10),
2,
))
.ts_init(expiration_ns)
.build();
engine.process_order_book_delta(&trigger_delta).unwrap();
let messages = get_order_event_handler_messages(&order_event_handler);
let accepted_idx = messages
.iter()
.position(|e| {
matches!(
e,
OrderEventAny::Accepted(a) if a.client_order_id.as_str().starts_with("EXPIRATION-")
)
})
.expect("Expected OrderAccepted for the EXPIRATION close order");
let fill_idx = messages
.iter()
.position(|e| {
matches!(
e,
OrderEventAny::Filled(f) if f.client_order_id.as_str().starts_with("EXPIRATION-")
)
})
.expect("Expected OrderFilled for the EXPIRATION close order");
assert!(
accepted_idx < fill_idx,
"OrderAccepted must precede OrderFilled for fallback fill_market_order path"
);
let fill = match &messages[fill_idx] {
OrderEventAny::Filled(f) => f,
_ => unreachable!(),
};
assert_eq!(fill.order_side, OrderSide::Sell);
assert_eq!(fill.last_qty, Quantity::from(1));
assert_eq!(fill.last_px, Price::from("4499.00"));
let _ = position;
}
#[rstest]
fn test_process_option_expiry_no_positions_is_noop(account_id: AccountId) {
let _ = account_id;
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let venue = "OPRA";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::OptionContract(option_contract(
"AAPL",
venue,
expiration_ns,
OptionKind::Call,
));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
let clock = Rc::new(RefCell::new(TestClock::new()));
clock.borrow_mut().set_time(expiration_ns);
let mut engine = OrderMatchingEngine::new(
option,
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
engine.iterate(expiration_ns, AggressorSide::NoAggressor);
let messages = get_order_event_handler_messages(&order_event_handler);
assert!(
messages
.iter()
.all(|e| !matches!(e, OrderEventAny::Filled(_))),
"No fills should be emitted when no positions exist"
);
}
#[rstest]
fn test_process_option_expiry_missing_underlying_instrument_is_noop(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let venue = "OPRA";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::OptionContract(option_contract(
"AAPL",
venue,
expiration_ns,
OptionKind::Call,
));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
let _position = open_long_option_position(
&cache,
&option,
account_id,
Quantity::from(1),
Price::from("5.00"),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
clock.borrow_mut().set_time(expiration_ns);
let mut engine = OrderMatchingEngine::new(
option,
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
engine.iterate(expiration_ns, AggressorSide::NoAggressor);
let settlement_fills = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.filter(|e| {
matches!(
e,
OrderEventAny::Filled(f)
if f.client_order_id.as_str().contains("-LEG-")
)
})
.count();
assert_eq!(
settlement_fills, 0,
"No settlement fills should be emitted when underlying instrument is absent"
);
}
#[rstest]
fn test_process_option_expiry_missing_underlying_price_is_noop(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let venue = "OPRA";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::OptionContract(option_contract(
"AAPL",
venue,
expiration_ns,
OptionKind::Call,
));
let underlying = InstrumentAny::Equity(underlying_equity(venue));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
cache.borrow_mut().add_instrument(underlying).unwrap();
let _position = open_long_option_position(
&cache,
&option,
account_id,
Quantity::from(1),
Price::from("5.00"),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
clock.borrow_mut().set_time(expiration_ns);
let mut engine = OrderMatchingEngine::new(
option,
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
engine.iterate(expiration_ns, AggressorSide::NoAggressor);
let settlement_fills = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.filter(|e| {
matches!(
e,
OrderEventAny::Filled(f)
if f.client_order_id.as_str().contains("-LEG-")
)
})
.count();
assert_eq!(
settlement_fills, 0,
"No settlement fills should be emitted without an underlying last price"
);
}
#[rstest]
fn test_check_instrument_expiration_idempotent_after_processed(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let venue = "OPRA";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::OptionContract(option_contract(
"AAPL",
venue,
expiration_ns,
OptionKind::Call,
));
let underlying = InstrumentAny::IndexInstrument(underlying_index(venue));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
cache
.borrow_mut()
.add_instrument(underlying.clone())
.unwrap();
cache
.borrow_mut()
.add_trade(TradeTick::new(
underlying.id(),
Price::from("160.00"),
Quantity::from(1),
AggressorSide::NoAggressor,
TradeId::from("U-1"),
UnixNanos::from(1),
UnixNanos::from(1),
))
.unwrap();
let _position = open_long_option_position(
&cache,
&option,
account_id,
Quantity::from(1),
Price::from("5.00"),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
clock.borrow_mut().set_time(expiration_ns);
let mut engine = OrderMatchingEngine::new(
option,
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
engine.iterate(expiration_ns, AggressorSide::NoAggressor);
let fills_first: usize = get_order_event_handler_messages(&order_event_handler)
.iter()
.filter(|e| matches!(e, OrderEventAny::Filled(_)))
.count();
assert!(fills_first >= 1, "First iterate must emit settlement fill");
clear_order_event_handler_messages(&order_event_handler);
engine.iterate(
UnixNanos::from(expiration_ns.as_u64() + 1),
AggressorSide::NoAggressor,
);
let fills_second: usize = get_order_event_handler_messages(&order_event_handler)
.iter()
.filter(|e| matches!(e, OrderEventAny::Filled(_)))
.count();
assert_eq!(
fills_second, 0,
"Second iterate must be a no-op once expiration_processed is set"
);
}
#[rstest]
fn test_check_instrument_expiration_uses_close_price_fallback(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let activation = UnixNanos::from(
Utc.with_ymd_and_hms(2021, 9, 10, 0, 0, 0)
.unwrap()
.timestamp_nanos_opt()
.unwrap() as u64,
);
let expiration_ns = UnixNanos::from(
Utc.with_ymd_and_hms(2099, 12, 17, 0, 0, 0)
.unwrap()
.timestamp_nanos_opt()
.unwrap() as u64,
);
let instrument =
InstrumentAny::FuturesContract(futures_contract_es(Some(activation), Some(expiration_ns)));
let clock = Rc::new(RefCell::new(TestClock::new()));
clock
.borrow_mut()
.set_time(UnixNanos::from(activation.as_u64() + 1));
let fee_model =
FeeModelAny::Fixed(FixedFeeModel::new(Money::new(0.0, Currency::USD()), None).unwrap());
let mut engine = OrderMatchingEngine::new(
instrument.clone(),
1,
FillModelHandle::default(),
fee_model.into(),
BookType::L2_MBP,
OmsType::Netting,
AccountType::Margin,
clock,
cache.clone(),
OrderMatchingEngineConfig {
use_position_ids: true,
..Default::default()
},
);
let opening_ask = OrderBookDeltaTestBuilder::new(instrument.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("4501.00"),
Quantity::from(10),
1,
))
.build();
engine.process_order_book_delta(&opening_ask).unwrap();
let mut buy_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(1))
.client_order_id(ClientOrderId::from("OPEN-1"))
.submit(true)
.build();
cache
.borrow_mut()
.add_order(buy_order.clone(), None, None, false)
.unwrap();
engine.process_order(&mut buy_order, account_id);
let mut opening_fill = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.expect("Expected opening fill");
opening_fill.position_id = Some(PositionId::from("P-001"));
let position = Position::new(&instrument, opening_fill);
cache
.borrow_mut()
.add_position(&position, OmsType::Netting)
.unwrap();
clear_order_event_handler_messages(&order_event_handler);
let close_price = Price::from("4550.00");
let close = InstrumentClose::new(
instrument.id(),
close_price,
InstrumentCloseType::ContractExpired,
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_instrument_close(close);
let fill = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.find_map(|e| match e {
OrderEventAny::Filled(f) if f.client_order_id.as_str().starts_with("EXPIRATION-") => {
Some(f)
}
_ => None,
})
.expect("Expected EXPIRATION close fill from close_price fallback");
assert_eq!(fill.last_px, close_price);
let _ = position;
}
#[rstest]
fn test_binary_option_pending_resolution_then_instrument_close_settles_position(
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let instrument = InstrumentAny::BinaryOption(binary_option());
cache
.borrow_mut()
.add_instrument(instrument.clone())
.unwrap();
let activation_ns = instrument.activation_ns().unwrap();
let expiration_ns = instrument.expiration_ns().unwrap();
let clock = Rc::new(RefCell::new(TestClock::new()));
clock
.borrow_mut()
.set_time(UnixNanos::from(activation_ns.as_u64() + 1));
let mut engine = OrderMatchingEngine::new(
instrument.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L2_MBP,
OmsType::Netting,
AccountType::Margin,
clock.clone(),
cache.clone(),
OrderMatchingEngineConfig {
use_position_ids: true,
..Default::default()
},
);
let opening_ask = OrderBookDeltaTestBuilder::new(instrument.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("0.400"),
Quantity::from("10.00"),
1,
))
.build();
engine.process_order_book_delta(&opening_ask).unwrap();
let mut opening_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.00"))
.client_order_id(ClientOrderId::from("OPEN-BO-1"))
.submit(true)
.build();
cache
.borrow_mut()
.add_order(opening_order.clone(), None, None, false)
.unwrap();
engine.process_order(&mut opening_order, account_id);
let mut opening_fill = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.find_map(|event| match event {
OrderEventAny::Filled(fill) => Some(fill),
_ => None,
})
.expect("expected opening fill");
opening_fill.position_id = Some(PositionId::from("P-BO-1"));
let position = Position::new(&instrument, opening_fill);
cache
.borrow_mut()
.add_position(&position, OmsType::Netting)
.unwrap();
clear_order_event_handler_messages(&order_event_handler);
let mut resting_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("0.050"))
.quantity(Quantity::from("1.00"))
.client_order_id(ClientOrderId::from("REST-BO-1"))
.submit(true)
.build();
cache
.borrow_mut()
.add_order(resting_order.clone(), None, None, false)
.unwrap();
engine.process_order(&mut resting_order, account_id);
clear_order_event_handler_messages(&order_event_handler);
clock
.borrow_mut()
.set_time(UnixNanos::from(expiration_ns.as_u64() + 1));
let mut probe_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("0.050"))
.quantity(Quantity::from("1.00"))
.client_order_id(ClientOrderId::from("PROBE-BO-1"))
.submit(true)
.build();
cache
.borrow_mut()
.add_order(probe_order.clone(), None, None, false)
.unwrap();
engine.process_order(&mut probe_order, account_id);
let events = get_order_event_handler_messages(&order_event_handler);
assert!(
events.iter().any(|event| matches!(
event,
OrderEventAny::Canceled(c) if c.client_order_id == ClientOrderId::from("REST-BO-1")
)),
"expected resting order cancellation when entering pending resolution"
);
let reject = events
.iter()
.find_map(|event| match event {
OrderEventAny::Rejected(rejected)
if rejected.client_order_id == ClientOrderId::from("PROBE-BO-1") =>
{
Some(rejected)
}
_ => None,
})
.expect("expected rejection for probe order submitted after expiry");
assert!(
reject.reason.as_str().contains("pending resolution"),
"expected pending resolution rejection, found: {}",
reject.reason,
);
clear_order_event_handler_messages(&order_event_handler);
let close = InstrumentClose::new(
instrument.id(),
Price::from("1.000"),
InstrumentCloseType::ContractExpired,
UnixNanos::from(2),
UnixNanos::from(2),
);
engine.process_instrument_close(close);
let settlement_fill = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.find_map(|event| match event {
OrderEventAny::Filled(fill)
if fill.client_order_id.as_str().starts_with("EXPIRATION-") =>
{
Some(fill)
}
_ => None,
})
.expect("expected settlement fill from instrument close");
assert_eq!(settlement_fill.last_px, Price::from("1.000"));
let _ = position;
}
#[rstest]
fn test_binary_option_expiration_check_uses_engine_clock_not_order_ts_init(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let instrument = InstrumentAny::BinaryOption(binary_option());
cache
.borrow_mut()
.add_instrument(instrument.clone())
.unwrap();
let activation_ns = instrument.activation_ns().unwrap();
let expiration_ns = instrument.expiration_ns().unwrap();
let clock = Rc::new(RefCell::new(TestClock::new()));
clock
.borrow_mut()
.set_time(UnixNanos::from(activation_ns.as_u64() + 1));
let mut engine = OrderMatchingEngine::new(
instrument.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L2_MBP,
OmsType::Netting,
AccountType::Margin,
clock,
cache.clone(),
OrderMatchingEngineConfig {
use_position_ids: true,
..Default::default()
},
);
let opening_ask = OrderBookDeltaTestBuilder::new(instrument.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("0.400"),
Quantity::from("10.00"),
1,
))
.build();
engine.process_order_book_delta(&opening_ask).unwrap();
let mut order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.00"))
.client_order_id(ClientOrderId::from("CLOCK-NOT-TSINIT"))
.ts_init(UnixNanos::from(expiration_ns.as_u64() + 100))
.submit(true)
.build();
cache
.borrow_mut()
.add_order(order.clone(), None, None, false)
.unwrap();
engine.process_order(&mut order, account_id);
let events = get_order_event_handler_messages(&order_event_handler);
assert!(
events.iter().any(|event| matches!(
event,
OrderEventAny::Filled(fill)
if fill.client_order_id == ClientOrderId::from("CLOCK-NOT-TSINIT")
)),
"order.ts_init should not cause expiry rejection when engine clock is still pre-expiry",
);
assert!(
!events.iter().any(|event| matches!(
event,
OrderEventAny::Rejected(rejected)
if rejected.client_order_id == ClientOrderId::from("CLOCK-NOT-TSINIT")
)),
"pre-expiry submission should not be rejected solely because order.ts_init is later",
);
}
#[rstest]
fn test_crypto_option_cash_settlement(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let venue = "DERIBIT";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let strike = Price::from("50000.00");
let option = InstrumentAny::CryptoOption(crypto_option_call_btc(venue, expiration_ns, strike));
let underlying = InstrumentAny::IndexInstrument(IndexInstrument::new(
InstrumentId::from(format!("BTC.{venue}").as_str()),
Symbol::from("BTC"),
Currency::USD(),
2,
0,
Price::from("0.01"),
Quantity::from(1),
None,
None,
UnixNanos::default(),
UnixNanos::default(),
));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
cache
.borrow_mut()
.add_instrument(underlying.clone())
.unwrap();
cache
.borrow_mut()
.add_trade(TradeTick::new(
underlying.id(),
Price::from("51000.00"),
Quantity::from(1),
AggressorSide::NoAggressor,
TradeId::from("U-1"),
UnixNanos::from(1),
UnixNanos::from(1),
))
.unwrap();
let position = open_long_option_position(
&cache,
&option,
account_id,
Quantity::from("1"),
Price::from("100.00"),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
clock.borrow_mut().set_time(expiration_ns);
let mut engine = OrderMatchingEngine::new(
option.clone(),
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
engine.iterate(expiration_ns, AggressorSide::NoAggressor);
let settlement_fill = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.find_map(|e| match e {
OrderEventAny::Filled(f)
if f.client_order_id
.as_str()
.starts_with(&format!("{venue}-LEG-CASH-")) =>
{
Some(f)
}
_ => None,
})
.expect("Expected CryptoOption cash settlement fill");
assert_eq!(settlement_fill.instrument_id, option.id());
assert_eq!(settlement_fill.order_side, OrderSide::Sell);
assert_eq!(settlement_fill.last_qty, position.quantity);
assert_eq!(settlement_fill.last_px, Price::from("1000.00"));
assert_eq!(settlement_fill.position_id, Some(position.id));
}
#[rstest]
fn test_capped_option_fee_uses_underlying_mid_quote(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let venue = "DERIBIT";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::CryptoOption(crypto_option_call_btc(
venue,
expiration_ns,
Price::from("50000.00"),
));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
cache
.borrow_mut()
.add_quote(QuoteTick::new(
InstrumentId::from(format!("BTC.{venue}").as_str()),
Price::from("49990.00"),
Price::from("50010.00"),
Quantity::from(1),
Quantity::from(1),
UnixNanos::from(1),
UnixNanos::from(1),
))
.unwrap();
let fee_model = FeeModelAny::CappedOption(
CappedOptionFeeModel::new(Some(dec!(0.0001)), Some(dec!(0.0003)), None).unwrap(),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
let mut engine = OrderMatchingEngine::new(
option.clone(),
1,
FillModelHandle::default(),
fee_model.into(),
BookType::L2_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
let ask = OrderBookDeltaTestBuilder::new(option.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("200.00"),
Quantity::from("1.0"),
1,
))
.build();
engine.process_order_book_delta(&ask).unwrap();
let mut order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(option.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.0"))
.client_order_id(ClientOrderId::from("DERIBIT-FEE-MID-QUOTE"))
.submit(true)
.build();
engine.process_order(&mut order, account_id);
let fill = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.find_map(|event| match event {
OrderEventAny::Filled(fill)
if fill.client_order_id == ClientOrderId::from("DERIBIT-FEE-MID-QUOTE") =>
{
Some(fill)
}
_ => None,
})
.expect("expected option fill");
let commission = fill.commission.expect("expected commission");
assert_eq!(commission.currency, Currency::USD());
assert_eq!(commission.as_decimal(), dec!(15.00));
}
#[rstest]
fn test_capped_option_fee_uses_option_greeks_underlying_price(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let venue = "DERIBIT";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::CryptoOption(crypto_option_call_btc(
venue,
expiration_ns,
Price::from("50000.00"),
));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
cache.borrow_mut().add_option_greeks(OptionGreeks {
instrument_id: option.id(),
underlying_price: Some(50000.0),
ts_event: UnixNanos::from(1),
ts_init: UnixNanos::from(1),
..Default::default()
});
let fee_model = FeeModelAny::CappedOption(
CappedOptionFeeModel::new(Some(dec!(0.0001)), Some(dec!(0.0003)), None).unwrap(),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
let mut engine = OrderMatchingEngine::new(
option.clone(),
1,
FillModelHandle::default(),
fee_model.into(),
BookType::L2_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
let ask = OrderBookDeltaTestBuilder::new(option.id())
.book_action(BookAction::Add)
.book_order(BookOrder::new(
OrderSide::Sell,
Price::from("200.00"),
Quantity::from("1.0"),
1,
))
.build();
engine.process_order_book_delta(&ask).unwrap();
let mut order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(option.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.0"))
.client_order_id(ClientOrderId::from("DERIBIT-FEE-OPTION-GREEKS"))
.submit(true)
.build();
engine.process_order(&mut order, account_id);
let fill = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.find_map(|event| match event {
OrderEventAny::Filled(fill)
if fill.client_order_id == ClientOrderId::from("DERIBIT-FEE-OPTION-GREEKS") =>
{
Some(fill)
}
_ => None,
})
.expect("expected option fill");
let commission = fill.commission.expect("expected commission");
assert_eq!(commission.currency, Currency::USD());
assert_eq!(commission.as_decimal(), dec!(15.00));
}
#[rstest]
fn test_option_cash_settlement_with_custom_settlement_price(account_id: AccountId) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
let venue = "OPRA";
let expiration_ns = UnixNanos::from(2_000_000_000_000_000_000u64);
let option = InstrumentAny::OptionContract(option_contract(
"AAPL",
venue,
expiration_ns,
OptionKind::Call,
));
let underlying = InstrumentAny::IndexInstrument(underlying_index(venue));
cache.borrow_mut().add_instrument(option.clone()).unwrap();
cache
.borrow_mut()
.add_instrument(underlying.clone())
.unwrap();
cache
.borrow_mut()
.add_trade(TradeTick::new(
underlying.id(),
Price::from("160.00"),
Quantity::from(1),
AggressorSide::NoAggressor,
TradeId::from("U-1"),
UnixNanos::from(1),
UnixNanos::from(1),
))
.unwrap();
let _position = open_long_option_position(
&cache,
&option,
account_id,
Quantity::from(1),
Price::from("5.00"),
);
let clock = Rc::new(RefCell::new(TestClock::new()));
clock.borrow_mut().set_time(expiration_ns);
let mut engine = OrderMatchingEngine::new(
option,
1,
FillModelHandle::default(),
FeeModelAny::default().into(),
BookType::L1_MBP,
OmsType::Netting,
AccountType::Cash,
clock,
cache,
OrderMatchingEngineConfig::default(),
);
let custom = Price::from("7.50");
engine.set_settlement_price(custom);
engine.iterate(expiration_ns, AggressorSide::NoAggressor);
let settlement_fill = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.find_map(|e| match e {
OrderEventAny::Filled(f)
if f.client_order_id
.as_str()
.starts_with(&format!("{venue}-LEG-CASH-")) =>
{
Some(f)
}
_ => None,
})
.expect("Expected cash settlement fill");
assert_eq!(settlement_fill.last_px, custom);
}
#[rstest]
#[case(OrderSide::Buy, "1010.00", "1010.01")]
#[case(OrderSide::Sell, "1000.00", "999.99")]
fn test_l1_market_order_slips_remainder_through_next_tick(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] side: OrderSide,
#[case] top_of_book_px: &str,
#[case] slip_px: &str,
) {
let mut engine = get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, None);
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Price::from("1010.00"),
Quantity::from("0.500"),
Quantity::from("0.500"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(side)
.quantity(Quantity::from("1.500"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let fills: Vec<OrderFilled> = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.filter_map(|event| match event {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
assert_eq!(fills.len(), 2);
assert_eq!(fills[0].last_px, Price::from(top_of_book_px));
assert_eq!(fills[0].last_qty, Quantity::from("0.500"));
assert_eq!(fills[1].last_px, Price::from(slip_px));
assert_eq!(fills[1].last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_reduce_only_l1_market_order_slip_caps_remaining_position(
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let cache = Rc::new(RefCell::new(Cache::default()));
let order_event_handler = order_event_handler_with_cache(cache.clone());
cache
.borrow_mut()
.add_instrument(instrument_eth_usdt.clone())
.unwrap();
let mut engine = get_order_matching_engine(
instrument_eth_usdt.clone(),
None,
Some(cache.clone()),
None,
None,
);
let position_id = PositionId::new(
format!(
"{}-{}",
instrument_eth_usdt.id(),
StrategyId::test_default()
)
.as_str(),
);
let opening_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.000"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-OPEN"))
.build();
let opening_fill = build_order_filled(
opening_order.trader_id(),
opening_order.strategy_id(),
opening_order.instrument_id(),
opening_order.client_order_id(),
VenueOrderId::from("V-OPEN"),
account_id,
TradeId::new("E-OPEN"),
opening_order.order_side(),
opening_order.order_type(),
opening_order.quantity(),
Price::from("1000.00"),
instrument_eth_usdt.quote_currency(),
LiquiditySide::Taker,
Some(position_id),
None,
);
let position = Position::new(&instrument_eth_usdt, opening_fill);
cache
.borrow_mut()
.add_position(&position, OmsType::Netting)
.unwrap();
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Price::from("1010.00"),
Quantity::from("0.500"),
Quantity::from("0.500"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-RO");
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Sell)
.quantity(Quantity::from("1.500"))
.client_order_id(client_order_id)
.reduce_only(true)
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<OrderFilled> = messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(fill) if fill.client_order_id == client_order_id => Some(*fill),
_ => None,
})
.collect();
let updated = messages
.iter()
.find_map(|event| match event {
OrderEventAny::Updated(updated) if updated.client_order_id == client_order_id => {
Some(updated)
}
_ => None,
})
.expect("Expected reduce-only order quantity update");
assert_eq!(fills.len(), 2);
assert_eq!(fills[0].last_px, Price::from("1000.00"));
assert_eq!(fills[0].last_qty, Quantity::from("0.500"));
assert_eq!(fills[1].last_px, Price::from("999.99"));
assert_eq!(fills[1].last_qty, Quantity::from("0.500"));
assert_eq!(updated.quantity, Quantity::from("1.000"));
let cache_ref = cache.borrow();
let order = cache_ref
.order(&client_order_id)
.expect("Expected reduce-only order in cache");
assert_eq!(order.quantity(), Quantity::from("1.000"));
assert_eq!(order.filled_qty(), Quantity::from("1.000"));
assert_eq!(order.status(), OrderStatus::Filled);
}
#[rstest]
#[case(OrderSide::Buy, "1010.00")]
#[case(OrderSide::Sell, "1000.00")]
fn test_l1_market_order_slip_respects_protection_boundary(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] side: OrderSide,
#[case] top_of_book_px: &str,
) {
let config = OrderMatchingEngineConfig::builder()
.price_protection_points(0u32)
.build();
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Price::from("1010.00"),
Quantity::from("0.500"),
Quantity::from("0.500"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(side)
.quantity(Quantity::from("1.500"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let fills: Vec<OrderFilled> = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.filter_map(|event| match event {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
assert_eq!(fills.len(), 1);
assert_eq!(fills[0].last_px, Price::from(top_of_book_px));
assert_eq!(fills[0].last_qty, Quantity::from("0.500"));
}
#[rstest]
#[case(OrderSide::Buy, "1010.00", "1010.01")]
#[case(OrderSide::Sell, "1000.00", "999.99")]
fn test_l1_market_order_slip_allowed_at_protection_boundary(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
#[case] side: OrderSide,
#[case] top_of_book_px: &str,
#[case] slip_px: &str,
) {
let config = OrderMatchingEngineConfig::builder()
.price_protection_points(1u32)
.build();
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Price::from("1010.00"),
Quantity::from("0.500"),
Quantity::from("0.500"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(side)
.quantity(Quantity::from("1.500"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let fills: Vec<OrderFilled> = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.filter_map(|event| match event {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
assert_eq!(fills.len(), 2);
assert_eq!(fills[0].last_px, Price::from(top_of_book_px));
assert_eq!(fills[0].last_qty, Quantity::from("0.500"));
assert_eq!(fills[1].last_px, Price::from(slip_px));
assert_eq!(fills[1].last_qty, Quantity::from("1.000"));
}
#[rstest]
fn test_l1_ioc_market_order_cancels_remainder_instead_of_slipping(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let mut engine = get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, None);
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Price::from("1010.00"),
Quantity::from("0.500"),
Quantity::from("0.500"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
let mut market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("1.500"))
.time_in_force(TimeInForce::Ioc)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut market_order, account_id);
let saved_messages = get_order_event_handler_messages(&order_event_handler);
let fills: Vec<&OrderFilled> = saved_messages
.iter()
.filter_map(|event| match event {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
let canceled_count = saved_messages
.iter()
.filter(|event| matches!(event, OrderEventAny::Canceled(_)))
.count();
assert_eq!(fills.len(), 1);
assert_eq!(fills[0].last_px, Price::from("1010.00"));
assert_eq!(fills[0].last_qty, Quantity::from("0.500"));
assert_eq!(canceled_count, 1);
}
#[rstest]
fn test_l1_stop_market_order_slips_remainder_after_trigger(
order_event_handler: TypedIntoMessageSavingHandler<OrderEventAny>,
account_id: AccountId,
instrument_eth_usdt: InstrumentAny,
) {
let config = OrderMatchingEngineConfig {
reject_stop_orders: false,
..Default::default()
};
let mut engine =
get_order_matching_engine(instrument_eth_usdt.clone(), None, None, None, Some(config));
let quote = QuoteTick::new(
instrument_eth_usdt.id(),
Price::from("1000.00"),
Price::from("1010.00"),
Quantity::from("0.500"),
Quantity::from("0.500"),
UnixNanos::default(),
UnixNanos::default(),
);
engine.process_quote_tick("e);
let mut stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_eth_usdt.id())
.side(OrderSide::Buy)
.trigger_price(Price::from("1005.00"))
.quantity(Quantity::from("1.500"))
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.submit(true)
.build();
engine.process_order(&mut stop_order, account_id);
let fills: Vec<OrderFilled> = get_order_event_handler_messages(&order_event_handler)
.into_iter()
.filter_map(|event| match event {
OrderEventAny::Filled(f) => Some(f),
_ => None,
})
.collect();
assert_eq!(fills.len(), 2);
assert_eq!(fills[0].last_px, Price::from("1010.00"));
assert_eq!(fills[0].last_qty, Quantity::from("0.500"));
assert_eq!(fills[1].last_px, Price::from("1010.01"));
assert_eq!(fills[1].last_qty, Quantity::from("1.000"));
}