Skip to content

Commit 952e0bf

Browse files
authored
Contingent orders handling for order matching engine (#2404)
1 parent 6bb3872 commit 952e0bf

File tree

5 files changed

+293
-11
lines changed

5 files changed

+293
-11
lines changed

crates/backtest/src/exchange.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -573,14 +573,12 @@ impl SimulatedExchange {
573573
TradingCommand::BatchCancelOrders(ref command) => {
574574
matching_engine.process_batch_cancel(command, account_id);
575575
}
576-
TradingCommand::QueryOrder(ref command) => {
577-
matching_engine.process_query_order(command, account_id);
578-
}
579576
TradingCommand::SubmitOrderList(mut command) => {
580577
for order in &mut command.order_list.orders {
581578
matching_engine.process_order(order, account_id);
582579
}
583580
}
581+
_ => {}
584582
}
585583
} else {
586584
panic!("Matching engine should be initialized");

crates/common/src/cache/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1229,7 +1229,7 @@ impl Cache {
12291229
pub fn add_position_id(
12301230
&mut self,
12311231
position_id: &PositionId,
1232-
_venue: &Venue,
1232+
venue: &Venue,
12331233
client_order_id: &ClientOrderId,
12341234
strategy_id: &StrategyId,
12351235
) -> anyhow::Result<()> {
@@ -1261,6 +1261,13 @@ impl Cache {
12611261
.or_default()
12621262
.insert(*position_id);
12631263

1264+
// Index: Venue -> set[PositionId]
1265+
self.index
1266+
.venue_positions
1267+
.entry(*venue)
1268+
.or_default()
1269+
.insert(*position_id);
1270+
12641271
Ok(())
12651272
}
12661273

crates/execution/src/matching_engine/engine.rs

Lines changed: 145 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ use ustr::Ustr;
5555
use crate::{
5656
matching_core::OrderMatchingCore,
5757
matching_engine::{config::OrderMatchingEngineConfig, ids_generator::IdsGenerator},
58-
messages::{BatchCancelOrders, CancelAllOrders, CancelOrder, ModifyOrder, QueryOrder},
58+
messages::{BatchCancelOrders, CancelAllOrders, CancelOrder, ModifyOrder},
5959
models::{
6060
fee::{FeeModel, FeeModelAny},
6161
fill::FillModel,
@@ -766,10 +766,6 @@ impl OrderMatchingEngine {
766766
}
767767
}
768768

769-
pub fn process_query_order(&self, command: &QueryOrder, account_id: AccountId) {
770-
todo!("implement process_query_order")
771-
}
772-
773769
fn process_market_order(&mut self, order: &mut OrderAny) {
774770
if order.time_in_force() == TimeInForce::AtTheOpen
775771
|| order.time_in_force() == TimeInForce::AtTheClose
@@ -1531,7 +1527,123 @@ impl OrderMatchingEngine {
15311527
return;
15321528
}
15331529

1534-
todo!("Check for contingent orders")
1530+
if let Some(contingency_type) = order.contingency_type() {
1531+
match contingency_type {
1532+
ContingencyType::Oto => {
1533+
if let Some(linked_orders_ids) = order.linked_order_ids() {
1534+
for client_order_id in &linked_orders_ids {
1535+
let mut child_order = match self.cache.borrow().order(client_order_id) {
1536+
Some(child_order) => child_order.clone(),
1537+
None => panic!("Order {client_order_id} not found in cache"),
1538+
};
1539+
1540+
if child_order.is_closed() || child_order.is_active_local() {
1541+
continue;
1542+
}
1543+
1544+
// Check if we need to index position id
1545+
if let (None, Some(position_id)) =
1546+
(child_order.position_id(), order.position_id())
1547+
{
1548+
self.cache
1549+
.borrow_mut()
1550+
.add_position_id(
1551+
&position_id,
1552+
&self.venue,
1553+
client_order_id,
1554+
&child_order.strategy_id(),
1555+
)
1556+
.unwrap();
1557+
log::debug!(
1558+
"Added position id {} to cache for order {}",
1559+
position_id,
1560+
client_order_id
1561+
)
1562+
}
1563+
1564+
if (!child_order.is_open())
1565+
|| (matches!(child_order.status(), OrderStatus::PendingUpdate)
1566+
&& child_order
1567+
.previous_status()
1568+
.is_some_and(|s| matches!(s, OrderStatus::Submitted)))
1569+
{
1570+
let account_id = order.account_id().unwrap_or_else(|| {
1571+
*self.account_ids.get(&order.trader_id()).unwrap_or_else(|| {
1572+
panic!(
1573+
"Account ID not found for trader {}",
1574+
order.trader_id()
1575+
)
1576+
})
1577+
});
1578+
self.process_order(&mut child_order, account_id);
1579+
}
1580+
}
1581+
} else {
1582+
log::error!(
1583+
"OTO order {} does not have linked orders",
1584+
order.client_order_id()
1585+
);
1586+
}
1587+
}
1588+
ContingencyType::Oco => {
1589+
if let Some(linked_orders_ids) = order.linked_order_ids() {
1590+
for client_order_id in &linked_orders_ids {
1591+
let child_order = match self.cache.borrow().order(client_order_id) {
1592+
Some(child_order) => child_order.clone(),
1593+
None => panic!("Order {client_order_id} not found in cache"),
1594+
};
1595+
1596+
if child_order.is_closed() || child_order.is_active_local() {
1597+
continue;
1598+
}
1599+
1600+
self.cancel_order(&child_order, None);
1601+
}
1602+
} else {
1603+
log::error!(
1604+
"OCO order {} does not have linked orders",
1605+
order.client_order_id()
1606+
);
1607+
}
1608+
}
1609+
ContingencyType::Ouo => {
1610+
if let Some(linked_orders_ids) = order.linked_order_ids() {
1611+
for client_order_id in &linked_orders_ids {
1612+
let mut child_order = match self.cache.borrow().order(client_order_id) {
1613+
Some(child_order) => child_order.clone(),
1614+
None => panic!("Order {client_order_id} not found in cache"),
1615+
};
1616+
1617+
if child_order.is_active_local() {
1618+
continue;
1619+
}
1620+
1621+
if order.is_closed() && child_order.is_open() {
1622+
self.cancel_order(&child_order, None);
1623+
} else if !order.leaves_qty().is_zero()
1624+
&& order.leaves_qty() != child_order.leaves_qty()
1625+
{
1626+
let price = child_order.price();
1627+
let trigger_price = child_order.trigger_price();
1628+
self.update_order(
1629+
&mut child_order,
1630+
Some(order.leaves_qty()),
1631+
price,
1632+
trigger_price,
1633+
Some(false),
1634+
)
1635+
}
1636+
}
1637+
} else {
1638+
log::error!(
1639+
"OUO order {} does not have linked orders",
1640+
order.client_order_id()
1641+
);
1642+
}
1643+
}
1644+
_ => {}
1645+
}
1646+
}
15351647
}
15361648

15371649
fn update_limit_order(&mut self, order: &mut OrderAny, quantity: Quantity, price: Price) {
@@ -1942,7 +2054,33 @@ impl OrderMatchingEngine {
19422054
}
19432055

19442056
fn update_contingent_order(&mut self, order: &OrderAny) {
1945-
todo!("update_contingent_order")
2057+
log::debug!("Updating OUO orders from {}", order.client_order_id());
2058+
if let Some(linked_order_ids) = order.linked_order_ids() {
2059+
for client_order_id in &linked_order_ids {
2060+
let mut child_order = match self.cache.borrow().order(client_order_id) {
2061+
Some(order) => order.clone(),
2062+
None => panic!("Order {client_order_id} not found in cache."),
2063+
};
2064+
2065+
if child_order.is_active_local() {
2066+
continue;
2067+
}
2068+
2069+
if order.leaves_qty().is_zero() {
2070+
self.cancel_order(&child_order, None);
2071+
} else if child_order.leaves_qty() != order.leaves_qty() {
2072+
let price = child_order.price();
2073+
let trigger_price = child_order.trigger_price();
2074+
self.update_order(
2075+
&mut child_order,
2076+
Some(order.leaves_qty()),
2077+
price,
2078+
trigger_price,
2079+
Some(false),
2080+
)
2081+
}
2082+
}
2083+
}
19462084
}
19472085

19482086
fn cancel_contingent_orders(&mut self, order: &OrderAny) {

crates/execution/src/matching_engine/tests.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2726,3 +2726,127 @@ fn test_updating_of_trailing_stop_market_order_with_no_trigger_price_set(
27262726
assert_eq!(order_updated.client_order_id, client_order_id);
27272727
assert_eq!(order_updated.trigger_price.unwrap(), Price::from("1481.00"));
27282728
}
2729+
2730+
#[rstest]
2731+
fn test_updating_of_contingent_orders(
2732+
instrument_eth_usdt: InstrumentAny,
2733+
mut msgbus: MessageBus,
2734+
order_event_handler: ShareableMessageHandler,
2735+
account_id: AccountId,
2736+
) {
2737+
msgbus.register(
2738+
msgbus.switchboard.exec_engine_process,
2739+
order_event_handler.clone(),
2740+
);
2741+
let cache = Rc::new(RefCell::new(Cache::default()));
2742+
// Create order matching engine which supports contingent orders
2743+
let mut engine_config = OrderMatchingEngineConfig::default();
2744+
engine_config.support_contingent_orders = true;
2745+
let mut engine_l2 = get_order_matching_engine_l2(
2746+
instrument_eth_usdt.clone(),
2747+
Rc::new(RefCell::new(msgbus)),
2748+
Some(cache.clone()),
2749+
None,
2750+
Some(engine_config),
2751+
);
2752+
2753+
let orderbook_delta_sell = OrderBookDeltaTestBuilder::new(instrument_eth_usdt.id())
2754+
.book_action(BookAction::Add)
2755+
.book_order(BookOrder::new(
2756+
OrderSide::Sell,
2757+
Price::from("1500.00"),
2758+
Quantity::from("1.000"),
2759+
1,
2760+
))
2761+
.build();
2762+
engine_l2.process_order_book_delta(&orderbook_delta_sell);
2763+
2764+
// Create primary limit order and StopMarket OUO orders
2765+
// and link them together
2766+
let client_order_id_primary = ClientOrderId::from("O-19700101-000000-001-001-1");
2767+
let client_order_id_contingent = ClientOrderId::from("O-19700101-000000-001-001-2");
2768+
let mut primary_order = OrderTestBuilder::new(OrderType::Limit)
2769+
.instrument_id(instrument_eth_usdt.id())
2770+
.side(OrderSide::Buy)
2771+
.price(Price::from("1495.00"))
2772+
.quantity(Quantity::from("1.000"))
2773+
.client_order_id(client_order_id_primary)
2774+
.contingency_type(ContingencyType::Ouo)
2775+
.linked_order_ids(vec![client_order_id_contingent])
2776+
.submit(true)
2777+
.build();
2778+
let mut contingent_stop_market_order = OrderTestBuilder::new(OrderType::StopMarket)
2779+
.instrument_id(instrument_eth_usdt.id())
2780+
.side(OrderSide::Sell)
2781+
.trigger_price(Price::from("1500.00"))
2782+
.quantity(Quantity::from("1.000"))
2783+
.client_order_id(client_order_id_contingent)
2784+
.linked_order_ids(vec![client_order_id_primary])
2785+
.contingency_type(ContingencyType::Ouo)
2786+
.submit(true)
2787+
.build();
2788+
2789+
// Save orders to cache and process it by engine
2790+
cache
2791+
.borrow_mut()
2792+
.add_order(primary_order.clone(), None, None, false)
2793+
.unwrap();
2794+
cache
2795+
.borrow_mut()
2796+
.add_order(contingent_stop_market_order.clone(), None, None, false)
2797+
.unwrap();
2798+
engine_l2.process_order(&mut primary_order, account_id);
2799+
2800+
engine_l2.process_order(&mut contingent_stop_market_order, account_id);
2801+
2802+
// Modify primary order quantity to 2.000 which will trigger the contingent order
2803+
// update of the same quantity
2804+
let modify_order_command = ModifyOrder::new(
2805+
TraderId::from("TRADER-001"),
2806+
ClientId::from("CLIENT-001"),
2807+
StrategyId::from("STRATEGY-001"),
2808+
instrument_eth_usdt.id(),
2809+
client_order_id_primary,
2810+
VenueOrderId::from("V1"),
2811+
Some(Quantity::from("2.000")),
2812+
None,
2813+
None,
2814+
UUID4::new(),
2815+
UnixNanos::default(),
2816+
);
2817+
engine_l2.process_modify(&modify_order_command.unwrap(), account_id);
2818+
2819+
// Check that we have received following sequence of events
2820+
// 1. OrderAccepted for primary limit order
2821+
// 2. OrderAccepted for contingent stop market order
2822+
// 3. OrderUpdated for primary limit order with new quantity of 2.000
2823+
// 4. OrderUpdated for contingent stop market order with new quantity of 2.000
2824+
let saved_messages = get_order_event_handler_messages(order_event_handler);
2825+
assert_eq!(saved_messages.len(), 4);
2826+
let order_event_first = saved_messages.first().unwrap();
2827+
let order_accepted = match order_event_first {
2828+
OrderEventAny::Accepted(order_accepted) => order_accepted,
2829+
_ => panic!("Expected OrderAccepted event in first message"),
2830+
};
2831+
assert_eq!(order_accepted.client_order_id, client_order_id_primary);
2832+
let order_event_second = saved_messages.get(1).unwrap();
2833+
let order_accepted = match order_event_second {
2834+
OrderEventAny::Accepted(order_accepted) => order_accepted,
2835+
_ => panic!("Expected OrderAccepted event in second message"),
2836+
};
2837+
assert_eq!(order_accepted.client_order_id, client_order_id_contingent);
2838+
let order_event_third = saved_messages.get(2).unwrap();
2839+
let order_updated = match order_event_third {
2840+
OrderEventAny::Updated(order_updated) => order_updated,
2841+
_ => panic!("Expected OrderUpdated event in third message"),
2842+
};
2843+
assert_eq!(order_updated.client_order_id, client_order_id_primary);
2844+
assert_eq!(order_updated.quantity, Quantity::from("2.000"));
2845+
let order_event_fourth = saved_messages.get(3).unwrap();
2846+
let order_updated = match order_event_fourth {
2847+
OrderEventAny::Updated(order_updated) => order_updated,
2848+
_ => panic!("Expected OrderUpdated event in fourth message"),
2849+
};
2850+
assert_eq!(order_updated.client_order_id, client_order_id_contingent);
2851+
assert_eq!(order_updated.quantity, Quantity::from("2.000"));
2852+
}

crates/model/src/orders/any.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,21 @@ impl OrderAny {
10091009
}
10101010
}
10111011

1012+
#[must_use]
1013+
pub fn previous_status(&self) -> Option<OrderStatus> {
1014+
match self {
1015+
Self::Limit(order) => order.previous_status,
1016+
Self::LimitIfTouched(order) => order.previous_status,
1017+
Self::Market(order) => order.previous_status,
1018+
Self::MarketIfTouched(order) => order.previous_status,
1019+
Self::MarketToLimit(order) => order.previous_status,
1020+
Self::StopLimit(order) => order.previous_status,
1021+
Self::StopMarket(order) => order.previous_status,
1022+
Self::TrailingStopLimit(order) => order.previous_status,
1023+
Self::TrailingStopMarket(order) => order.previous_status,
1024+
}
1025+
}
1026+
10121027
pub fn set_position_id(&mut self, position_id: Option<PositionId>) {
10131028
match self {
10141029
Self::Limit(order) => order.position_id = position_id,

0 commit comments

Comments
 (0)