Skip to content

Commit 6242f78

Browse files
authored
Port handling of market-if-touched orders for matching engine in Rust (#2329)
1 parent 19b4dde commit 6242f78

File tree

3 files changed

+192
-5
lines changed

3 files changed

+192
-5
lines changed

crates/execution/src/matching_core/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,14 @@ impl OrderMatchingCore {
272272
OrderSideSpecified::Sell => self.bid.is_some_and(|b| b <= price),
273273
}
274274
}
275+
276+
#[must_use]
277+
pub fn is_touch_triggered(&self, side: OrderSideSpecified, trigger_price: Price) -> bool {
278+
match side {
279+
OrderSideSpecified::Buy => self.ask.is_some_and(|a| a <= trigger_price),
280+
OrderSideSpecified::Sell => self.bid.is_some_and(|b| b >= trigger_price),
281+
}
282+
}
275283
}
276284

277285
////////////////////////////////////////////////////////////////////////////////

crates/execution/src/matching_engine/engine.rs

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -930,8 +930,35 @@ impl OrderMatchingEngine {
930930
self.accept_order(order);
931931
}
932932

933-
fn process_market_if_touched_order(&mut self, order: &OrderAny) {
934-
todo!("process_market_if_touched_order")
933+
fn process_market_if_touched_order(&mut self, order: &mut OrderAny) {
934+
if self
935+
.core
936+
.is_touch_triggered(order.order_side_specified(), order.trigger_price().unwrap())
937+
{
938+
if self.config.reject_stop_orders {
939+
self.generate_order_rejected(
940+
order,
941+
format!(
942+
"{} {} order trigger px of {} was in the market: bid={}, ask={}, but rejected because of configuration",
943+
order.order_type(),
944+
order.order_side(),
945+
order.trigger_price().unwrap(),
946+
self.core
947+
.bid
948+
.map_or_else(|| "None".to_string(), |p| p.to_string()),
949+
self.core
950+
.ask
951+
.map_or_else(|| "None".to_string(), |p| p.to_string())
952+
).into(),
953+
);
954+
return;
955+
}
956+
self.fill_market_order(order);
957+
return;
958+
}
959+
960+
// Order is valid and accepted
961+
self.accept_order(order);
935962
}
936963

937964
fn process_limit_if_touched_order(&mut self, order: &OrderAny) {
@@ -1584,11 +1611,42 @@ impl OrderMatchingEngine {
15841611

15851612
fn update_market_if_touched_order(
15861613
&mut self,
1587-
order: &OrderAny,
1614+
order: &mut OrderAny,
15881615
quantity: Quantity,
1589-
price: Price,
1616+
trigger_price: Price,
15901617
) {
1591-
todo!("update_market_if_touched_order")
1618+
if self
1619+
.core
1620+
.is_touch_triggered(order.order_side_specified(), trigger_price)
1621+
{
1622+
self.generate_order_modify_rejected(
1623+
order.trader_id(),
1624+
order.strategy_id(),
1625+
order.instrument_id(),
1626+
order.client_order_id(),
1627+
Ustr::from(
1628+
format!(
1629+
"{} {} order new trigger px of {} was in the market: bid={}, ask={}",
1630+
order.order_type(),
1631+
order.order_side(),
1632+
trigger_price,
1633+
self.core
1634+
.bid
1635+
.map_or_else(|| "None".to_string(), |p| p.to_string()),
1636+
self.core
1637+
.ask
1638+
.map_or_else(|| "None".to_string(), |p| p.to_string())
1639+
)
1640+
.as_str(),
1641+
),
1642+
order.venue_order_id(),
1643+
order.account_id(),
1644+
);
1645+
// Cannot update order
1646+
return;
1647+
}
1648+
1649+
self.generate_order_updated(order, quantity, None, Some(trigger_price));
15921650
}
15931651

15941652
fn update_limit_if_touched_order(

crates/execution/src/matching_engine/tests.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2161,3 +2161,124 @@ fn test_update_stop_limit_order_valid_update_not_triggered(
21612161
assert_eq!(order_updated.client_order_id, client_order_id);
21622162
assert_eq!(order_updated.trigger_price.unwrap(), new_trigger_price);
21632163
}
2164+
2165+
#[rstest]
2166+
fn test_process_market_if_touched_order_already_triggered(
2167+
instrument_eth_usdt: InstrumentAny,
2168+
mut msgbus: MessageBus,
2169+
order_event_handler: ShareableMessageHandler,
2170+
account_id: AccountId,
2171+
) {
2172+
msgbus.register(
2173+
msgbus.switchboard.exec_engine_process,
2174+
order_event_handler.clone(),
2175+
);
2176+
let mut engine_l2 = get_order_matching_engine_l2(
2177+
instrument_eth_usdt.clone(),
2178+
Rc::new(RefCell::new(msgbus)),
2179+
None,
2180+
None,
2181+
None,
2182+
);
2183+
2184+
// Add SELL limit orderbook delta to have ask initialized
2185+
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
2186+
.book_action(BookAction::Add)
2187+
.book_order(BookOrder::new(
2188+
OrderSide::Sell,
2189+
Price::from("1500.00"),
2190+
Quantity::from("1.000"),
2191+
1,
2192+
))
2193+
.build();
2194+
engine_l2.process_order_book_delta(&orderbook_delta_sell);
2195+
2196+
// Create MARKET IF TOUCHED order which is already activated as trigger price of 1500.00 is equal to current ask of 1500.00
2197+
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
2198+
let mut stop_market_order = OrderTestBuilder::new(OrderType::MarketIfTouched)
2199+
.instrument_id(instrument_eth_usdt.id())
2200+
.side(OrderSide::Buy)
2201+
.trigger_price(Price::from("1500.00"))
2202+
.quantity(Quantity::from("1.000"))
2203+
.client_order_id(client_order_id)
2204+
.build();
2205+
engine_l2.process_order(&mut stop_market_order, account_id);
2206+
2207+
// Check that order was filled immediately with correct price and quantity
2208+
let saved_messages = get_order_event_handler_messages(order_event_handler);
2209+
assert_eq!(saved_messages.len(), 1);
2210+
let order_event = saved_messages.first().unwrap();
2211+
let order_filled = match order_event {
2212+
OrderEventAny::Filled(order_filled) => order_filled,
2213+
_ => panic!("Expected OrderFilled event in first message"),
2214+
};
2215+
assert_eq!(order_filled.client_order_id, client_order_id);
2216+
assert_eq!(order_filled.last_px, Price::from("1500.00"));
2217+
assert_eq!(order_filled.last_qty, Quantity::from("1.000"));
2218+
}
2219+
2220+
#[rstest]
2221+
fn test_update_market_if_touched_order_valid(
2222+
instrument_eth_usdt: InstrumentAny,
2223+
mut msgbus: MessageBus,
2224+
order_event_handler: ShareableMessageHandler,
2225+
account_id: AccountId,
2226+
) {
2227+
msgbus.register(
2228+
msgbus.switchboard.exec_engine_process,
2229+
order_event_handler.clone(),
2230+
);
2231+
let mut engine_l2 = get_order_matching_engine_l2(
2232+
instrument_eth_usdt.clone(),
2233+
Rc::new(RefCell::new(msgbus)),
2234+
None,
2235+
None,
2236+
None,
2237+
);
2238+
2239+
// Create MARKET IF TOUCHED order which is not activated as trigger price of 1505.00 is above current ask of 1500.00
2240+
let client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
2241+
let mut stop_market_order = OrderTestBuilder::new(OrderType::MarketIfTouched)
2242+
.instrument_id(instrument_eth_usdt.id())
2243+
.side(OrderSide::Buy)
2244+
.trigger_price(Price::from("1505.00"))
2245+
.quantity(Quantity::from("1.000"))
2246+
.client_order_id(client_order_id)
2247+
.build();
2248+
engine_l2.process_order(&mut stop_market_order, account_id);
2249+
2250+
// Create modify command which moves trigger price to 1501.00 which won't trigger the stop price again
2251+
// as ask is at 1500.00 and order will be correctly updated
2252+
let new_trigger_price = Price::from("1501.00");
2253+
let modify_order_command = ModifyOrder::new(
2254+
TraderId::from("TRADER-001"),
2255+
ClientId::from("CLIENT-001"),
2256+
StrategyId::from("STRATEGY-001"),
2257+
instrument_eth_usdt.id(),
2258+
client_order_id,
2259+
VenueOrderId::from("V1"),
2260+
None,
2261+
None,
2262+
Some(new_trigger_price),
2263+
UUID4::new(),
2264+
UnixNanos::default(),
2265+
);
2266+
engine_l2.process_modify(&modify_order_command.unwrap(), account_id);
2267+
2268+
// Check that we have received OrderAccepted and then OrderUpdated
2269+
let saved_messages = get_order_event_handler_messages(order_event_handler);
2270+
assert_eq!(saved_messages.len(), 2);
2271+
let order_event_first = saved_messages.first().unwrap();
2272+
let order_accepted = match order_event_first {
2273+
OrderEventAny::Accepted(order_accepted) => order_accepted,
2274+
_ => panic!("Expected OrderAccepted event in first message"),
2275+
};
2276+
assert_eq!(order_accepted.client_order_id, client_order_id);
2277+
let order_event_second = saved_messages.get(1).unwrap();
2278+
let order_updated = match order_event_second {
2279+
OrderEventAny::Updated(order_updated) => order_updated,
2280+
_ => panic!("Expected OrderUpdated event in second message"),
2281+
};
2282+
assert_eq!(order_updated.client_order_id, client_order_id);
2283+
assert_eq!(order_updated.trigger_price.unwrap(), new_trigger_price);
2284+
}

0 commit comments

Comments
 (0)