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    stubs::{TestDefault, test_uuid},
28    types::{Price, Quantity},
29};
30
31/// Test-only fluent spec for [`OrderInitialized`].
32///
33/// All fields carry sensible defaults so callers only set what differs.
34/// `build()` constructs the event through [`OrderInitialized::new`] so any future invariants
35/// added to the production constructor are exercised by tests built on this spec.
36#[derive(Debug, Clone, bon::Builder)]
37#[builder(finish_fn = into_spec)]
38#[expect(
39    clippy::struct_excessive_bools,
40    reason = "spec mirrors `OrderInitialized` field set; bool count is fixed by the event"
41)]
42pub struct OrderInitializedSpec {
43    #[builder(default = TraderId::test_default())]
44    pub trader_id: TraderId,
45    #[builder(default = StrategyId::test_default())]
46    pub strategy_id: StrategyId,
47    #[builder(default = InstrumentId::test_default())]
48    pub instrument_id: InstrumentId,
49    #[builder(default = ClientOrderId::test_default())]
50    pub client_order_id: ClientOrderId,
51    #[builder(default = OrderSide::Buy)]
52    pub order_side: OrderSide,
53    #[builder(default = OrderType::Market)]
54    pub order_type: OrderType,
55    #[builder(default = Quantity::new(100_000.0, 0))]
56    pub quantity: Quantity,
57    #[builder(default = TimeInForce::Day)]
58    pub time_in_force: TimeInForce,
59    #[builder(default = false)]
60    pub post_only: bool,
61    #[builder(default = false)]
62    pub reduce_only: bool,
63    #[builder(default = false)]
64    pub quote_quantity: bool,
65    #[builder(default = false)]
66    pub reconciliation: bool,
67    #[builder(default = test_uuid())]
68    pub event_id: UUID4,
69    #[builder(default = UnixNanos::default())]
70    pub ts_event: UnixNanos,
71    #[builder(default = UnixNanos::default())]
72    pub ts_init: UnixNanos,
73    pub price: Option<Price>,
74    pub trigger_price: Option<Price>,
75    pub trigger_type: Option<TriggerType>,
76    pub limit_offset: Option<Decimal>,
77    pub trailing_offset: Option<Decimal>,
78    pub trailing_offset_type: Option<TrailingOffsetType>,
79    pub expire_time: Option<UnixNanos>,
80    pub display_qty: Option<Quantity>,
81    pub emulation_trigger: Option<TriggerType>,
82    pub trigger_instrument_id: Option<InstrumentId>,
83    pub contingency_type: Option<ContingencyType>,
84    pub order_list_id: Option<OrderListId>,
85    pub linked_order_ids: Option<Vec<ClientOrderId>>,
86    pub parent_order_id: Option<ClientOrderId>,
87    pub exec_algorithm_id: Option<ExecAlgorithmId>,
88    pub exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
89    pub exec_spawn_id: Option<ClientOrderId>,
90    pub tags: Option<Vec<Ustr>>,
91}
92
93impl<S: order_initialized_spec_builder::IsComplete> OrderInitializedSpecBuilder<S> {
94    /// Builds the spec and constructs an [`OrderInitialized`] through its production constructor.
95    #[must_use]
96    pub fn build(self) -> OrderInitialized {
97        let spec = self.into_spec();
98        OrderInitialized::new(
99            spec.trader_id,
100            spec.strategy_id,
101            spec.instrument_id,
102            spec.client_order_id,
103            spec.order_side,
104            spec.order_type,
105            spec.quantity,
106            spec.time_in_force,
107            spec.post_only,
108            spec.reduce_only,
109            spec.quote_quantity,
110            spec.reconciliation,
111            spec.event_id,
112            spec.ts_event,
113            spec.ts_init,
114            spec.price,
115            spec.trigger_price,
116            spec.trigger_type,
117            spec.limit_offset,
118            spec.trailing_offset,
119            spec.trailing_offset_type,
120            spec.expire_time,
121            spec.display_qty,
122            spec.emulation_trigger,
123            spec.trigger_instrument_id,
124            spec.contingency_type,
125            spec.order_list_id,
126            spec.linked_order_ids,
127            spec.parent_order_id,
128            spec.exec_algorithm_id,
129            spec.exec_algorithm_params,
130            spec.exec_spawn_id,
131            spec.tags,
132        )
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use rstest::rstest;
139
140    use super::*;
141    use crate::stubs::reset_test_uuid_rng;
142
143    #[rstest]
144    fn defaults_are_sensible() {
145        // Pin the spec's no-arg defaults so accidental drift in any individual default surfaces here,
146        // rather than as silent behavior change in downstream tests.
147        let order = OrderInitializedSpec::builder().build();
148        assert_eq!(order.trader_id, TraderId::test_default());
149        assert_eq!(order.strategy_id, StrategyId::test_default());
150        assert_eq!(order.instrument_id, InstrumentId::test_default());
151        assert_eq!(order.client_order_id, ClientOrderId::test_default());
152        assert_eq!(order.order_side, OrderSide::Buy);
153        assert_eq!(order.order_type, OrderType::Market);
154        assert_eq!(order.quantity, Quantity::new(100_000.0, 0));
155        assert_eq!(order.time_in_force, TimeInForce::Day);
156        assert!(!order.post_only);
157        assert!(!order.reduce_only);
158        assert!(!order.quote_quantity);
159        assert!(!order.reconciliation);
160        assert_eq!(order.ts_event, UnixNanos::default());
161        assert_eq!(order.ts_init, UnixNanos::default());
162        assert_eq!(order.price, None);
163        assert_eq!(order.trigger_price, None);
164        assert_eq!(order.trigger_type, None);
165        assert_eq!(order.limit_offset, None);
166        assert_eq!(order.trailing_offset, None);
167        assert_eq!(order.trailing_offset_type, None);
168        assert_eq!(order.expire_time, None);
169        assert_eq!(order.display_qty, None);
170        assert_eq!(order.emulation_trigger, None);
171        assert_eq!(order.trigger_instrument_id, None);
172        assert_eq!(order.contingency_type, None);
173        assert_eq!(order.order_list_id, None);
174        assert_eq!(order.linked_order_ids, None);
175        assert_eq!(order.parent_order_id, None);
176        assert_eq!(order.exec_algorithm_id, None);
177        assert_eq!(order.exec_algorithm_params, None);
178        assert_eq!(order.exec_spawn_id, None);
179        assert_eq!(order.tags, None);
180    }
181
182    #[rstest]
183    fn overrides_apply_through_constructor() {
184        let order = OrderInitializedSpec::builder()
185            .order_type(OrderType::Limit)
186            .order_side(OrderSide::Sell)
187            .quantity(Quantity::from("50"))
188            .price(Price::from("1.25000"))
189            .post_only(true)
190            .build();
191
192        assert_eq!(order.order_type, OrderType::Limit);
193        assert_eq!(order.order_side, OrderSide::Sell);
194        assert_eq!(order.quantity, Quantity::from("50"));
195        assert_eq!(order.price, Some(Price::from("1.25000")));
196        assert!(order.post_only);
197        assert_eq!(order.trader_id, TraderId::test_default());
198    }
199
200    #[rstest]
201    fn event_ids_are_unique_within_a_run() {
202        reset_test_uuid_rng();
203        let a = OrderInitializedSpec::builder().build();
204        let b = OrderInitializedSpec::builder().build();
205        let c = OrderInitializedSpec::builder().build();
206        assert_ne!(a.event_id, b.event_id);
207        assert_ne!(b.event_id, c.event_id);
208        assert_ne!(a.event_id, c.event_id);
209    }
210
211    #[rstest]
212    fn event_id_sequence_is_reproducible() {
213        // Reset before each draw so the comparison is run-order independent.
214        reset_test_uuid_rng();
215        let first_run: Vec<_> = (0..3)
216            .map(|_| OrderInitializedSpec::builder().build().event_id)
217            .collect();
218
219        reset_test_uuid_rng();
220        let second_run: Vec<_> = (0..3)
221            .map(|_| OrderInitializedSpec::builder().build().event_id)
222            .collect();
223
224        assert_eq!(first_run, second_run);
225    }
226}