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