nautilus_model/orders/
list.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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::fmt::Display;
17
18use nautilus_core::{UnixNanos, correctness::check_slice_not_empty};
19use serde::{Deserialize, Serialize};
20
21use super::{Order, OrderAny};
22use crate::identifiers::{InstrumentId, OrderListId, StrategyId};
23
24#[derive(Clone, Eq, Debug, Serialize, Deserialize)]
25#[cfg_attr(
26    feature = "python",
27    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
28)]
29pub struct OrderList {
30    pub id: OrderListId,
31    pub instrument_id: InstrumentId,
32    pub strategy_id: StrategyId,
33    pub orders: Vec<OrderAny>,
34    pub ts_init: UnixNanos,
35}
36
37impl OrderList {
38    /// Creates a new [`OrderList`] instance.
39    ///
40    /// # Panics
41    ///
42    /// Panics if `orders` is empty or if any order's instrument or strategy ID does not match.
43    pub fn new(
44        order_list_id: OrderListId,
45        instrument_id: InstrumentId,
46        strategy_id: StrategyId,
47        orders: Vec<OrderAny>,
48        ts_init: UnixNanos,
49    ) -> Self {
50        check_slice_not_empty(orders.as_slice(), stringify!(orders)).unwrap();
51        for order in &orders {
52            assert_eq!(instrument_id, order.instrument_id());
53            assert_eq!(strategy_id, order.strategy_id());
54        }
55        Self {
56            id: order_list_id,
57            instrument_id,
58            strategy_id,
59            orders,
60            ts_init,
61        }
62    }
63}
64
65impl PartialEq for OrderList {
66    fn eq(&self, other: &Self) -> bool {
67        self.id == other.id
68    }
69}
70
71impl Display for OrderList {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        write!(
74            f,
75            "OrderList(\
76            id={}, \
77            instrument_id={}, \
78            strategy_id={}, \
79            orders={:?}, \
80            ts_init={}\
81            )",
82            self.id, self.instrument_id, self.strategy_id, self.orders, self.ts_init,
83        )
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use rstest::rstest;
90
91    use super::*;
92    use crate::{
93        enums::{OrderSide, OrderType},
94        identifiers::{OrderListId, StrategyId},
95        instruments::{CurrencyPair, stubs::*},
96        orders::OrderTestBuilder,
97        types::{Price, Quantity},
98    };
99
100    #[rstest]
101    fn test_new_and_display(audusd_sim: CurrencyPair) {
102        let order1 = OrderTestBuilder::new(OrderType::Limit)
103            .instrument_id(audusd_sim.id)
104            .side(OrderSide::Buy)
105            .price(Price::from("1.00000"))
106            .quantity(Quantity::from(100_000))
107            .build();
108        let order2 = OrderTestBuilder::new(OrderType::Limit)
109            .instrument_id(audusd_sim.id)
110            .side(OrderSide::Buy)
111            .price(Price::from("1.00000"))
112            .quantity(Quantity::from(100_000))
113            .build();
114        let order3 = OrderTestBuilder::new(OrderType::Limit)
115            .instrument_id(audusd_sim.id)
116            .side(OrderSide::Buy)
117            .price(Price::from("1.00000"))
118            .quantity(Quantity::from(100_000))
119            .build();
120
121        let orders = vec![order1, order2, order3];
122
123        let order_list = OrderList::new(
124            OrderListId::from("OL-001"),
125            audusd_sim.id,
126            StrategyId::default(),
127            orders,
128            UnixNanos::default(),
129        );
130
131        assert!(order_list.to_string().starts_with(
132            "OrderList(id=OL-001, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders="
133        ));
134    }
135
136    #[rstest]
137    #[should_panic(expected = "assertion `left == right` failed")]
138    fn test_order_list_creation_with_mismatched_instrument_id(audusd_sim: CurrencyPair) {
139        let order1 = OrderTestBuilder::new(OrderType::Limit)
140            .instrument_id(audusd_sim.id)
141            .side(OrderSide::Buy)
142            .price(Price::from("1.00000"))
143            .quantity(Quantity::from(100_000))
144            .build();
145        let order2 = OrderTestBuilder::new(OrderType::Limit)
146            .instrument_id(InstrumentId::from("EUR/USD.SIM"))
147            .side(OrderSide::Sell)
148            .price(Price::from("1.01000"))
149            .quantity(Quantity::from(50_000))
150            .build();
151
152        let orders = vec![order1, order2];
153
154        // This should panic because the instrument IDs do not match
155        OrderList::new(
156            OrderListId::from("OL-003"),
157            audusd_sim.id,
158            StrategyId::default(),
159            orders,
160            UnixNanos::default(),
161        );
162    }
163
164    #[rstest]
165    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: the 'orders' slice")]
166    fn test_order_list_creation_with_empty_orders(audusd_sim: CurrencyPair) {
167        let orders: Vec<OrderAny> = vec![];
168
169        // This should panic because the orders list is empty
170        OrderList::new(
171            OrderListId::from("OL-004"),
172            audusd_sim.id,
173            StrategyId::default(),
174            orders,
175            UnixNanos::default(),
176        );
177    }
178
179    #[rstest]
180    fn test_order_list_equality(audusd_sim: CurrencyPair) {
181        let order1 = OrderTestBuilder::new(OrderType::Limit)
182            .instrument_id(audusd_sim.id)
183            .side(OrderSide::Buy)
184            .price(Price::from("1.00000"))
185            .quantity(Quantity::from(100_000))
186            .build();
187
188        let orders = vec![order1];
189
190        let order_list1 = OrderList::new(
191            OrderListId::from("OL-006"),
192            audusd_sim.id,
193            StrategyId::default(),
194            orders.clone(),
195            UnixNanos::default(),
196        );
197
198        let order_list2 = OrderList::new(
199            OrderListId::from("OL-006"),
200            audusd_sim.id,
201            StrategyId::default(),
202            orders,
203            UnixNanos::default(),
204        );
205
206        assert_eq!(order_list1, order_list2);
207    }
208
209    #[rstest]
210    fn test_order_list_inequality(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-007"),
222            audusd_sim.id,
223            StrategyId::default(),
224            orders.clone(),
225            UnixNanos::default(),
226        );
227
228        let order_list2 = OrderList::new(
229            OrderListId::from("OL-008"),
230            audusd_sim.id,
231            StrategyId::default(),
232            orders,
233            UnixNanos::default(),
234        );
235
236        assert_ne!(order_list1, order_list2);
237    }
238}