1use std::fmt::{Debug, Display};
17
18use indexmap::IndexMap;
19use nautilus_core::{
20 UUID4, UnixNanos,
21 correctness::{FAILED, check_predicate_false},
22};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use crate::{
28 enums::{
29 ContingencyType, LiquiditySide, OrderSide, OrderType, TimeInForce, TrailingOffsetType,
30 TriggerType,
31 },
32 events::OrderEvent,
33 identifiers::{
34 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
35 StrategyId, TradeId, TraderId, VenueOrderId,
36 },
37 orders::{OrderAny, OrderError},
38 types::{Currency, Money, Price, Quantity},
39};
40
41#[repr(C)]
48#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(tag = "type")]
50#[cfg_attr(
51 feature = "python",
52 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
53)]
54#[cfg_attr(
55 feature = "python",
56 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
57)]
58pub struct OrderInitialized {
59 pub trader_id: TraderId,
61 pub strategy_id: StrategyId,
63 pub instrument_id: InstrumentId,
65 pub client_order_id: ClientOrderId,
67 pub order_side: OrderSide,
69 pub order_type: OrderType,
71 pub quantity: Quantity,
73 pub time_in_force: TimeInForce,
75 pub post_only: bool,
77 pub reduce_only: bool,
79 pub quote_quantity: bool,
81 pub reconciliation: bool,
83 pub event_id: UUID4,
85 pub ts_event: UnixNanos,
87 pub ts_init: UnixNanos,
89 pub price: Option<Price>,
91 pub activation_price: Option<Price>,
93 pub trigger_price: Option<Price>,
95 #[serde(default, with = "crate::enums::serde_option_trigger_type")]
97 pub trigger_type: Option<TriggerType>,
98 pub limit_offset: Option<Decimal>,
100 pub trailing_offset: Option<Decimal>,
102 #[serde(default, with = "crate::enums::serde_option_trailing_offset_type")]
104 pub trailing_offset_type: Option<TrailingOffsetType>,
105 pub expire_time: Option<UnixNanos>,
107 pub display_qty: Option<Quantity>,
109 #[serde(default, with = "crate::enums::serde_option_trigger_type")]
111 pub emulation_trigger: Option<TriggerType>,
112 pub trigger_instrument_id: Option<InstrumentId>,
114 #[serde(default, with = "crate::enums::serde_option_contingency_type")]
116 pub contingency_type: Option<ContingencyType>,
117 pub order_list_id: Option<OrderListId>,
119 pub linked_order_ids: Option<Vec<ClientOrderId>>,
121 pub parent_order_id: Option<ClientOrderId>,
123 pub exec_algorithm_id: Option<ExecAlgorithmId>,
125 pub exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
127 pub exec_spawn_id: Option<ClientOrderId>,
129 pub tags: Option<Vec<Ustr>>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub causation_id: Option<UUID4>,
134}
135
136impl OrderInitialized {
137 #[expect(clippy::too_many_arguments)]
145 #[expect(
146 clippy::fn_params_excessive_bools,
147 reason = "domain event constructor requires multiple boolean flags"
148 )]
149 pub fn new_checked(
150 trader_id: TraderId,
151 strategy_id: StrategyId,
152 instrument_id: InstrumentId,
153 client_order_id: ClientOrderId,
154 order_side: OrderSide,
155 order_type: OrderType,
156 quantity: Quantity,
157 time_in_force: TimeInForce,
158 post_only: bool,
159 reduce_only: bool,
160 quote_quantity: bool,
161 reconciliation: bool,
162 event_id: UUID4,
163 ts_event: UnixNanos,
164 ts_init: UnixNanos,
165 price: Option<Price>,
166 activation_price: Option<Price>,
167 trigger_price: Option<Price>,
168 trigger_type: Option<TriggerType>,
169 limit_offset: Option<Decimal>,
170 trailing_offset: Option<Decimal>,
171 trailing_offset_type: Option<TrailingOffsetType>,
172 expire_time: Option<UnixNanos>,
173 display_qty: Option<Quantity>,
174 emulation_trigger: Option<TriggerType>,
175 trigger_instrument_id: Option<InstrumentId>,
176 contingency_type: Option<ContingencyType>,
177 order_list_id: Option<OrderListId>,
178 linked_order_ids: Option<Vec<ClientOrderId>>,
179 parent_order_id: Option<ClientOrderId>,
180 exec_algorithm_id: Option<ExecAlgorithmId>,
181 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
182 exec_spawn_id: Option<ClientOrderId>,
183 tags: Option<Vec<Ustr>>,
184 ) -> Result<Self, OrderError> {
185 check_predicate_false(
186 contingency_type.is_some() && linked_order_ids.as_ref().is_none_or(Vec::is_empty),
187 "`linked_order_ids` is required for contingent orders",
188 )?;
189 check_predicate_false(
190 exec_algorithm_id.is_some() && exec_spawn_id.is_none(),
191 "`exec_spawn_id` is required when `exec_algorithm_id` is set",
192 )?;
193
194 Ok(Self {
195 trader_id,
196 strategy_id,
197 instrument_id,
198 client_order_id,
199 order_side,
200 order_type,
201 quantity,
202 time_in_force,
203 post_only,
204 reduce_only,
205 quote_quantity,
206 reconciliation,
207 event_id,
208 ts_event,
209 ts_init,
210 price,
211 activation_price,
212 trigger_price,
213 trigger_type,
214 limit_offset,
215 trailing_offset,
216 trailing_offset_type,
217 expire_time,
218 display_qty,
219 emulation_trigger,
220 trigger_instrument_id,
221 contingency_type,
222 order_list_id,
223 linked_order_ids,
224 parent_order_id,
225 exec_algorithm_id,
226 exec_algorithm_params,
227 exec_spawn_id,
228 tags,
229 causation_id: None,
230 })
231 }
232
233 #[expect(clippy::too_many_arguments)]
240 #[expect(
241 clippy::fn_params_excessive_bools,
242 reason = "domain event constructor requires multiple boolean flags"
243 )]
244 #[must_use]
245 pub fn new(
246 trader_id: TraderId,
247 strategy_id: StrategyId,
248 instrument_id: InstrumentId,
249 client_order_id: ClientOrderId,
250 order_side: OrderSide,
251 order_type: OrderType,
252 quantity: Quantity,
253 time_in_force: TimeInForce,
254 post_only: bool,
255 reduce_only: bool,
256 quote_quantity: bool,
257 reconciliation: bool,
258 event_id: UUID4,
259 ts_event: UnixNanos,
260 ts_init: UnixNanos,
261 price: Option<Price>,
262 activation_price: Option<Price>,
263 trigger_price: Option<Price>,
264 trigger_type: Option<TriggerType>,
265 limit_offset: Option<Decimal>,
266 trailing_offset: Option<Decimal>,
267 trailing_offset_type: Option<TrailingOffsetType>,
268 expire_time: Option<UnixNanos>,
269 display_qty: Option<Quantity>,
270 emulation_trigger: Option<TriggerType>,
271 trigger_instrument_id: Option<InstrumentId>,
272 contingency_type: Option<ContingencyType>,
273 order_list_id: Option<OrderListId>,
274 linked_order_ids: Option<Vec<ClientOrderId>>,
275 parent_order_id: Option<ClientOrderId>,
276 exec_algorithm_id: Option<ExecAlgorithmId>,
277 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
278 exec_spawn_id: Option<ClientOrderId>,
279 tags: Option<Vec<Ustr>>,
280 ) -> Self {
281 Self::new_checked(
282 trader_id,
283 strategy_id,
284 instrument_id,
285 client_order_id,
286 order_side,
287 order_type,
288 quantity,
289 time_in_force,
290 post_only,
291 reduce_only,
292 quote_quantity,
293 reconciliation,
294 event_id,
295 ts_event,
296 ts_init,
297 price,
298 activation_price,
299 trigger_price,
300 trigger_type,
301 limit_offset,
302 trailing_offset,
303 trailing_offset_type,
304 expire_time,
305 display_qty,
306 emulation_trigger,
307 trigger_instrument_id,
308 contingency_type,
309 order_list_id,
310 linked_order_ids,
311 parent_order_id,
312 exec_algorithm_id,
313 exec_algorithm_params,
314 exec_spawn_id,
315 tags,
316 )
317 .unwrap_or_else(|e| panic!("{FAILED}: {e}"))
318 }
319}
320
321impl Debug for OrderInitialized {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 write!(
324 f,
325 "{}(\
326 trader_id={}, \
327 strategy_id={}, \
328 instrument_id={}, \
329 client_order_id={}, \
330 side={}, \
331 type={}, \
332 quantity={}, \
333 time_in_force={}, \
334 post_only={}, \
335 reduce_only={}, \
336 quote_quantity={}, \
337 price={}, \
338 emulation_trigger={}, \
339 trigger_instrument_id={}, \
340 contingency_type={}, \
341 order_list_id={}, \
342 linked_order_ids=[{}], \
343 parent_order_id={}, \
344 exec_algorithm_id={}, \
345 exec_algorithm_params={}, \
346 exec_spawn_id={}, \
347 tags={}, \
348 event_id={}, \
349 ts_init={})",
350 stringify!(OrderInitialized),
351 self.trader_id,
352 self.strategy_id,
353 self.instrument_id,
354 self.client_order_id,
355 self.order_side,
356 self.order_type,
357 self.quantity,
358 self.time_in_force,
359 self.post_only,
360 self.reduce_only,
361 self.quote_quantity,
362 self.price
363 .map_or("None".to_string(), |price| format!("{price}")),
364 self.emulation_trigger
365 .map_or("None".to_string(), |trigger| format!("{trigger}")),
366 self.trigger_instrument_id
367 .map_or("None".to_string(), |instrument_id| format!(
368 "{instrument_id}"
369 )),
370 self.contingency_type
371 .map_or("None".to_string(), |contingency_type| format!(
372 "{contingency_type}"
373 )),
374 self.order_list_id
375 .map_or("None".to_string(), |order_list_id| format!(
376 "{order_list_id}"
377 )),
378 self.linked_order_ids
379 .as_ref()
380 .map_or("None".to_string(), |linked_order_ids| linked_order_ids
381 .iter()
382 .map(ToString::to_string)
383 .collect::<Vec<_>>()
384 .join(", ")),
385 self.parent_order_id
386 .map_or("None".to_string(), |parent_order_id| format!(
387 "{parent_order_id}"
388 )),
389 self.exec_algorithm_id
390 .map_or("None".to_string(), |exec_algorithm_id| format!(
391 "{exec_algorithm_id}"
392 )),
393 self.exec_algorithm_params
394 .as_ref()
395 .map_or("None".to_string(), |exec_algorithm_params| format!(
396 "{exec_algorithm_params:?}"
397 )),
398 self.exec_spawn_id
399 .map_or("None".to_string(), |exec_spawn_id| format!(
400 "{exec_spawn_id}"
401 )),
402 self.tags.as_ref().map_or("None".to_string(), |tags| tags
403 .iter()
404 .map(ToString::to_string)
405 .collect::<Vec<String>>()
406 .join(", ")),
407 self.event_id,
408 self.ts_init
409 )
410 }
411}
412
413impl Display for OrderInitialized {
414 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
415 write!(
416 f,
417 "{}(\
418 instrument_id={}, \
419 client_order_id={}, \
420 side={}, \
421 type={}, \
422 quantity={}, \
423 time_in_force={}, \
424 post_only={}, \
425 reduce_only={}, \
426 quote_quantity={}, \
427 price={}, \
428 emulation_trigger={}, \
429 trigger_instrument_id={}, \
430 contingency_type={}, \
431 order_list_id={}, \
432 linked_order_ids=[{}], \
433 parent_order_id={}, \
434 exec_algorithm_id={}, \
435 exec_algorithm_params={}, \
436 exec_spawn_id={}, \
437 tags={})",
438 stringify!(OrderInitialized),
439 self.instrument_id,
440 self.client_order_id,
441 self.order_side,
442 self.order_type,
443 self.quantity,
444 self.time_in_force,
445 self.post_only,
446 self.reduce_only,
447 self.quote_quantity,
448 self.price
449 .map_or("None".to_string(), |price| format!("{price}")),
450 self.emulation_trigger
451 .map_or("None".to_string(), |trigger| format!("{trigger}")),
452 self.trigger_instrument_id
453 .map_or("None".to_string(), |instrument_id| format!(
454 "{instrument_id}"
455 )),
456 self.contingency_type
457 .map_or("None".to_string(), |contingency_type| format!(
458 "{contingency_type}"
459 )),
460 self.order_list_id
461 .map_or("None".to_string(), |order_list_id| format!(
462 "{order_list_id}"
463 )),
464 self.linked_order_ids
465 .as_ref()
466 .map_or("None".to_string(), |linked_order_ids| linked_order_ids
467 .iter()
468 .map(ToString::to_string)
469 .collect::<Vec<_>>()
470 .join(", ")),
471 self.parent_order_id
472 .map_or("None".to_string(), |parent_order_id| format!(
473 "{parent_order_id}"
474 )),
475 self.exec_algorithm_id
476 .map_or("None".to_string(), |exec_algorithm_id| format!(
477 "{exec_algorithm_id}"
478 )),
479 self.exec_algorithm_params
480 .as_ref()
481 .map_or("None".to_string(), |exec_algorithm_params| format!(
482 "{exec_algorithm_params:?}"
483 )),
484 self.exec_spawn_id
485 .map_or("None".to_string(), |exec_spawn_id| format!(
486 "{exec_spawn_id}"
487 )),
488 self.tags.as_ref().map_or("None".to_string(), |tags| tags
489 .iter()
490 .map(ToString::to_string)
491 .collect::<Vec<String>>()
492 .join(", ")),
493 )
494 }
495}
496
497impl OrderEvent for OrderInitialized {
498 fn id(&self) -> UUID4 {
499 self.event_id
500 }
501
502 fn type_name(&self) -> &'static str {
503 stringify!(OrderInitialized)
504 }
505
506 fn order_type(&self) -> Option<OrderType> {
507 Some(self.order_type)
508 }
509
510 fn order_side(&self) -> Option<OrderSide> {
511 Some(self.order_side)
512 }
513
514 fn trader_id(&self) -> TraderId {
515 self.trader_id
516 }
517
518 fn strategy_id(&self) -> StrategyId {
519 self.strategy_id
520 }
521
522 fn instrument_id(&self) -> InstrumentId {
523 self.instrument_id
524 }
525
526 fn trade_id(&self) -> Option<TradeId> {
527 None
528 }
529
530 fn currency(&self) -> Option<Currency> {
531 None
532 }
533
534 fn client_order_id(&self) -> ClientOrderId {
535 self.client_order_id
536 }
537
538 fn reason(&self) -> Option<Ustr> {
539 None
540 }
541
542 fn quantity(&self) -> Option<Quantity> {
543 Some(self.quantity)
544 }
545
546 fn time_in_force(&self) -> Option<TimeInForce> {
547 Some(self.time_in_force)
548 }
549
550 fn liquidity_side(&self) -> Option<LiquiditySide> {
551 None
552 }
553
554 fn post_only(&self) -> Option<bool> {
555 Some(self.post_only)
556 }
557
558 fn reduce_only(&self) -> Option<bool> {
559 Some(self.reduce_only)
560 }
561
562 fn quote_quantity(&self) -> Option<bool> {
563 Some(self.quote_quantity)
564 }
565
566 fn reconciliation(&self) -> bool {
567 false
568 }
569
570 fn price(&self) -> Option<Price> {
571 self.price
572 }
573
574 fn last_px(&self) -> Option<Price> {
575 None
576 }
577
578 fn last_qty(&self) -> Option<Quantity> {
579 None
580 }
581
582 fn activation_price(&self) -> Option<Price> {
583 self.activation_price
584 }
585
586 fn trigger_price(&self) -> Option<Price> {
587 self.trigger_price
588 }
589
590 fn trigger_type(&self) -> Option<TriggerType> {
591 self.trigger_type
592 }
593
594 fn limit_offset(&self) -> Option<Decimal> {
595 self.limit_offset
596 }
597
598 fn trailing_offset(&self) -> Option<Decimal> {
599 self.trailing_offset
600 }
601
602 fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
603 self.trailing_offset_type
604 }
605
606 fn expire_time(&self) -> Option<UnixNanos> {
607 self.expire_time
608 }
609
610 fn display_qty(&self) -> Option<Quantity> {
611 self.display_qty
612 }
613
614 fn emulation_trigger(&self) -> Option<TriggerType> {
615 self.emulation_trigger
616 }
617
618 fn trigger_instrument_id(&self) -> Option<InstrumentId> {
619 self.trigger_instrument_id
620 }
621
622 fn contingency_type(&self) -> Option<ContingencyType> {
623 self.contingency_type
624 }
625
626 fn order_list_id(&self) -> Option<OrderListId> {
627 self.order_list_id
628 }
629
630 fn linked_order_ids(&self) -> Option<Vec<ClientOrderId>> {
631 self.linked_order_ids.clone()
632 }
633
634 fn parent_order_id(&self) -> Option<ClientOrderId> {
635 self.parent_order_id
636 }
637
638 fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
639 self.exec_algorithm_id
640 }
641
642 fn exec_algorithm_params(&self) -> Option<IndexMap<Ustr, Ustr>> {
643 self.exec_algorithm_params.clone()
644 }
645
646 fn exec_spawn_id(&self) -> Option<ClientOrderId> {
647 self.exec_spawn_id
648 }
649
650 fn tags(&self) -> Option<Vec<Ustr>> {
651 self.tags.clone()
652 }
653
654 fn venue_order_id(&self) -> Option<VenueOrderId> {
655 None
656 }
657
658 fn account_id(&self) -> Option<AccountId> {
659 None
660 }
661
662 fn position_id(&self) -> Option<PositionId> {
663 None
664 }
665
666 fn commission(&self) -> Option<Money> {
667 None
668 }
669
670 fn ts_event(&self) -> UnixNanos {
671 self.ts_event
672 }
673
674 fn ts_init(&self) -> UnixNanos {
675 self.ts_init
676 }
677}
678
679impl TryFrom<OrderInitialized> for OrderAny {
680 type Error = OrderError;
681
682 fn try_from(order: OrderInitialized) -> Result<Self, Self::Error> {
683 Ok(match order.order_type {
684 OrderType::Limit => Self::Limit(order.try_into()?),
685 OrderType::Market => Self::Market(order.try_into()?),
686 OrderType::StopMarket => Self::StopMarket(order.try_into()?),
687 OrderType::StopLimit => Self::StopLimit(order.try_into()?),
688 OrderType::LimitIfTouched => Self::LimitIfTouched(order.try_into()?),
689 OrderType::TrailingStopLimit => Self::TrailingStopLimit(order.try_into()?),
690 OrderType::TrailingStopMarket => Self::TrailingStopMarket(order.try_into()?),
691 OrderType::MarketToLimit => Self::MarketToLimit(order.try_into()?),
692 OrderType::MarketIfTouched => Self::MarketIfTouched(order.try_into()?),
693 })
694 }
695}
696
697#[cfg(test)]
698mod test {
699 use indexmap::IndexMap;
700 use rstest::rstest;
701 use ustr::Ustr;
702
703 use crate::events::{
704 OrderEvent,
705 order::{initialized::OrderInitialized, stubs::*},
706 };
707
708 #[rstest]
709 fn test_order_initialized(order_initialized_buy_limit: OrderInitialized) {
710 let display = format!("{order_initialized_buy_limit}");
711 assert_eq!(
712 display,
713 "OrderInitialized(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-000000-001-001-1, \
714 side=BUY, type=LIMIT, quantity=0.561, time_in_force=DAY, post_only=true, reduce_only=true, \
715 quote_quantity=false, price=22000, emulation_trigger=BID_ASK, trigger_instrument_id=BTCUSDT.COINBASE, \
716 contingency_type=OTO, order_list_id=1, linked_order_ids=[O-2020872378424], parent_order_id=None, \
717 exec_algorithm_id=None, exec_algorithm_params=None, exec_spawn_id=None, tags=None)"
718 );
719 }
720
721 #[rstest]
722 fn test_order_initialized_event_exposes_tags_and_exec_algorithm_params() {
723 let mut params = IndexMap::new();
724 params.insert(Ustr::from("speed"), Ustr::from("fast"));
725 let tags = vec![Ustr::from("tag-1"), Ustr::from("tag-2")];
726 let event = OrderInitialized {
727 exec_algorithm_params: Some(params.clone()),
728 tags: Some(tags.clone()),
729 ..OrderInitialized::default()
730 };
731
732 assert_eq!(OrderEvent::exec_algorithm_params(&event), Some(params));
733 assert_eq!(OrderEvent::tags(&event), Some(tags));
734 }
735
736 #[rstest]
737 fn test_order_initialized_serialization() {
738 let original = OrderInitialized::default();
739 let json = serde_json::to_string(&original).unwrap();
740 let deserialized: OrderInitialized = serde_json::from_str(&json).unwrap();
741 assert_eq!(original, deserialized);
742 }
743
744 #[rstest]
745 fn test_order_initialized_serialization_preserves_activation_price() {
746 use crate::types::Price;
747
748 let original = OrderInitialized {
749 activation_price: Some(Price::from("0.68500")),
750 ..OrderInitialized::default()
751 };
752 let json = serde_json::to_string(&original).unwrap();
753 let deserialized: OrderInitialized = serde_json::from_str(&json).unwrap();
754
755 assert_eq!(deserialized.activation_price, Some(Price::from("0.68500")));
756 assert_eq!(original, deserialized);
757 }
758}