Skip to main content

nautilus_model/orders/
list.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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/// Lightweight identifier container for a group of related orders.
37///
38/// Stores only the order IDs - full order data lives in the cache.
39/// For serialization payload, see `SubmitOrderList.order_inits`.
40#[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    /// Creates a new [`OrderList`] instance.
55    ///
56    /// # Panics
57    ///
58    /// Panics if:
59    /// - `orders` is empty.
60    /// - `orders` contains duplicate client order IDs.
61    #[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    /// Creates a new [`OrderList`] from a slice of orders.
87    ///
88    /// Derives `order_list_id`, `instrument_id`, `strategy_id` and `trader_id`
89    /// from the first order.
90    ///
91    /// # Panics
92    ///
93    /// Panics if:
94    /// - `orders` is empty.
95    /// - Any order has `None` for `order_list_id`.
96    /// - Any order has a different `order_list_id` than the first.
97    /// - Any order has a different `trader_id` than the first.
98    /// - Any order has a different `instrument_id` than the first.
99    /// - Any order has a different `strategy_id` than the first.
100    /// - Orders contain duplicate client order IDs.
101    #[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    /// Returns the number of orders in the list.
175    #[must_use]
176    pub fn len(&self) -> usize {
177        self.client_order_ids.len()
178    }
179
180    /// Returns true if the list contains no orders.
181    #[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}