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