Skip to content

Commit

Permalink
implement processing of market-to-limit orders
Browse files Browse the repository at this point in the history
  • Loading branch information
filipmacek committed Feb 24, 2025
1 parent 08d3fa9 commit 60887da
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 3 deletions.
23 changes: 21 additions & 2 deletions crates/execution/src/matching_engine/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -846,8 +846,24 @@ impl OrderMatchingEngine {
}
}

fn process_market_to_limit_order(&mut self, order: &OrderAny) {
todo!("process_market_to_limit_order")
fn process_market_to_limit_order(&mut self, order: &mut OrderAny) {
// Check that market exists
if order.order_side() == OrderSide::Buy && !self.core.is_ask_initialized {

Check failure on line 851 in crates/execution/src/matching_engine/engine.rs

View workflow job for this annotation

GitHub Actions / pre-commit

this `if` has identical blocks
self.generate_order_rejected(
order,
format!("No market for {}", order.instrument_id()).into(),
);
return;
} else if order.order_side() == OrderSide::Sell && !self.core.is_bid_initialized {
self.generate_order_rejected(
order,
format!("No market for {}", order.instrument_id()).into(),
);
return;
}

// Immediately fill marketable order
self.fill_market_order(order);
}

fn process_stop_market_order(&mut self, order: &mut OrderAny) {
Expand Down Expand Up @@ -1364,6 +1380,9 @@ impl OrderMatchingEngine {
if order.filled_qty() == Quantity::zero(order.filled_qty().precision)
&& order.order_type() == OrderType::MarketToLimit
{
// Matching engine should the first accept order then update limit price
self.accept_order(order);

self.generate_order_updated(order, order.quantity(), Some(fill_px), None);
initial_market_to_limit_fill = true;
}
Expand Down
84 changes: 83 additions & 1 deletion crates/execution/src/matching_engine/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ use nautilus_model::{
CryptoPerpetual, Equity, InstrumentAny,
stubs::{crypto_perpetual_ethusdt, equity_aapl, futures_contract_es},
},
orders::{OrderAny, OrderTestBuilder, stubs::TestOrderStubs},
orders::{
OrderAny, OrderTestBuilder,
stubs::{TestOrderEventStubs, TestOrderStubs},
},
types::{Price, Quantity},
};
use rstest::{fixture, rstest};
Expand Down Expand Up @@ -2481,3 +2484,82 @@ fn test_update_limit_if_touched_order_valid(
assert_eq!(order_updated.client_order_id, client_order_id);
assert_eq!(order_updated.trigger_price.unwrap(), new_trigger_price);
}

#[rstest]
fn test_process_market_to_limit_orders_not_fully_filled(
instrument_eth_usdt: InstrumentAny,
mut msgbus: MessageBus,
order_event_handler: ShareableMessageHandler,
account_id: AccountId,
) {
msgbus.register(
msgbus.switchboard.exec_engine_process,
order_event_handler.clone(),
);
let mut engine_l2 = get_order_matching_engine_l2(
instrument_eth_usdt.clone(),
Rc::new(RefCell::new(msgbus)),
None,
None,
None,
);

// Add SELL limit orderbook delta to have ask initialized
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);

// Create MARKET TO LIMIT order with quantity of 2 which will be half filled
// and order half will be accepted as limit order
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();
// Make order submitted
let order_submitted_event =
TestOrderEventStubs::order_submitted(&market_to_limit_order, account_id);
market_to_limit_order.apply(order_submitted_event).unwrap();
engine_l2.process_order(&mut market_to_limit_order, account_id);

// Check sequence of events for MARKET-TO-LIMIT order being not fully filled
// 1. OrderAccepted - order fill be transformed to limit so we must receive accepted event
// 2. OrderUpdated - order was updated to new limix price where market order stopped filling
// 3. OrderFilled - after inclusion in book we emit order filled event for market order at start
let saved_messages = get_order_event_handler_messages(order_event_handler);
assert_eq!(saved_messages.len(), 3);
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_updated = match order_event_second {
OrderEventAny::Updated(order_updated) => order_updated,
_ => panic!("Expected OrderUpdated event in second message"),
};
assert_eq!(order_updated.client_order_id, client_order_id);
let order_event_third = saved_messages.get(2).unwrap();
let order_filled = match order_event_third {
OrderEventAny::Filled(order_filled) => order_filled,
_ => panic!("Expected OrderFilled event in third message"),
};
assert_eq!(order_filled.client_order_id, client_order_id);
assert_eq!(order_filled.last_px, Price::from("1500.00"));
assert_eq!(order_filled.last_qty, Quantity::from("1.000"));
// Check that we have one resting limit order in the matching core
let resting_orders = engine_l2.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);
}
1 change: 1 addition & 0 deletions crates/model/src/orders/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,7 @@ impl From<OrderAny> for PassiveOrderAny {
OrderAny::StopMarket(_) => PassiveOrderAny::Stop(order.into()),
OrderAny::TrailingStopLimit(_) => PassiveOrderAny::Stop(order.into()),
OrderAny::TrailingStopMarket(_) => PassiveOrderAny::Stop(order.into()),
OrderAny::MarketToLimit(_) => PassiveOrderAny::Limit(order.into()),
_ => panic!("WIP: Implement trait bound to require `HasPrice`"),
}
}
Expand Down

0 comments on commit 60887da

Please sign in to comment.