Skip to content

Commit b01a469

Browse files
committed
implement limit order filling for OrderMatchingEngine
1 parent bee3511 commit b01a469

File tree

2 files changed

+345
-10
lines changed

2 files changed

+345
-10
lines changed

nautilus_core/backtest/src/matching_engine/mod.rs

Lines changed: 226 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ use nautilus_model::{
5454
instruments::{InstrumentAny, EXPIRING_INSTRUMENT_TYPES},
5555
orderbook::OrderBook,
5656
orders::{
57-
OrderAny, PassiveOrderAny, StopOrderAny, TrailingStopLimitOrder, TrailingStopMarketOrder,
57+
LimitOrderAny, OrderAny, PassiveOrderAny, StopOrderAny, TrailingStopLimitOrder,
58+
TrailingStopMarketOrder,
5859
},
5960
position::Position,
6061
types::{
@@ -618,8 +619,8 @@ impl OrderMatchingEngine {
618619
)
619620
.into(),
620621
);
622+
return;
621623
}
622-
return;
623624
}
624625

625626
// Check for valid order trigger price precision
@@ -737,8 +738,53 @@ impl OrderMatchingEngine {
737738
self.fill_market_order(order);
738739
}
739740

740-
fn process_limit_order(&mut self, order: &OrderAny) {
741-
todo!("process_limit_order")
741+
fn process_limit_order(&mut self, order: &mut OrderAny) {
742+
if order.is_post_only()
743+
&& self
744+
.core
745+
.is_limit_matched(&LimitOrderAny::from(order.to_owned()))
746+
{
747+
self.generate_order_rejected(
748+
order,
749+
format!(
750+
"POST_ONLY {} {} order limit px of {} would have been a TAKER: bid={}, ask={}",
751+
order.order_type(),
752+
order.order_side(),
753+
order.price().unwrap(),
754+
self.core
755+
.bid
756+
.map(|p| p.to_string())
757+
.unwrap_or_else(|| "None".to_string()),
758+
self.core
759+
.ask
760+
.map(|p| p.to_string())
761+
.unwrap_or_else(|| "None".to_string())
762+
)
763+
.into(),
764+
);
765+
return;
766+
}
767+
768+
// Order is valid and accepted
769+
self.accept_order(order);
770+
771+
// Check for immediate fill
772+
if self
773+
.core
774+
.is_limit_matched(&LimitOrderAny::from(order.to_owned()))
775+
{
776+
// Filling as liquidity taker
777+
if order.liquidity_side().is_some()
778+
&& order.liquidity_side().unwrap() == LiquiditySide::NoLiquiditySide
779+
{
780+
order.set_liquidity_side(LiquiditySide::Taker)
781+
}
782+
self.fill_limit_order(order);
783+
} else if order.time_in_force() == TimeInForce::Fok
784+
|| order.time_in_force() == TimeInForce::Ioc
785+
{
786+
self.cancel_order(order, None);
787+
}
742788
}
743789

744790
fn process_market_to_limit_order(&mut self, order: &OrderAny) {
@@ -822,9 +868,18 @@ impl OrderMatchingEngine {
822868
}
823869

824870
// Move market back to targets
825-
self.core.bid = self.target_bid;
826-
self.core.ask = self.target_ask;
827-
self.core.last = self.target_last;
871+
if let Some(target_bid) = self.target_bid {
872+
self.core.bid = Some(target_bid);
873+
self.target_bid = None;
874+
}
875+
if let Some(target_ask) = self.target_ask {
876+
self.core.ask = Some(target_ask);
877+
self.target_ask = None;
878+
}
879+
if let Some(target_last) = self.target_last {
880+
self.core.last = Some(target_last);
881+
self.target_last = None;
882+
}
828883
}
829884

830885
// Reset any targets after iteration
@@ -833,8 +888,107 @@ impl OrderMatchingEngine {
833888
self.target_last = None;
834889
}
835890

836-
fn determine_limit_price_and_volume(&self, order: &OrderAny) {
837-
todo!("determine_limit_price_and_volume")
891+
fn determine_limit_price_and_volume(&mut self, order: &OrderAny) -> Vec<(Price, Quantity)> {
892+
match order.price() {
893+
Some(order_price) => {
894+
// construct book order with price as passive with limit order price
895+
let book_order =
896+
BookOrder::new(order.order_side(), order_price, order.quantity(), 1);
897+
898+
let mut fills = self.book.simulate_fills(&book_order);
899+
900+
// return immediately if no fills
901+
if fills.is_empty() {
902+
return fills;
903+
}
904+
905+
// check if trigger price exists
906+
if let Some(triggered_price) = order.trigger_price() {
907+
// Filling as TAKER from trigger
908+
if order
909+
.liquidity_side()
910+
.is_some_and(|liquidity_side| liquidity_side == LiquiditySide::Taker)
911+
{
912+
if order.order_side() == OrderSide::Sell && order_price > triggered_price {
913+
// manually change the fills index 0
914+
let first_fill = fills.first().unwrap();
915+
let triggered_qty = first_fill.1;
916+
fills[0] = (triggered_price, triggered_qty);
917+
self.target_bid = self.core.bid;
918+
self.target_ask = self.core.ask;
919+
self.target_last = self.core.last;
920+
self.core.set_ask_raw(order_price);
921+
self.core.set_last_raw(order_price);
922+
} else if order.order_side() == OrderSide::Buy
923+
&& order_price < triggered_price
924+
{
925+
// manually change the fills index 0
926+
let first_fill = fills.first().unwrap();
927+
let triggered_qty = first_fill.1;
928+
fills[0] = (triggered_price, triggered_qty);
929+
self.target_bid = self.core.bid;
930+
self.target_ask = self.core.ask;
931+
self.target_last = self.core.last;
932+
self.core.set_bid_raw(order_price);
933+
self.core.set_last_raw(order_price);
934+
}
935+
}
936+
}
937+
938+
// Filling as MAKER from trigger
939+
if order
940+
.liquidity_side()
941+
.is_some_and(|liquidity_side| liquidity_side == LiquiditySide::Maker)
942+
{
943+
if order.order_side() == OrderSide::Buy {
944+
let target_price = if order
945+
.trigger_price()
946+
.is_some_and(|trigger_price| order_price > trigger_price)
947+
{
948+
order.trigger_price().unwrap()
949+
} else {
950+
order_price
951+
};
952+
for fill in &fills {
953+
let last_px = fill.0;
954+
if last_px < order_price {
955+
// Marketable SELL would have filled at limit
956+
self.target_bid = self.core.bid;
957+
self.target_ask = self.core.ask;
958+
self.target_last = self.core.last;
959+
self.core.set_ask_raw(target_price);
960+
self.core.set_last_raw(target_price);
961+
}
962+
}
963+
} else if order.order_side() == OrderSide::Sell {
964+
let target_price = if order
965+
.trigger_price()
966+
.is_some_and(|trigger_price| order_price < trigger_price)
967+
{
968+
order.trigger_price().unwrap()
969+
} else {
970+
order_price
971+
};
972+
for fill in &fills {
973+
let last_px = fill.0;
974+
if last_px > order_price {
975+
// Marketable BUY would have filled at limit
976+
self.target_bid = self.core.bid;
977+
self.target_ask = self.core.ask;
978+
self.target_last = self.core.last;
979+
self.core.set_bid_raw(target_price);
980+
self.core.set_last_raw(target_price);
981+
}
982+
}
983+
} else {
984+
panic!("Invalid order side {}", order.order_side());
985+
}
986+
}
987+
988+
fills
989+
}
990+
None => panic!("Limit order must have a price"),
991+
}
838992
}
839993

840994
fn determine_market_price_and_volume(&self, order: &OrderAny) -> Vec<(Price, Quantity)> {
@@ -892,7 +1046,69 @@ impl OrderMatchingEngine {
8921046
}
8931047

8941048
fn fill_limit_order(&mut self, order: &OrderAny) {
895-
todo!("fill_limit_order")
1049+
match order.price() {
1050+
Some(order_price) => {
1051+
let cached_filled_qty = self.cached_filled_qty.get(&order.client_order_id());
1052+
if cached_filled_qty.is_some() && *cached_filled_qty.unwrap() >= order.quantity() {
1053+
log::debug!(
1054+
"Ignoring fill as already filled pending pending application of events: {}, {}, {}, {}",
1055+
cached_filled_qty.unwrap(),
1056+
order.quantity(),
1057+
order.filled_qty(),
1058+
order.leaves_qty(),
1059+
);
1060+
return;
1061+
}
1062+
1063+
if order
1064+
.liquidity_side()
1065+
.is_some_and(|liquidity_side| liquidity_side == LiquiditySide::Maker)
1066+
{
1067+
if order.order_side() == OrderSide::Buy
1068+
&& self.core.bid.is_some_and(|bid| bid == order_price)
1069+
&& !self.fill_model.is_limit_filled()
1070+
{
1071+
// no filled
1072+
return;
1073+
}
1074+
if order.order_side() == OrderSide::Sell
1075+
&& self.core.ask.is_some_and(|ask| ask == order_price)
1076+
&& !self.fill_model.is_limit_filled()
1077+
{
1078+
// no filled
1079+
return;
1080+
}
1081+
}
1082+
1083+
let venue_position_id = self.ids_generator.get_position_id(order, None);
1084+
let position = if let Some(venue_position_id) = venue_position_id {
1085+
let cache = self.cache.as_ref().borrow();
1086+
cache.position(&venue_position_id).cloned()
1087+
} else {
1088+
None
1089+
};
1090+
1091+
if self.config.use_reduce_only && order.is_reduce_only() && position.is_none() {
1092+
log::warn!(
1093+
"Canceling REDUCE_ONLY {} as would increase position",
1094+
order.order_type()
1095+
);
1096+
self.cancel_order(order, None);
1097+
return;
1098+
}
1099+
1100+
let fills = self.determine_limit_price_and_volume(order);
1101+
1102+
self.apply_fills(
1103+
order,
1104+
fills,
1105+
order.liquidity_side().unwrap(),
1106+
venue_position_id,
1107+
position,
1108+
);
1109+
}
1110+
None => panic!("Limit order must have a price"),
1111+
}
8961112
}
8971113

8981114
fn apply_fills(

0 commit comments

Comments
 (0)