nautilus_model/orders/
list.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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 = "posei_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////////////////////////////////////////////////////////////////////////////////
88// Tests
89////////////////////////////////////////////////////////////////////////////////
90#[cfg(test)]
91mod tests {
92    use rstest::rstest;
93
94    use super::*;
95    use crate::{
96        enums::{OrderSide, OrderType},
97        identifiers::{OrderListId, StrategyId},
98        instruments::{CurrencyPair, stubs::*},
99        orders::OrderTestBuilder,
100        types::{Price, Quantity},
101    };
102
103    #[rstest]
104    fn test_new_and_display(audusd_sim: CurrencyPair) {
105        let order1 = OrderTestBuilder::new(OrderType::Limit)
106            .instrument_id(audusd_sim.id)
107            .side(OrderSide::Buy)
108            .price(Price::from("1.00000"))
109            .quantity(Quantity::from(100_000))
110            .build();
111        let order2 = OrderTestBuilder::new(OrderType::Limit)
112            .instrument_id(audusd_sim.id)
113            .side(OrderSide::Buy)
114            .price(Price::from("1.00000"))
115            .quantity(Quantity::from(100_000))
116            .build();
117        let order3 = OrderTestBuilder::new(OrderType::Limit)
118            .instrument_id(audusd_sim.id)
119            .side(OrderSide::Buy)
120            .price(Price::from("1.00000"))
121            .quantity(Quantity::from(100_000))
122            .build();
123
124        let orders = vec![order1, order2, order3];
125
126        let order_list = OrderList::new(
127            OrderListId::from("OL-001"),
128            audusd_sim.id,
129            StrategyId::default(),
130            orders,
131            UnixNanos::default(),
132        );
133
134        assert!(order_list.to_string().starts_with(
135            "OrderList(id=OL-001, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders="
136        ));
137    }
138
139    #[rstest]
140    #[should_panic(expected = "assertion `left == right` failed")]
141    fn test_order_list_creation_with_mismatched_instrument_id(audusd_sim: CurrencyPair) {
142        let order1 = OrderTestBuilder::new(OrderType::Limit)
143            .instrument_id(audusd_sim.id)
144            .side(OrderSide::Buy)
145            .price(Price::from("1.00000"))
146            .quantity(Quantity::from(100_000))
147            .build();
148        let order2 = OrderTestBuilder::new(OrderType::Limit)
149            .instrument_id(InstrumentId::from("EUR/USD.SIM"))
150            .side(OrderSide::Sell)
151            .price(Price::from("1.01000"))
152            .quantity(Quantity::from(50_000))
153            .build();
154
155        let orders = vec![order1, order2];
156
157        // This should panic because the instrument IDs do not match
158        OrderList::new(
159            OrderListId::from("OL-003"),
160            audusd_sim.id,
161            StrategyId::default(),
162            orders,
163            UnixNanos::default(),
164        );
165    }
166
167    #[rstest]
168    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: the 'orders' slice")]
169    fn test_order_list_creation_with_empty_orders(audusd_sim: CurrencyPair) {
170        let orders: Vec<OrderAny> = vec![];
171
172        // This should panic because the orders list is empty
173        OrderList::new(
174            OrderListId::from("OL-004"),
175            audusd_sim.id,
176            StrategyId::default(),
177            orders,
178            UnixNanos::default(),
179        );
180    }
181
182    #[rstest]
183    fn test_order_list_equality(audusd_sim: CurrencyPair) {
184        let order1 = OrderTestBuilder::new(OrderType::Limit)
185            .instrument_id(audusd_sim.id)
186            .side(OrderSide::Buy)
187            .price(Price::from("1.00000"))
188            .quantity(Quantity::from(100_000))
189            .build();
190
191        let orders = vec![order1];
192
193        let order_list1 = OrderList::new(
194            OrderListId::from("OL-006"),
195            audusd_sim.id,
196            StrategyId::default(),
197            orders.clone(),
198            UnixNanos::default(),
199        );
200
201        let order_list2 = OrderList::new(
202            OrderListId::from("OL-006"),
203            audusd_sim.id,
204            StrategyId::default(),
205            orders,
206            UnixNanos::default(),
207        );
208
209        assert_eq!(order_list1, order_list2);
210    }
211
212    #[rstest]
213    fn test_order_list_inequality(audusd_sim: CurrencyPair) {
214        let order1 = OrderTestBuilder::new(OrderType::Limit)
215            .instrument_id(audusd_sim.id)
216            .side(OrderSide::Buy)
217            .price(Price::from("1.00000"))
218            .quantity(Quantity::from(100_000))
219            .build();
220
221        let orders = vec![order1];
222
223        let order_list1 = OrderList::new(
224            OrderListId::from("OL-007"),
225            audusd_sim.id,
226            StrategyId::default(),
227            orders.clone(),
228            UnixNanos::default(),
229        );
230
231        let order_list2 = OrderList::new(
232            OrderListId::from("OL-008"),
233            audusd_sim.id,
234            StrategyId::default(),
235            orders,
236            UnixNanos::default(),
237        );
238
239        assert_ne!(order_list1, order_list2);
240    }
241}