Skip to main content

nautilus_model/events/order/spec/
initialized.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 indexmap::IndexMap;
17use nautilus_core::{UUID4, UnixNanos};
18use rust_decimal::Decimal;
19use ustr::Ustr;
20
21use crate::{
22    enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType},
23    events::OrderInitialized,
24    identifiers::{
25        ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, StrategyId, TraderId,
26    },
27    orders::OrderError,
28    stubs::{TestDefault, test_uuid},
29    types::{Price, Quantity},
30};
31
32/// Test-only fluent spec for [`OrderInitialized`].
33///
34/// All fields carry sensible defaults so callers only set what differs.
35/// `build()` constructs the event through [`OrderInitialized::new_checked`] so any future invariants
36/// added to the production constructor are exercised by tests built on this spec.
37#[derive(Debug, Clone, bon::Builder)]
38#[builder(finish_fn = into_spec)]
39#[expect(
40    clippy::struct_excessive_bools,
41    reason = "spec mirrors `OrderInitialized` field set; bool count is fixed by the event"
42)]
43pub struct OrderInitializedSpec {
44    #[builder(default = TraderId::test_default())]
45    pub trader_id: TraderId,
46    #[builder(default = StrategyId::test_default())]
47    pub strategy_id: StrategyId,
48    #[builder(default = InstrumentId::test_default())]
49    pub instrument_id: InstrumentId,
50    #[builder(default = ClientOrderId::test_default())]
51    pub client_order_id: ClientOrderId,
52    #[builder(default = OrderSide::Buy)]
53    pub order_side: OrderSide,
54    #[builder(default = OrderType::Market)]
55    pub order_type: OrderType,
56    #[builder(default = Quantity::new(100_000.0, 0))]
57    pub quantity: Quantity,
58    #[builder(default = TimeInForce::Day)]
59    pub time_in_force: TimeInForce,
60    #[builder(default = false)]
61    pub post_only: bool,
62    #[builder(default = false)]
63    pub reduce_only: bool,
64    #[builder(default = false)]
65    pub quote_quantity: bool,
66    #[builder(default = false)]
67    pub reconciliation: bool,
68    #[builder(default = test_uuid())]
69    pub event_id: UUID4,
70    #[builder(default = UnixNanos::default())]
71    pub ts_event: UnixNanos,
72    #[builder(default = UnixNanos::default())]
73    pub ts_init: UnixNanos,
74    pub price: Option<Price>,
75    pub activation_price: Option<Price>,
76    pub trigger_price: Option<Price>,
77    pub trigger_type: Option<TriggerType>,
78    pub limit_offset: Option<Decimal>,
79    pub trailing_offset: Option<Decimal>,
80    pub trailing_offset_type: Option<TrailingOffsetType>,
81    pub expire_time: Option<UnixNanos>,
82    pub display_qty: Option<Quantity>,
83    pub emulation_trigger: Option<TriggerType>,
84    pub trigger_instrument_id: Option<InstrumentId>,
85    pub contingency_type: Option<ContingencyType>,
86    pub order_list_id: Option<OrderListId>,
87    pub linked_order_ids: Option<Vec<ClientOrderId>>,
88    pub parent_order_id: Option<ClientOrderId>,
89    pub exec_algorithm_id: Option<ExecAlgorithmId>,
90    pub exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
91    pub exec_spawn_id: Option<ClientOrderId>,
92    pub tags: Option<Vec<Ustr>>,
93}
94
95impl<S: order_initialized_spec_builder::IsComplete> OrderInitializedSpecBuilder<S> {
96    /// Builds the spec and constructs an [`OrderInitialized`] through its production constructor.
97    ///
98    /// # Panics
99    ///
100    /// Panics if the order metadata violates an invariant.
101    #[must_use]
102    pub fn build(self) -> OrderInitialized {
103        self.build_checked()
104            .unwrap_or_else(|e| panic!("Failed to build OrderInitialized: {e}"))
105    }
106
107    /// Builds the spec and validates the resulting [`OrderInitialized`].
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if the order metadata violates an invariant.
112    pub fn build_checked(self) -> Result<OrderInitialized, OrderError> {
113        let spec = self.into_spec();
114        OrderInitialized::new_checked(
115            spec.trader_id,
116            spec.strategy_id,
117            spec.instrument_id,
118            spec.client_order_id,
119            spec.order_side,
120            spec.order_type,
121            spec.quantity,
122            spec.time_in_force,
123            spec.post_only,
124            spec.reduce_only,
125            spec.quote_quantity,
126            spec.reconciliation,
127            spec.event_id,
128            spec.ts_event,
129            spec.ts_init,
130            spec.price,
131            spec.activation_price,
132            spec.trigger_price,
133            spec.trigger_type,
134            spec.limit_offset,
135            spec.trailing_offset,
136            spec.trailing_offset_type,
137            spec.expire_time,
138            spec.display_qty,
139            spec.emulation_trigger,
140            spec.trigger_instrument_id,
141            spec.contingency_type,
142            spec.order_list_id,
143            spec.linked_order_ids,
144            spec.parent_order_id,
145            spec.exec_algorithm_id,
146            spec.exec_algorithm_params,
147            spec.exec_spawn_id,
148            spec.tags,
149        )
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use nautilus_core::correctness::CorrectnessError;
156    use rstest::rstest;
157
158    use super::*;
159    use crate::stubs::reset_test_uuid_rng;
160
161    #[rstest]
162    fn defaults_are_sensible() {
163        // Pin the spec's no-arg defaults so accidental drift in any individual default surfaces here,
164        // rather than as silent behavior change in downstream tests.
165        let order = OrderInitializedSpec::builder().build();
166        assert_eq!(order.trader_id, TraderId::test_default());
167        assert_eq!(order.strategy_id, StrategyId::test_default());
168        assert_eq!(order.instrument_id, InstrumentId::test_default());
169        assert_eq!(order.client_order_id, ClientOrderId::test_default());
170        assert_eq!(order.order_side, OrderSide::Buy);
171        assert_eq!(order.order_type, OrderType::Market);
172        assert_eq!(order.quantity, Quantity::new(100_000.0, 0));
173        assert_eq!(order.time_in_force, TimeInForce::Day);
174        assert!(!order.post_only);
175        assert!(!order.reduce_only);
176        assert!(!order.quote_quantity);
177        assert!(!order.reconciliation);
178        assert_eq!(order.ts_event, UnixNanos::default());
179        assert_eq!(order.ts_init, UnixNanos::default());
180        assert_eq!(order.price, None);
181        assert_eq!(order.trigger_price, None);
182        assert_eq!(order.trigger_type, None);
183        assert_eq!(order.limit_offset, None);
184        assert_eq!(order.trailing_offset, None);
185        assert_eq!(order.trailing_offset_type, None);
186        assert_eq!(order.expire_time, None);
187        assert_eq!(order.display_qty, None);
188        assert_eq!(order.emulation_trigger, None);
189        assert_eq!(order.trigger_instrument_id, None);
190        assert_eq!(order.contingency_type, None);
191        assert_eq!(order.order_list_id, None);
192        assert_eq!(order.linked_order_ids, None);
193        assert_eq!(order.parent_order_id, None);
194        assert_eq!(order.exec_algorithm_id, None);
195        assert_eq!(order.exec_algorithm_params, None);
196        assert_eq!(order.exec_spawn_id, None);
197        assert_eq!(order.tags, None);
198    }
199
200    #[rstest]
201    fn overrides_apply_through_constructor() {
202        let order = OrderInitializedSpec::builder()
203            .order_type(OrderType::Limit)
204            .order_side(OrderSide::Sell)
205            .quantity(Quantity::from("50"))
206            .price(Price::from("1.25000"))
207            .post_only(true)
208            .build();
209
210        assert_eq!(order.order_type, OrderType::Limit);
211        assert_eq!(order.order_side, OrderSide::Sell);
212        assert_eq!(order.quantity, Quantity::from("50"));
213        assert_eq!(order.price, Some(Price::from("1.25000")));
214        assert!(order.post_only);
215        assert_eq!(order.trader_id, TraderId::test_default());
216    }
217
218    #[rstest]
219    #[case(None)]
220    #[case(Some(Vec::new()))]
221    fn rejects_contingent_orders_without_linked_order_ids(
222        #[case] linked_order_ids: Option<Vec<ClientOrderId>>,
223    ) {
224        let result = OrderInitializedSpec::builder()
225            .contingency_type(ContingencyType::Oco)
226            .maybe_linked_order_ids(linked_order_ids)
227            .build_checked();
228
229        let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
230        else {
231            panic!("expected a predicate violation, was {result:?}");
232        };
233        assert_eq!(
234            message,
235            "`linked_order_ids` is required for contingent orders"
236        );
237    }
238
239    #[rstest]
240    fn rejects_exec_algorithm_without_exec_spawn_id() {
241        let result = OrderInitializedSpec::builder()
242            .exec_algorithm_id(ExecAlgorithmId::from("TWAP"))
243            .build_checked();
244
245        let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
246        else {
247            panic!("expected a predicate violation, was {result:?}");
248        };
249        assert_eq!(
250            message,
251            "`exec_spawn_id` is required when `exec_algorithm_id` is set"
252        );
253    }
254
255    #[rstest]
256    fn event_ids_are_unique_within_a_run() {
257        reset_test_uuid_rng();
258        let a = OrderInitializedSpec::builder().build();
259        let b = OrderInitializedSpec::builder().build();
260        let c = OrderInitializedSpec::builder().build();
261        assert_ne!(a.event_id, b.event_id);
262        assert_ne!(b.event_id, c.event_id);
263        assert_ne!(a.event_id, c.event_id);
264    }
265
266    #[rstest]
267    fn event_id_sequence_is_reproducible() {
268        // Reset before each draw so the comparison is run-order independent.
269        reset_test_uuid_rng();
270        let first_run: Vec<_> = (0..3)
271            .map(|_| OrderInitializedSpec::builder().build().event_id)
272            .collect();
273
274        reset_test_uuid_rng();
275        let second_run: Vec<_> = (0..3)
276            .map(|_| OrderInitializedSpec::builder().build().event_id)
277            .collect();
278
279        assert_eq!(first_run, second_run);
280    }
281}