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::{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/// Lightweight identifier container for a group of related orders.
35///
36/// Stores only the order IDs - full order data lives in the cache.
37/// For serialization payload, see `SubmitOrderList.order_inits`.
38#[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    /// Creates a new [`OrderList`] instance.
53    ///
54    /// # Panics
55    ///
56    /// Panics if:
57    /// - `orders` is empty.
58    /// - `orders` contains duplicate client order IDs.
59    #[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    /// Creates a new [`OrderList`] from a slice of orders.
84    ///
85    /// Derives `order_list_id`, `instrument_id`, `strategy_id` and `trader_id`
86    /// from the first order.
87    ///
88    /// # Panics
89    ///
90    /// Panics if:
91    /// - `orders` is empty.
92    /// - Any order has `None` for `order_list_id`.
93    /// - Any order has a different `order_list_id` than the first.
94    /// - Any order has a different `trader_id` than the first.
95    /// - Any order has a different `instrument_id` than the first.
96    /// - Any order has a different `strategy_id` than the first.
97    /// - Orders contain duplicate client order IDs.
98    #[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    /// Returns the number of orders in the list.
172    #[must_use]
173    pub fn len(&self) -> usize {
174        self.client_order_ids.len()
175    }
176
177    /// Returns true if the list contains no orders.
178    #[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}