nautilus_execution/matching_engine/
orders.rs1use std::{cell::RefCell, collections::VecDeque, rc::Rc};
19
20use indexmap::{IndexMap, IndexSet};
21use nautilus_common::cache::Cache;
22use nautilus_model::{
23 events::OrderUpdated,
24 identifiers::{AccountId, ClientOrderId, InstrumentId},
25 orders::Order,
26 types::Quantity,
27};
28
29#[derive(Debug, Clone, Default)]
43pub struct MatchingOrders {
44 pub(super) pending_updates: Rc<RefCell<IndexMap<ClientOrderId, Vec<OrderUpdated>>>>,
45 pub(super) cached_filled_qty: Rc<RefCell<IndexMap<ClientOrderId, Quantity>>>,
46 pub(super) pending_oto_order_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
47 pub(super) released_oto_order_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
48 pub(super) rejected_oto_parent_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
49 pub(super) closed_oto_order_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
50 pub(super) commands: Rc<RefCell<VecDeque<OtoCommand>>>,
51}
52
53#[derive(Debug, Clone)]
55pub enum OtoCommand {
56 Release {
58 instrument_id: InstrumentId,
59 client_order_id: ClientOrderId,
60 account_id: AccountId,
61 parent_instrument_id: InstrumentId,
62 parent_order_id: ClientOrderId,
63 parent_quantity: Option<Quantity>,
64 },
65 Resize {
67 instrument_id: InstrumentId,
68 parent_order_id: ClientOrderId,
69 quantity: Quantity,
70 },
71 Cancel {
76 instrument_id: InstrumentId,
77 client_order_id: ClientOrderId,
78 cancel_contingencies: bool,
79 excluded: Vec<ClientOrderId>,
80 },
81}
82
83impl OtoCommand {
84 pub fn instrument_id(&self) -> InstrumentId {
86 match self {
87 Self::Release { instrument_id, .. }
88 | Self::Resize { instrument_id, .. }
89 | Self::Cancel { instrument_id, .. } => *instrument_id,
90 }
91 }
92}
93
94impl MatchingOrders {
95 pub fn pop_command(&self) -> Option<OtoCommand> {
97 self.commands.borrow_mut().pop_front()
98 }
99
100 pub(super) fn reset(&self, instrument_id: InstrumentId, cache: &Cache) {
101 let keep = |id: &ClientOrderId| {
102 cache
103 .order(id)
104 .is_some_and(|order| order.instrument_id() != instrument_id)
105 };
106
107 self.pending_updates.borrow_mut().retain(|id, _| keep(id));
108 self.cached_filled_qty.borrow_mut().retain(|id, _| keep(id));
109 self.pending_oto_order_ids.borrow_mut().retain(keep);
110 self.released_oto_order_ids.borrow_mut().retain(keep);
111 self.rejected_oto_parent_ids.borrow_mut().retain(keep);
112 self.closed_oto_order_ids.borrow_mut().retain(keep);
113 self.commands.borrow_mut().retain(|command| {
114 command.instrument_id() != instrument_id && !matches!(command,
115 OtoCommand::Release { parent_instrument_id, .. } if *parent_instrument_id == instrument_id)
116 });
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use nautilus_model::{
123 enums::OrderType,
124 events::order::spec::OrderUpdatedSpec,
125 instruments::{CryptoPerpetual, Instrument, stubs::crypto_perpetual_ethusdt},
126 orders::OrderTestBuilder,
127 };
128 use rstest::rstest;
129
130 use super::*;
131
132 #[rstest]
133 fn test_reset_retains_other_instrument_state(crypto_perpetual_ethusdt: CryptoPerpetual) {
134 let first = crypto_perpetual_ethusdt.id();
135 let second = InstrumentId::from("BTCUSDT-PERP.BINANCE");
136 let first_id = ClientOrderId::from("FIRST");
137 let second_id = ClientOrderId::from("SECOND");
138 let orders = MatchingOrders::default();
139 let mut cache = Cache::default();
140 let update = OrderUpdatedSpec::builder()
141 .instrument_id(second)
142 .client_order_id(second_id)
143 .quantity(Quantity::from("4.560"))
144 .build();
145
146 for (instrument, id, qty) in [(first, first_id, "1.230"), (second, second_id, "4.560")] {
147 let order = OrderTestBuilder::new(OrderType::Market)
148 .instrument_id(instrument)
149 .client_order_id(id)
150 .quantity(Quantity::from(qty))
151 .build();
152 cache.add_order(order, None, None, false).unwrap();
153
154 let pending = if id == second_id {
155 update
156 } else {
157 OrderUpdatedSpec::builder()
158 .instrument_id(instrument)
159 .client_order_id(id)
160 .quantity(Quantity::from(qty))
161 .build()
162 };
163
164 orders
165 .pending_updates
166 .borrow_mut()
167 .insert(id, vec![pending]);
168 orders
169 .cached_filled_qty
170 .borrow_mut()
171 .insert(id, Quantity::from(qty));
172 orders.pending_oto_order_ids.borrow_mut().insert(id);
173 orders.released_oto_order_ids.borrow_mut().insert(id);
174 orders.rejected_oto_parent_ids.borrow_mut().insert(id);
175 orders.closed_oto_order_ids.borrow_mut().insert(id);
176 }
177
178 orders.commands.borrow_mut().push_back(OtoCommand::Release {
179 instrument_id: second,
180 client_order_id: second_id,
181 account_id: AccountId::from("SIM-001"),
182 parent_instrument_id: first,
183 parent_order_id: first_id,
184 parent_quantity: Some(Quantity::from("1.230")),
185 });
186
187 orders.commands.borrow_mut().push_back(OtoCommand::Cancel {
188 instrument_id: second,
189 client_order_id: second_id,
190 cancel_contingencies: true,
191 excluded: vec![first_id],
192 });
193
194 for (instrument_id, parent_order_id, quantity) in [
195 (first, first_id, Quantity::from("2.340")),
196 (second, second_id, Quantity::from("5.670")),
197 ] {
198 orders.commands.borrow_mut().push_back(OtoCommand::Resize {
199 instrument_id,
200 parent_order_id,
201 quantity,
202 });
203 }
204
205 orders.reset(first, &cache);
206 assert_eq!(
207 *orders.pending_updates.borrow(),
208 IndexMap::from([(second_id, vec![update])])
209 );
210 assert_eq!(
211 *orders.cached_filled_qty.borrow(),
212 IndexMap::from([(second_id, Quantity::from("4.560"))])
213 );
214 assert_eq!(
215 *orders.pending_oto_order_ids.borrow(),
216 IndexSet::from([second_id])
217 );
218 assert_eq!(
219 *orders.released_oto_order_ids.borrow(),
220 IndexSet::from([second_id])
221 );
222 assert_eq!(
223 *orders.rejected_oto_parent_ids.borrow(),
224 IndexSet::from([second_id])
225 );
226 assert_eq!(
227 *orders.closed_oto_order_ids.borrow(),
228 IndexSet::from([second_id])
229 );
230
231 let OtoCommand::Cancel {
232 instrument_id,
233 client_order_id,
234 cancel_contingencies,
235 excluded,
236 } = orders.pop_command().unwrap()
237 else {
238 panic!("Expected remaining peer cancellation");
239 };
240
241 assert_eq!(instrument_id, second);
242 assert_eq!(client_order_id, second_id);
243 assert!(cancel_contingencies);
244 assert_eq!(excluded, vec![first_id]);
245
246 let OtoCommand::Resize {
247 instrument_id,
248 parent_order_id,
249 quantity,
250 } = orders.pop_command().unwrap()
251 else {
252 panic!("Expected remaining peer resize");
253 };
254
255 assert_eq!(instrument_id, second);
256 assert_eq!(parent_order_id, second_id);
257 assert_eq!(quantity, Quantity::from("5.670"));
258 assert!(orders.pop_command().is_none());
259 }
260}