Skip to content

Commit

Permalink
Port process cancel for OrderMatchingEngine in Rust (#2231)
Browse files Browse the repository at this point in the history
  • Loading branch information
filipmacek authored Jan 23, 2025
1 parent e384c6f commit fafcc30
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 4 deletions.
37 changes: 35 additions & 2 deletions nautilus_core/backtest/src/exchange.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,8 +495,41 @@ impl SimulatedExchange {
todo!("reset")
}

pub fn process_trading_command(&mut self, _command: TradingCommand) {
todo!("process trading command")
pub fn process_trading_command(&mut self, command: TradingCommand) {
if let Some(matching_engine) = self.matching_engines.get_mut(&command.instrument_id()) {
let account_id = if let Some(exec_client) = &self.exec_client {
exec_client.account_id
} else {
panic!("Execution client should be initialized");
};
match command {
TradingCommand::SubmitOrder(mut command) => {
matching_engine.process_order(&mut command.order, account_id)
}
TradingCommand::ModifyOrder(ref command) => {
matching_engine.process_modify(command, account_id)
}
TradingCommand::CancelOrder(ref command) => {
matching_engine.process_cancel(command, account_id)
}
TradingCommand::CancelAllOrders(ref command) => {
matching_engine.process_cancel_all(command, account_id)
}
TradingCommand::BatchCancelOrders(ref command) => {
matching_engine.process_batch_cancel(command, account_id)
}
TradingCommand::QueryOrder(ref command) => {
matching_engine.process_query_order(command, account_id)
}
TradingCommand::SubmitOrderList(mut command) => {
for order in &mut command.order_list.orders {
matching_engine.process_order(order, account_id);
}
}
}
} else {
panic!("Matching engine should be initialized");
}
}

pub fn generate_fresh_account_state(&self) {
Expand Down
40 changes: 39 additions & 1 deletion nautilus_core/backtest/src/matching_engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ use std::{
use chrono::TimeDelta;
use nautilus_common::{cache::Cache, msgbus::MessageBus};
use nautilus_core::{AtomicTime, UnixNanos, UUID4};
use nautilus_execution::matching_core::OrderMatchingCore;
use nautilus_execution::{
matching_core::OrderMatchingCore,
messages::{BatchCancelOrders, CancelAllOrders, CancelOrder, ModifyOrder, QueryOrder},
};
use nautilus_model::{
data::{order::BookOrder, Bar, BarType, OrderBookDelta, OrderBookDeltas, QuoteTick, TradeTick},
enums::{
Expand Down Expand Up @@ -707,6 +710,41 @@ impl OrderMatchingEngine {
}
}

pub fn process_modify(&self, command: &ModifyOrder, account_id: AccountId) {
todo!("implement process_modify")
}

pub fn process_cancel(&mut self, command: &CancelOrder, account_id: AccountId) {
match self.core.get_order(command.client_order_id) {
Some(passive_order) => {
if passive_order.is_inflight() || passive_order.is_open() {
self.cancel_order(&OrderAny::from(passive_order.to_owned()), None);
}
}
None => self.generate_order_cancel_rejected(
command.trader_id,
command.strategy_id,
account_id,
command.instrument_id,
command.client_order_id,
command.venue_order_id,
Ustr::from(format!("Order {} not found", command.client_order_id).as_str()),
),
}
}

pub fn process_cancel_all(&self, command: &CancelAllOrders, account_id: AccountId) {
todo!("implement process_cancel_all")
}

pub fn process_batch_cancel(&self, command: &BatchCancelOrders, account_id: AccountId) {
todo!("implement process_batch_cancel")
}

pub fn process_query_order(&self, command: &QueryOrder, account_id: AccountId) {
todo!("implement process_query_order")
}

fn process_market_order(&mut self, order: &mut OrderAny) {
if order.time_in_force() == TimeInForce::AtTheOpen
|| order.time_in_force() == TimeInForce::AtTheClose
Expand Down
125 changes: 124 additions & 1 deletion nautilus_core/backtest/src/matching_engine/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use nautilus_common::{
},
};
use nautilus_core::{AtomicTime, UnixNanos, UUID4};
use nautilus_execution::messages::CancelOrder;
use nautilus_model::{
data::{BookOrder, OrderBookDelta},
enums::{
Expand All @@ -37,7 +38,10 @@ use nautilus_model::{
order::rejected::OrderRejectedBuilder, OrderEventAny, OrderEventType, OrderFilled,
OrderRejected,
},
identifiers::{stubs::account_id, AccountId, ClientOrderId, PositionId, TradeId, VenueOrderId},
identifiers::{
stubs::account_id, AccountId, ClientId, ClientOrderId, PositionId, StrategyId, TradeId,
TraderId, VenueOrderId,
},
instruments::{
stubs::{crypto_perpetual_ethusdt, equity_aapl, futures_contract_es},
CryptoPerpetual, Equity, InstrumentAny,
Expand Down Expand Up @@ -1319,3 +1323,122 @@ fn test_process_stop_limit_order_triggered_filled(
assert_eq!(order_filled.last_px, Price::from("1500.00"));
assert_eq!(order_filled.last_qty, Quantity::from("1.000"));
}

#[rstest]
fn test_process_cancel_command_valid(
instrument_eth_usdt: InstrumentAny,
orderbook_delta_sell: OrderBookDelta,
mut msgbus: MessageBus,
order_event_handler: ShareableMessageHandler,
account_id: AccountId,
time: AtomicTime,
) {
msgbus.register(
msgbus.switchboard.exec_engine_process,
order_event_handler.clone(),
);
// create normal l2 engine without reject_stop_orders config param
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
Rc::new(RefCell::new(msgbus)),
None,
None,
None,
);
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");

// create BUY LIMIT order bellow current ask, so it wont be filled
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)
.build();
// create cancel command for limit order above
let cancel_command = CancelOrder::new(
TraderId::from("TRADER-001"),
ClientId::from("CLIENT-001"),
StrategyId::from("STRATEGY-001"),
instrument_eth_usdt.id(),
client_order_id,
VenueOrderId::from("V1"),
UUID4::new(),
UnixNanos::default(),
)
.unwrap();

engine_l2.process_order_book_delta(&orderbook_delta_sell);
engine_l2.process_order(&mut limit_order, account_id);
engine_l2.process_cancel(&cancel_command, account_id);

// check we have received OrderAccepted and then OrderCanceled event
let saved_messages = get_order_event_handler_messages(order_event_handler);
assert_eq!(saved_messages.len(), 2);
let order_event_first = saved_messages.first().unwrap();
let order_accepted = match order_event_first {
OrderEventAny::Accepted(order_accepted) => order_accepted,
_ => panic!("Expected OrderAccepted event in first message"),
};
assert_eq!(order_accepted.client_order_id, client_order_id);
let order_event_second = saved_messages.get(1).unwrap();
let order_canceled = match order_event_second {
OrderEventAny::Canceled(order_canceled) => order_canceled,
_ => panic!("Expected OrderCanceled event in second message"),
};
assert_eq!(order_canceled.client_order_id, client_order_id);
}

#[rstest]
fn test_process_cancel_command_order_not_found(
instrument_eth_usdt: InstrumentAny,
orderbook_delta_sell: OrderBookDelta,
mut msgbus: MessageBus,
order_event_handler: ShareableMessageHandler,
account_id: AccountId,
time: AtomicTime,
) {
msgbus.register(
msgbus.switchboard.exec_engine_process,
order_event_handler.clone(),
);
// create normal l2 engine without reject_stop_orders config param
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
Rc::new(RefCell::new(msgbus)),
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::from("TRADER-001"),
ClientId::from("CLIENT-001"),
StrategyId::from("STRATEGY-001"),
instrument_eth_usdt.id(),
client_order_id,
VenueOrderId::from("V1"),
UUID4::new(),
UnixNanos::default(),
)
.unwrap();

// process cancel command for order which doesn't exists
engine_l2.process_cancel(&cancel_command, account_id);

// check we have received OrderCancelRejected event
let saved_messages = get_order_event_handler_messages(order_event_handler);
assert_eq!(saved_messages.len(), 1);
let order_event = saved_messages.first().unwrap();
let order_rejected = match order_event {
OrderEventAny::CancelRejected(order_rejected) => order_rejected,
_ => panic!("Expected OrderRejected event in first message"),
};
assert_eq!(order_rejected.client_order_id, client_order_id);
assert_eq!(
order_rejected.reason,
Ustr::from(format!("Order {client_order_id} not found").as_str())
);
}
12 changes: 12 additions & 0 deletions nautilus_core/execution/src/matching_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ impl OrderMatchingCore {
self.price_increment.precision
}

#[must_use]
pub fn get_order(&self, client_order_id: ClientOrderId) -> Option<&PassiveOrderAny> {
self.orders_bid
.iter()
.find(|o| o.client_order_id() == client_order_id)
.or_else(|| {
self.orders_ask
.iter()
.find(|o| o.client_order_id() == client_order_id)
})
}

#[must_use]
pub fn get_orders_bid(&self) -> &[PassiveOrderAny] {
self.orders_bid.as_slice()
Expand Down
60 changes: 60 additions & 0 deletions nautilus_core/model/src/orders/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,22 @@ impl PassiveOrderAny {
}
}

#[must_use]
pub fn is_open(&self) -> bool {
match self {
Self::Limit(order) => order.is_open(),
Self::Stop(order) => order.is_open(),
}
}

#[must_use]
pub fn is_inflight(&self) -> bool {
match self {
Self::Limit(order) => order.is_inflight(),
Self::Stop(order) => order.is_inflight(),
}
}

#[must_use]
pub fn expire_time(&self) -> Option<UnixNanos> {
match self {
Expand Down Expand Up @@ -1314,6 +1330,26 @@ impl LimitOrderAny {
}
}

#[must_use]
pub fn is_open(&self) -> bool {
match self {
Self::Limit(order) => order.is_open(),
Self::MarketToLimit(order) => order.is_open(),
Self::StopLimit(order) => order.is_open(),
Self::TrailingStopLimit(order) => order.is_open(),
}
}

#[must_use]
pub fn is_inflight(&self) -> bool {
match self {
Self::Limit(order) => order.is_inflight(),
Self::MarketToLimit(order) => order.is_inflight(),
Self::StopLimit(order) => order.is_inflight(),
Self::TrailingStopLimit(order) => order.is_inflight(),
}
}

#[must_use]
pub fn expire_time(&self) -> Option<UnixNanos> {
match self {
Expand Down Expand Up @@ -1395,6 +1431,30 @@ impl StopOrderAny {
}
}

#[must_use]
pub fn is_open(&self) -> bool {
match self {
Self::LimitIfTouched(order) => order.is_open(),
Self::MarketIfTouched(order) => order.is_open(),
Self::StopLimit(order) => order.is_open(),
Self::StopMarket(order) => order.is_open(),
Self::TrailingStopLimit(order) => order.is_open(),
Self::TrailingStopMarket(order) => order.is_open(),
}
}

#[must_use]
pub fn is_inflight(&self) -> bool {
match self {
Self::LimitIfTouched(order) => order.is_inflight(),
Self::MarketIfTouched(order) => order.is_inflight(),
Self::StopLimit(order) => order.is_inflight(),
Self::StopMarket(order) => order.is_inflight(),
Self::TrailingStopLimit(order) => order.is_inflight(),
Self::TrailingStopMarket(order) => order.is_inflight(),
}
}

#[must_use]
pub fn expire_time(&self) -> Option<UnixNanos> {
match self {
Expand Down

0 comments on commit fafcc30

Please sign in to comment.