nautilus_model/events/order/spec/
initialized.rs1use 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#[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 #[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 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 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_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}