Skip to content

Commit

Permalink
Add linked orders check in OrderMatchingEngine (#1843)
Browse files Browse the repository at this point in the history
  • Loading branch information
filipmacek committed Aug 11, 2024
1 parent 7106352 commit 3d6bce3
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 63 deletions.
202 changes: 144 additions & 58 deletions nautilus_core/backtest/src/matching_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,29 @@ impl OrderMatchingEngine {
}
}
}

if let Some(linked_order_ids) = order.linked_order_ids() {
for client_order_id in linked_order_ids {
match cache_borrow.order(&client_order_id) {
Some(contingent_order)
if (order.contingency_type().unwrap() == ContingencyType::Oco
|| order.contingency_type().unwrap()
== ContingencyType::Ouo)
&& !order.is_closed()
&& contingent_order.is_closed() =>
{
self.generate_order_rejected(
order,
format!("Contingent order {} already closed", client_order_id)
.into(),
);
return;
}
None => panic!("Cannot find contingent order for {}", client_order_id),
_ => {}
}
}
}
}

// Check fo valid order quantity precision
Expand Down Expand Up @@ -828,19 +851,17 @@ mod tests {
};
use nautilus_core::{nanos::UnixNanos, time::AtomicTime, uuid::UUID4};
use nautilus_model::{
enums::{
AccountType, BookType, ContingencyType, OmsType, OrderSide, TimeInForce, TriggerType,
enums::{AccountType, BookType, ContingencyType, OmsType, OrderSide, TimeInForce},
events::order::{
rejected::OrderRejectedBuilder, OrderEventAny, OrderEventType, OrderRejected,
},
events::order::{OrderEventAny, OrderEventType, OrderRejected},
identifiers::{AccountId, ClientOrderId, StrategyId, TraderId},
instruments::{
any::InstrumentAny,
equity::Equity,
stubs::{futures_contract_es, *},
},
orders::{
any::OrderAny, market::MarketOrder, stop_market::StopMarketOrder, stubs::TestOrderStubs,
},
orders::{any::OrderAny, market::MarketOrder, stubs::TestOrderStubs},
types::{price::Price, quantity::Quantity},
};
use rstest::{fixture, rstest};
Expand Down Expand Up @@ -891,7 +912,19 @@ mod tests {
InstrumentAny::FuturesContract(futures_contract_es(Some(activation), Some(expiration)))
}

// -- HELPERS ---------------------------------------------------------------------------------
#[fixture]
fn engine_config() -> OrderMatchingEngineConfig {
OrderMatchingEngineConfig {
bar_execution: false,
reject_stop_orders: false,
support_gtd_orders: false,
support_contingent_orders: true,
use_position_ids: false,
use_random_ids: false,
use_reduce_only: true,
}
}
// -- HELPERS ---------------------------------------------------------------------------

fn get_order_matching_engine(
instrument: InstrumentAny,
Expand Down Expand Up @@ -1113,6 +1146,9 @@ mod tests {
None,
None,
None,
None,
None,
None,
);

engine.process_order(&stop_order, account_id);
Expand Down Expand Up @@ -1178,29 +1214,20 @@ mod tests {
account_id: AccountId,
time: AtomicTime,
instrument_es: InstrumentAny,
engine_config: OrderMatchingEngineConfig,
) {
// Register saving message handler to exec engine endpoint
msgbus.register(
msgbus.switchboard.exec_engine_process,
order_event_handler.clone(),
);

// Create engine (with reduce_only option) and process order
let config = OrderMatchingEngineConfig {
use_reduce_only: true,
bar_execution: false,
reject_stop_orders: false,
support_gtd_orders: false,
support_contingent_orders: false,
use_position_ids: false,
use_random_ids: false,
};
let mut engine = get_order_matching_engine(
instrument_es.clone(),
Rc::new(msgbus),
None,
None,
Some(config),
Some(engine_config),
);
let market_order = TestOrderStubs::market_order_reduce(
instrument_es.id(),
Expand Down Expand Up @@ -1230,30 +1257,21 @@ mod tests {
account_id: AccountId,
time: AtomicTime,
instrument_es: InstrumentAny,
engine_config: OrderMatchingEngineConfig,
) {
// Register saving message handler to exec engine endpoint
msgbus.register(
msgbus.switchboard.exec_engine_process,
order_event_handler.clone(),
);

// Create engine (with reduce_only option) and process order
let config = OrderMatchingEngineConfig {
use_reduce_only: false,
bar_execution: false,
reject_stop_orders: false,
support_gtd_orders: false,
support_contingent_orders: true,
use_position_ids: false,
use_random_ids: false,
};
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = get_order_matching_engine(
instrument_es.clone(),
Rc::new(msgbus),
Some(cache.clone()),
None,
Some(config),
Some(engine_config),
);

let entry_client_order_id = ClientOrderId::from("O-19700101-000000-001-001-1");
Expand Down Expand Up @@ -1291,35 +1309,17 @@ mod tests {
.unwrap();

// Create stop loss order
let stop_order = OrderAny::StopMarket(
StopMarketOrder::new(
entry_order.trader_id(),
entry_order.strategy_id(),
entry_order.instrument_id(),
stop_loss_client_order_id,
OrderSide::Sell,
entry_order.quantity(),
Price::from("0.95"),
TriggerType::BidAsk,
TimeInForce::Gtc,
None,
true,
false,
None,
None,
None,
Some(ContingencyType::Oto),
None,
None,
Some(entry_client_order_id), // <- parent order id set from entry order
None,
None,
None,
None,
UUID4::new(),
UnixNanos::default(),
)
.unwrap(),
let stop_order = TestOrderStubs::stop_market_order(
instrument_es.id(),
OrderSide::Sell,
Price::from("0.95"),
Quantity::from(1),
None,
Some(ContingencyType::Oto),
Some(stop_loss_client_order_id),
None,
Some(entry_client_order_id),
None,
);
// Make it Accepted
let accepted_stop_order = TestOrderStubs::make_accepted_order(&stop_order);
Expand All @@ -1343,4 +1343,90 @@ mod tests {
Ustr::from(format!("Rejected OTO order from {entry_client_order_id}").as_str())
);
}

#[rstest]
fn test_process_order_when_closed_linked_order(
mut msgbus: MessageBus,
order_event_handler: ShareableMessageHandler,
account_id: AccountId,
time: AtomicTime,
instrument_es: InstrumentAny,
engine_config: OrderMatchingEngineConfig,
) {
// Register saving message handler to exec engine endpoint
msgbus.register(
msgbus.switchboard.exec_engine_process,
order_event_handler.clone(),
);

let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = get_order_matching_engine(
instrument_es.clone(),
Rc::new(msgbus),
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");
// Create two linked orders: stop loss and take profit
let mut stop_loss_order = TestOrderStubs::stop_market_order(
instrument_es.id(),
OrderSide::Sell,
Price::from("0.95"),
Quantity::from(1),
None,
Some(ContingencyType::Oco),
Some(stop_loss_client_order_id),
None,
None,
Some(vec![take_profit_client_order_id]),
);
let take_profit_order = TestOrderStubs::market_if_touched_order(
instrument_es.id(),
OrderSide::Sell,
Price::from("1.1"),
Quantity::from(1),
None,
Some(ContingencyType::Oco),
Some(take_profit_client_order_id),
None,
Some(vec![stop_loss_client_order_id]),
);
// Set stop loss order status to Rejected with proper event
let rejected_event: OrderRejected = OrderRejectedBuilder::default()
.client_order_id(stop_loss_client_order_id)
.reason(Ustr::from("Rejected"))
.build()
.unwrap();
stop_loss_order
.apply(OrderEventAny::Rejected(rejected_event))
.unwrap();

// Make take profit order Accepted
let accepted_take_profit = TestOrderStubs::make_accepted_order(&take_profit_order);

// 1. save stop loss order in cache which is rejected and closed is set to true
// 2. send the take profit order which has linked the stop loss 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(&accepted_take_profit, account_id);

// Get messages and test
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()
)
);
}
}
6 changes: 6 additions & 0 deletions nautilus_core/execution/src/matching_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,9 @@ mod tests {
None,
None,
None,
None,
None,
None,
);

let result = matching_core.is_stop_matched(&order.into());
Expand Down Expand Up @@ -523,6 +526,9 @@ mod tests {
None,
None,
None,
None,
None,
None,
);

matching_core.match_stop_order(&order.clone().into());
Expand Down
15 changes: 15 additions & 0 deletions nautilus_core/model/src/orders/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,21 @@ impl OrderAny {
Self::TrailingStopMarket(order) => order.contingency_type,
}
}

#[must_use]
pub fn linked_order_ids(&self) -> Option<Vec<ClientOrderId>> {
match self {
Self::Limit(order) => order.linked_order_ids.clone(),
Self::LimitIfTouched(order) => order.linked_order_ids.clone(),
Self::Market(order) => order.linked_order_ids.clone(),
Self::MarketIfTouched(order) => order.linked_order_ids.clone(),
Self::MarketToLimit(order) => order.linked_order_ids.clone(),
Self::StopLimit(order) => order.linked_order_ids.clone(),
Self::StopMarket(order) => order.linked_order_ids.clone(),
Self::TrailingStopLimit(order) => order.linked_order_ids.clone(),
Self::TrailingStopMarket(order) => order.linked_order_ids.clone(),
}
}
}

impl PartialEq for OrderAny {
Expand Down
Loading

0 comments on commit 3d6bce3

Please sign in to comment.