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    fmt::Display,
18    hash::{Hash, Hasher},
19};
20
21use nautilus_core::{UnixNanos, correctness::check_slice_not_empty};
22use serde::{Deserialize, Serialize};
23
24use super::{Order, OrderAny};
25use crate::identifiers::{InstrumentId, OrderListId, StrategyId};
26
27#[derive(Clone, Eq, Debug, Serialize, Deserialize)]
28#[cfg_attr(
29    feature = "python",
30    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
31)]
32pub struct OrderList {
33    pub id: OrderListId,
34    pub instrument_id: InstrumentId,
35    pub strategy_id: StrategyId,
36    pub orders: Vec<OrderAny>,
37    pub ts_init: UnixNanos,
38}
39
40impl OrderList {
41    /// Creates a new [`OrderList`] instance.
42    ///
43    /// # Panics
44    ///
45    /// Panics if `orders` is empty or if any order's instrument or strategy ID does not match.
46    pub fn new(
47        order_list_id: OrderListId,
48        instrument_id: InstrumentId,
49        strategy_id: StrategyId,
50        orders: Vec<OrderAny>,
51        ts_init: UnixNanos,
52    ) -> Self {
53        check_slice_not_empty(orders.as_slice(), stringify!(orders)).unwrap();
54        for order in &orders {
55            assert_eq!(instrument_id, order.instrument_id());
56            assert_eq!(strategy_id, order.strategy_id());
57        }
58        Self {
59            id: order_list_id,
60            instrument_id,
61            strategy_id,
62            orders,
63            ts_init,
64        }
65    }
66
67    /// Returns a reference to the first order in the list.
68    #[must_use]
69    pub fn first(&self) -> Option<&OrderAny> {
70        self.orders.first()
71    }
72
73    /// Returns the number of orders in the list.
74    #[must_use]
75    pub fn len(&self) -> usize {
76        self.orders.len()
77    }
78
79    /// Returns true if the list contains no orders.
80    #[must_use]
81    pub fn is_empty(&self) -> bool {
82        self.orders.is_empty()
83    }
84}
85
86impl PartialEq for OrderList {
87    fn eq(&self, other: &Self) -> bool {
88        self.id == other.id
89    }
90}
91
92impl Hash for OrderList {
93    fn hash<H: Hasher>(&self, state: &mut H) {
94        self.id.hash(state);
95    }
96}
97
98impl Display for OrderList {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        write!(
101            f,
102            "OrderList(\
103            id={}, \
104            instrument_id={}, \
105            strategy_id={}, \
106            orders={:?}, \
107            ts_init={}\
108            )",
109            self.id, self.instrument_id, self.strategy_id, self.orders, self.ts_init,
110        )
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use std::collections::hash_map::DefaultHasher;
117
118    use rstest::rstest;
119
120    use super::*;
121    use crate::{
122        enums::{OrderSide, OrderType},
123        identifiers::{OrderListId, StrategyId},
124        instruments::{CurrencyPair, stubs::*},
125        orders::OrderTestBuilder,
126        stubs::TestDefault,
127        types::{Price, Quantity},
128    };
129
130    #[rstest]
131    fn test_new_and_display(audusd_sim: CurrencyPair) {
132        let order1 = OrderTestBuilder::new(OrderType::Limit)
133            .instrument_id(audusd_sim.id)
134            .side(OrderSide::Buy)
135            .price(Price::from("1.00000"))
136            .quantity(Quantity::from(100_000))
137            .build();
138        let order2 = OrderTestBuilder::new(OrderType::Limit)
139            .instrument_id(audusd_sim.id)
140            .side(OrderSide::Buy)
141            .price(Price::from("1.00000"))
142            .quantity(Quantity::from(100_000))
143            .build();
144        let order3 = OrderTestBuilder::new(OrderType::Limit)
145            .instrument_id(audusd_sim.id)
146            .side(OrderSide::Buy)
147            .price(Price::from("1.00000"))
148            .quantity(Quantity::from(100_000))
149            .build();
150
151        let orders = vec![order1, order2, order3];
152
153        let order_list = OrderList::new(
154            OrderListId::from("OL-001"),
155            audusd_sim.id,
156            StrategyId::test_default(),
157            orders,
158            UnixNanos::default(),
159        );
160
161        assert!(order_list.to_string().starts_with(
162            "OrderList(id=OL-001, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders="
163        ));
164    }
165
166    #[rstest]
167    #[should_panic(expected = "assertion `left == right` failed")]
168    fn test_order_list_creation_with_mismatched_instrument_id(audusd_sim: CurrencyPair) {
169        let order1 = OrderTestBuilder::new(OrderType::Limit)
170            .instrument_id(audusd_sim.id)
171            .side(OrderSide::Buy)
172            .price(Price::from("1.00000"))
173            .quantity(Quantity::from(100_000))
174            .build();
175        let order2 = OrderTestBuilder::new(OrderType::Limit)
176            .instrument_id(InstrumentId::from("EUR/USD.SIM"))
177            .side(OrderSide::Sell)
178            .price(Price::from("1.01000"))
179            .quantity(Quantity::from(50_000))
180            .build();
181
182        let orders = vec![order1, order2];
183
184        // This should panic because the instrument IDs do not match
185        OrderList::new(
186            OrderListId::from("OL-003"),
187            audusd_sim.id,
188            StrategyId::test_default(),
189            orders,
190            UnixNanos::default(),
191        );
192    }
193
194    #[rstest]
195    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: the 'orders' slice")]
196    fn test_order_list_creation_with_empty_orders(audusd_sim: CurrencyPair) {
197        let orders: Vec<OrderAny> = vec![];
198
199        // This should panic because the orders list is empty
200        OrderList::new(
201            OrderListId::from("OL-004"),
202            audusd_sim.id,
203            StrategyId::test_default(),
204            orders,
205            UnixNanos::default(),
206        );
207    }
208
209    #[rstest]
210    fn test_order_list_equality(audusd_sim: CurrencyPair) {
211        let order1 = OrderTestBuilder::new(OrderType::Limit)
212            .instrument_id(audusd_sim.id)
213            .side(OrderSide::Buy)
214            .price(Price::from("1.00000"))
215            .quantity(Quantity::from(100_000))
216            .build();
217
218        let orders = vec![order1];
219
220        let order_list1 = OrderList::new(
221            OrderListId::from("OL-006"),
222            audusd_sim.id,
223            StrategyId::test_default(),
224            orders.clone(),
225            UnixNanos::default(),
226        );
227
228        let order_list2 = OrderList::new(
229            OrderListId::from("OL-006"),
230            audusd_sim.id,
231            StrategyId::test_default(),
232            orders,
233            UnixNanos::default(),
234        );
235
236        assert_eq!(order_list1, order_list2);
237    }
238
239    #[rstest]
240    fn test_order_list_inequality(audusd_sim: CurrencyPair) {
241        let order1 = OrderTestBuilder::new(OrderType::Limit)
242            .instrument_id(audusd_sim.id)
243            .side(OrderSide::Buy)
244            .price(Price::from("1.00000"))
245            .quantity(Quantity::from(100_000))
246            .build();
247
248        let orders = vec![order1];
249
250        let order_list1 = OrderList::new(
251            OrderListId::from("OL-007"),
252            audusd_sim.id,
253            StrategyId::test_default(),
254            orders.clone(),
255            UnixNanos::default(),
256        );
257
258        let order_list2 = OrderList::new(
259            OrderListId::from("OL-008"),
260            audusd_sim.id,
261            StrategyId::test_default(),
262            orders,
263            UnixNanos::default(),
264        );
265
266        assert_ne!(order_list1, order_list2);
267    }
268
269    #[rstest]
270    fn test_order_list_first(audusd_sim: CurrencyPair) {
271        let order1 = OrderTestBuilder::new(OrderType::Limit)
272            .instrument_id(audusd_sim.id)
273            .side(OrderSide::Buy)
274            .price(Price::from("1.00000"))
275            .quantity(Quantity::from(100_000))
276            .build();
277        let order2 = OrderTestBuilder::new(OrderType::Limit)
278            .instrument_id(audusd_sim.id)
279            .side(OrderSide::Sell)
280            .price(Price::from("1.01000"))
281            .quantity(Quantity::from(50_000))
282            .build();
283
284        let first_order_id = order1.client_order_id();
285        let orders = vec![order1, order2];
286
287        let order_list = OrderList::new(
288            OrderListId::from("OL-009"),
289            audusd_sim.id,
290            StrategyId::test_default(),
291            orders,
292            UnixNanos::default(),
293        );
294
295        let first = order_list.first();
296        assert!(first.is_some());
297        assert_eq!(first.unwrap().client_order_id(), first_order_id);
298    }
299
300    #[rstest]
301    fn test_order_list_len(audusd_sim: CurrencyPair) {
302        let order1 = OrderTestBuilder::new(OrderType::Limit)
303            .instrument_id(audusd_sim.id)
304            .side(OrderSide::Buy)
305            .price(Price::from("1.00000"))
306            .quantity(Quantity::from(100_000))
307            .build();
308        let order2 = OrderTestBuilder::new(OrderType::Limit)
309            .instrument_id(audusd_sim.id)
310            .side(OrderSide::Sell)
311            .price(Price::from("1.01000"))
312            .quantity(Quantity::from(50_000))
313            .build();
314        let order3 = OrderTestBuilder::new(OrderType::Limit)
315            .instrument_id(audusd_sim.id)
316            .side(OrderSide::Buy)
317            .price(Price::from("0.99000"))
318            .quantity(Quantity::from(75_000))
319            .build();
320
321        let orders = vec![order1, order2, order3];
322
323        let order_list = OrderList::new(
324            OrderListId::from("OL-010"),
325            audusd_sim.id,
326            StrategyId::test_default(),
327            orders,
328            UnixNanos::default(),
329        );
330
331        assert_eq!(order_list.len(), 3);
332        assert!(!order_list.is_empty());
333    }
334
335    #[rstest]
336    fn test_order_list_hash(audusd_sim: CurrencyPair) {
337        let order1 = OrderTestBuilder::new(OrderType::Limit)
338            .instrument_id(audusd_sim.id)
339            .side(OrderSide::Buy)
340            .price(Price::from("1.00000"))
341            .quantity(Quantity::from(100_000))
342            .build();
343
344        let orders = vec![order1];
345
346        let order_list1 = OrderList::new(
347            OrderListId::from("OL-011"),
348            audusd_sim.id,
349            StrategyId::test_default(),
350            orders.clone(),
351            UnixNanos::default(),
352        );
353
354        let order_list2 = OrderList::new(
355            OrderListId::from("OL-011"),
356            audusd_sim.id,
357            StrategyId::test_default(),
358            orders,
359            UnixNanos::default(),
360        );
361
362        let mut hasher1 = DefaultHasher::new();
363        let mut hasher2 = DefaultHasher::new();
364        order_list1.hash(&mut hasher1);
365        order_list2.hash(&mut hasher2);
366
367        assert_eq!(hasher1.finish(), hasher2.finish());
368    }
369}