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