1use std::{
17 collections::HashSet,
18 fmt::Display,
19 hash::{Hash, Hasher},
20};
21
22use ahash::AHashSet;
23use nautilus_core::{
24 UnixNanos,
25 correctness::{check_equal, check_predicate_true, check_slice_not_empty},
26};
27use serde::{Deserialize, Serialize};
28
29use crate::{
30 identifiers::{ClientOrderId, InstrumentId, OrderListId, StrategyId},
31 orders::{Order, OrderAny},
32};
33
34#[derive(Clone, Eq, Debug, Serialize, Deserialize)]
39#[cfg_attr(
40 feature = "python",
41 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
42)]
43pub struct OrderList {
44 pub id: OrderListId,
45 pub instrument_id: InstrumentId,
46 pub strategy_id: StrategyId,
47 pub client_order_ids: Vec<ClientOrderId>,
48 pub ts_init: UnixNanos,
49}
50
51impl OrderList {
52 #[must_use]
60 pub fn new(
61 order_list_id: OrderListId,
62 instrument_id: InstrumentId,
63 strategy_id: StrategyId,
64 client_order_ids: Vec<ClientOrderId>,
65 ts_init: UnixNanos,
66 ) -> Self {
67 check_slice_not_empty(client_order_ids.as_slice(), stringify!(client_order_ids)).unwrap();
68 let unique: HashSet<&ClientOrderId> = client_order_ids.iter().collect();
69 check_predicate_true(
70 unique.len() == client_order_ids.len(),
71 "client_order_ids must not contain duplicates",
72 )
73 .unwrap();
74 Self {
75 id: order_list_id,
76 instrument_id,
77 strategy_id,
78 client_order_ids,
79 ts_init,
80 }
81 }
82
83 #[must_use]
99 pub fn from_orders(orders: &[OrderAny], ts_init: UnixNanos) -> Self {
100 check_slice_not_empty(orders, stringify!(orders)).unwrap();
101
102 let first = &orders[0];
103 let order_list_id = first
104 .order_list_id()
105 .expect("First order must have order_list_id");
106 let trader_id = first.trader_id();
107 let instrument_id = first.instrument_id();
108 let strategy_id = first.strategy_id();
109
110 let mut seen_ids: AHashSet<ClientOrderId> = AHashSet::new();
111 seen_ids.insert(first.client_order_id());
112
113 for order in orders.iter().skip(1) {
114 let other_list_id = order
115 .order_list_id()
116 .expect("All orders must have order_list_id");
117 check_equal(
118 &other_list_id,
119 &order_list_id,
120 "order_list_id",
121 "first order order_list_id",
122 )
123 .unwrap();
124 check_equal(
125 &order.trader_id(),
126 &trader_id,
127 "trader_id",
128 "first order trader_id",
129 )
130 .unwrap();
131 check_equal(
132 &order.instrument_id(),
133 &instrument_id,
134 "instrument_id",
135 "first order instrument_id",
136 )
137 .unwrap();
138 check_equal(
139 &order.strategy_id(),
140 &strategy_id,
141 "strategy_id",
142 "first order strategy_id",
143 )
144 .unwrap();
145 check_predicate_true(
146 seen_ids.insert(order.client_order_id()),
147 &format!(
148 "duplicate client_order_id {} in order list",
149 order.client_order_id()
150 ),
151 )
152 .unwrap();
153 }
154
155 let client_order_ids = orders.iter().map(|o| o.client_order_id()).collect();
156
157 Self {
158 id: order_list_id,
159 instrument_id,
160 strategy_id,
161 client_order_ids,
162 ts_init,
163 }
164 }
165
166 #[must_use]
167 pub fn first(&self) -> Option<&ClientOrderId> {
168 self.client_order_ids.first()
169 }
170
171 #[must_use]
173 pub fn len(&self) -> usize {
174 self.client_order_ids.len()
175 }
176
177 #[must_use]
179 pub fn is_empty(&self) -> bool {
180 self.client_order_ids.is_empty()
181 }
182}
183
184impl PartialEq for OrderList {
185 fn eq(&self, other: &Self) -> bool {
186 self.id == other.id
187 }
188}
189
190impl Hash for OrderList {
191 fn hash<H: Hasher>(&self, state: &mut H) {
192 self.id.hash(state);
193 }
194}
195
196impl Display for OrderList {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 write!(
199 f,
200 "OrderList(\
201 id={}, \
202 instrument_id={}, \
203 strategy_id={}, \
204 client_order_ids={:?}, \
205 ts_init={}\
206 )",
207 self.id, self.instrument_id, self.strategy_id, self.client_order_ids, self.ts_init,
208 )
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use std::collections::hash_map::DefaultHasher;
215
216 use rstest::rstest;
217
218 use super::*;
219 use crate::{
220 enums::OrderType,
221 identifiers::{InstrumentId, OrderListId, TraderId},
222 orders::builder::OrderTestBuilder,
223 types::Quantity,
224 };
225
226 fn create_client_order_ids(count: usize) -> Vec<ClientOrderId> {
227 (0..count)
228 .map(|i| ClientOrderId::from(format!("O-00{}", i + 1).as_str()))
229 .collect()
230 }
231
232 fn create_orders(count: usize, order_list_id: OrderListId) -> Vec<OrderAny> {
233 (0..count)
234 .map(|i| {
235 OrderTestBuilder::new(OrderType::Market)
236 .instrument_id(InstrumentId::from("AUD/USD.SIM"))
237 .client_order_id(ClientOrderId::from(format!("O-00{}", i + 1).as_str()))
238 .order_list_id(order_list_id)
239 .quantity(Quantity::from(1))
240 .build()
241 })
242 .collect()
243 }
244
245 #[rstest]
246 fn test_new_and_display() {
247 let orders = create_client_order_ids(3);
248
249 let order_list = OrderList::new(
250 OrderListId::from("OL-001"),
251 InstrumentId::from("AUD/USD.SIM"),
252 StrategyId::from("S-001"),
253 orders,
254 UnixNanos::default(),
255 );
256
257 assert!(order_list.to_string().starts_with(
258 "OrderList(id=OL-001, instrument_id=AUD/USD.SIM, strategy_id=S-001, client_order_ids="
259 ));
260 }
261
262 #[rstest]
263 #[should_panic(
264 expected = "called `Result::unwrap()` on an `Err` value: the 'client_order_ids'"
265 )]
266 fn test_order_list_creation_with_empty_orders() {
267 let orders: Vec<ClientOrderId> = vec![];
268
269 let _ = OrderList::new(
270 OrderListId::from("OL-004"),
271 InstrumentId::from("AUD/USD.SIM"),
272 StrategyId::from("S-001"),
273 orders,
274 UnixNanos::default(),
275 );
276 }
277
278 #[rstest]
279 fn test_from_orders() {
280 let order_list_id = OrderListId::from("OL-002");
281 let orders = create_orders(3, order_list_id);
282
283 let order_list = OrderList::from_orders(&orders, UnixNanos::default());
284
285 assert_eq!(order_list.id, order_list_id);
286 assert_eq!(order_list.len(), 3);
287 assert_eq!(order_list.instrument_id, InstrumentId::from("AUD/USD.SIM"));
288 assert_eq!(order_list.client_order_ids[0], ClientOrderId::from("O-001"));
289 }
290
291 #[rstest]
292 fn test_order_list_equality() {
293 let orders = create_client_order_ids(1);
294
295 let order_list1 = OrderList::new(
296 OrderListId::from("OL-006"),
297 InstrumentId::from("AUD/USD.SIM"),
298 StrategyId::from("S-001"),
299 orders.clone(),
300 UnixNanos::default(),
301 );
302
303 let order_list2 = OrderList::new(
304 OrderListId::from("OL-006"),
305 InstrumentId::from("AUD/USD.SIM"),
306 StrategyId::from("S-001"),
307 orders,
308 UnixNanos::default(),
309 );
310
311 assert_eq!(order_list1, order_list2);
312 }
313
314 #[rstest]
315 fn test_order_list_inequality() {
316 let orders = create_client_order_ids(1);
317
318 let order_list1 = OrderList::new(
319 OrderListId::from("OL-007"),
320 InstrumentId::from("AUD/USD.SIM"),
321 StrategyId::from("S-001"),
322 orders.clone(),
323 UnixNanos::default(),
324 );
325
326 let order_list2 = OrderList::new(
327 OrderListId::from("OL-008"),
328 InstrumentId::from("AUD/USD.SIM"),
329 StrategyId::from("S-001"),
330 orders,
331 UnixNanos::default(),
332 );
333
334 assert_ne!(order_list1, order_list2);
335 }
336
337 #[rstest]
338 fn test_order_list_first() {
339 let orders = create_client_order_ids(2);
340 let first_id = orders[0];
341
342 let order_list = OrderList::new(
343 OrderListId::from("OL-009"),
344 InstrumentId::from("AUD/USD.SIM"),
345 StrategyId::from("S-001"),
346 orders,
347 UnixNanos::default(),
348 );
349
350 let first = order_list.first();
351 assert!(first.is_some());
352 assert_eq!(*first.unwrap(), first_id);
353 }
354
355 #[rstest]
356 fn test_order_list_len() {
357 let orders = create_client_order_ids(3);
358
359 let order_list = OrderList::new(
360 OrderListId::from("OL-010"),
361 InstrumentId::from("AUD/USD.SIM"),
362 StrategyId::from("S-001"),
363 orders,
364 UnixNanos::default(),
365 );
366
367 assert_eq!(order_list.len(), 3);
368 assert!(!order_list.is_empty());
369 }
370
371 #[rstest]
372 fn test_order_list_hash() {
373 let orders = create_client_order_ids(1);
374
375 let order_list1 = OrderList::new(
376 OrderListId::from("OL-011"),
377 InstrumentId::from("AUD/USD.SIM"),
378 StrategyId::from("S-001"),
379 orders.clone(),
380 UnixNanos::default(),
381 );
382
383 let order_list2 = OrderList::new(
384 OrderListId::from("OL-011"),
385 InstrumentId::from("AUD/USD.SIM"),
386 StrategyId::from("S-001"),
387 orders,
388 UnixNanos::default(),
389 );
390
391 let mut hasher1 = DefaultHasher::new();
392 let mut hasher2 = DefaultHasher::new();
393 order_list1.hash(&mut hasher1);
394 order_list2.hash(&mut hasher2);
395
396 assert_eq!(hasher1.finish(), hasher2.finish());
397 }
398
399 #[rstest]
400 #[should_panic(expected = "client_order_ids must not contain duplicates")]
401 fn test_new_with_duplicate_client_order_ids() {
402 let id = ClientOrderId::from("O-001");
403 let _ = OrderList::new(
404 OrderListId::from("OL-012"),
405 InstrumentId::from("AUD/USD.SIM"),
406 StrategyId::from("S-001"),
407 vec![id, id],
408 UnixNanos::default(),
409 );
410 }
411
412 #[rstest]
413 #[should_panic(expected = "duplicate client_order_id O-001 in order list")]
414 fn test_from_orders_with_duplicate_client_order_ids() {
415 let order_list_id = OrderListId::from("OL-013");
416 let order = OrderTestBuilder::new(OrderType::Market)
417 .instrument_id(InstrumentId::from("AUD/USD.SIM"))
418 .client_order_id(ClientOrderId::from("O-001"))
419 .order_list_id(order_list_id)
420 .quantity(Quantity::from(1))
421 .build();
422 let _ = OrderList::from_orders(&[order.clone(), order], UnixNanos::default());
423 }
424
425 #[rstest]
426 #[should_panic(expected = "trader_id")]
427 fn test_from_orders_with_mismatched_trader_id() {
428 let order_list_id = OrderListId::from("OL-014");
429 let order1 = OrderTestBuilder::new(OrderType::Market)
430 .trader_id(TraderId::from("TRADER-001"))
431 .instrument_id(InstrumentId::from("AUD/USD.SIM"))
432 .client_order_id(ClientOrderId::from("O-001"))
433 .order_list_id(order_list_id)
434 .quantity(Quantity::from(1))
435 .build();
436 let order2 = OrderTestBuilder::new(OrderType::Market)
437 .trader_id(TraderId::from("TRADER-002"))
438 .instrument_id(InstrumentId::from("AUD/USD.SIM"))
439 .client_order_id(ClientOrderId::from("O-002"))
440 .order_list_id(order_list_id)
441 .quantity(Quantity::from(1))
442 .build();
443 let _ = OrderList::from_orders(&[order1, order2], UnixNanos::default());
444 }
445
446 #[rstest]
447 #[should_panic(expected = "instrument_id")]
448 fn test_from_orders_with_mismatched_instrument_id() {
449 let order_list_id = OrderListId::from("OL-015");
450 let order1 = OrderTestBuilder::new(OrderType::Market)
451 .instrument_id(InstrumentId::from("AUD/USD.SIM"))
452 .client_order_id(ClientOrderId::from("O-001"))
453 .order_list_id(order_list_id)
454 .quantity(Quantity::from(1))
455 .build();
456 let order2 = OrderTestBuilder::new(OrderType::Market)
457 .instrument_id(InstrumentId::from("EUR/USD.SIM"))
458 .client_order_id(ClientOrderId::from("O-002"))
459 .order_list_id(order_list_id)
460 .quantity(Quantity::from(1))
461 .build();
462 let _ = OrderList::from_orders(&[order1, order2], UnixNanos::default());
463 }
464
465 #[rstest]
466 #[should_panic(expected = "strategy_id")]
467 fn test_from_orders_with_mismatched_strategy_id() {
468 let order_list_id = OrderListId::from("OL-016");
469 let order1 = OrderTestBuilder::new(OrderType::Market)
470 .instrument_id(InstrumentId::from("AUD/USD.SIM"))
471 .strategy_id(StrategyId::from("S-001"))
472 .client_order_id(ClientOrderId::from("O-001"))
473 .order_list_id(order_list_id)
474 .quantity(Quantity::from(1))
475 .build();
476 let order2 = OrderTestBuilder::new(OrderType::Market)
477 .instrument_id(InstrumentId::from("AUD/USD.SIM"))
478 .strategy_id(StrategyId::from("S-002"))
479 .client_order_id(ClientOrderId::from("O-002"))
480 .order_list_id(order_list_id)
481 .quantity(Quantity::from(1))
482 .build();
483 let _ = OrderList::from_orders(&[order1, order2], UnixNanos::default());
484 }
485}