1use std::fmt::Display;
17
18use nautilus_core::{UUID4, UnixNanos};
19use rust_decimal::Decimal;
20use serde::{Deserialize, Serialize};
21
22use crate::{
23 enums::{
24 ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType,
25 TriggerType,
26 },
27 identifiers::{AccountId, ClientOrderId, InstrumentId, OrderListId, PositionId, VenueOrderId},
28 types::{Price, Quantity},
29};
30
31#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
33#[serde(tag = "type")]
34#[cfg_attr(
35 feature = "python",
36 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
37)]
38pub struct OrderStatusReport {
39 pub account_id: AccountId,
41 pub instrument_id: InstrumentId,
43 pub client_order_id: Option<ClientOrderId>,
45 pub venue_order_id: VenueOrderId,
47 pub order_side: OrderSide,
49 pub order_type: OrderType,
51 pub time_in_force: TimeInForce,
53 pub order_status: OrderStatus,
55 pub quantity: Quantity,
57 pub filled_qty: Quantity,
59 pub report_id: UUID4,
61 pub ts_accepted: UnixNanos,
63 pub ts_last: UnixNanos,
65 pub ts_init: UnixNanos,
67 pub order_list_id: Option<OrderListId>,
69 pub venue_position_id: Option<PositionId>,
71 pub linked_order_ids: Option<Vec<ClientOrderId>>,
73 pub parent_order_id: Option<ClientOrderId>,
75 pub contingency_type: ContingencyType,
77 pub expire_time: Option<UnixNanos>,
79 pub price: Option<Price>,
81 pub trigger_price: Option<Price>,
83 pub trigger_type: Option<TriggerType>,
85 pub limit_offset: Option<Decimal>,
87 pub trailing_offset: Option<Decimal>,
89 pub trailing_offset_type: TrailingOffsetType,
91 pub avg_px: Option<Decimal>,
93 pub display_qty: Option<Quantity>,
95 pub post_only: bool,
97 pub reduce_only: bool,
99 pub cancel_reason: Option<String>,
101 pub ts_triggered: Option<UnixNanos>,
103}
104
105impl OrderStatusReport {
106 #[allow(clippy::too_many_arguments)]
108 #[must_use]
109 pub fn new(
110 account_id: AccountId,
111 instrument_id: InstrumentId,
112 client_order_id: Option<ClientOrderId>,
113 venue_order_id: VenueOrderId,
114 order_side: OrderSide,
115 order_type: OrderType,
116 time_in_force: TimeInForce,
117 order_status: OrderStatus,
118 quantity: Quantity,
119 filled_qty: Quantity,
120 ts_accepted: UnixNanos,
121 ts_last: UnixNanos,
122 ts_init: UnixNanos,
123 report_id: Option<UUID4>,
124 ) -> Self {
125 Self {
126 account_id,
127 instrument_id,
128 client_order_id,
129 venue_order_id,
130 order_side,
131 order_type,
132 time_in_force,
133 order_status,
134 quantity,
135 filled_qty,
136 report_id: report_id.unwrap_or_default(),
137 ts_accepted,
138 ts_last,
139 ts_init,
140 order_list_id: None,
141 venue_position_id: None,
142 linked_order_ids: None,
143 parent_order_id: None,
144 contingency_type: ContingencyType::default(),
145 expire_time: None,
146 price: None,
147 trigger_price: None,
148 trigger_type: None,
149 limit_offset: None,
150 trailing_offset: None,
151 trailing_offset_type: TrailingOffsetType::default(),
152 avg_px: None,
153 display_qty: None,
154 post_only: false,
155 reduce_only: false,
156 cancel_reason: None,
157 ts_triggered: None,
158 }
159 }
160
161 #[must_use]
163 pub const fn with_client_order_id(mut self, client_order_id: ClientOrderId) -> Self {
164 self.client_order_id = Some(client_order_id);
165 self
166 }
167
168 #[must_use]
170 pub const fn with_order_list_id(mut self, order_list_id: OrderListId) -> Self {
171 self.order_list_id = Some(order_list_id);
172 self
173 }
174
175 #[must_use]
177 pub fn with_linked_order_ids(
178 mut self,
179 linked_order_ids: impl IntoIterator<Item = ClientOrderId>,
180 ) -> Self {
181 self.linked_order_ids = Some(linked_order_ids.into_iter().collect());
182 self
183 }
184
185 #[must_use]
187 pub const fn with_parent_order_id(mut self, parent_order_id: ClientOrderId) -> Self {
188 self.parent_order_id = Some(parent_order_id);
189 self
190 }
191
192 #[must_use]
194 pub const fn with_venue_position_id(mut self, venue_position_id: PositionId) -> Self {
195 self.venue_position_id = Some(venue_position_id);
196 self
197 }
198
199 #[must_use]
201 pub const fn with_price(mut self, price: Price) -> Self {
202 self.price = Some(price);
203 self
204 }
205
206 pub fn with_avg_px(mut self, avg_px: f64) -> anyhow::Result<Self> {
212 if !avg_px.is_finite() {
213 anyhow::bail!(
214 "avg_px must be finite, got: {} (is_nan: {}, is_infinite: {})",
215 avg_px,
216 avg_px.is_nan(),
217 avg_px.is_infinite()
218 );
219 }
220
221 self.avg_px = Some(Decimal::from_f64_retain(avg_px).ok_or_else(|| {
222 anyhow::anyhow!(
223 "Failed to convert avg_px to Decimal: {} (possible overflow/underflow)",
224 avg_px
225 )
226 })?);
227 Ok(self)
228 }
229
230 #[must_use]
232 pub const fn with_trigger_price(mut self, trigger_price: Price) -> Self {
233 self.trigger_price = Some(trigger_price);
234 self
235 }
236
237 #[must_use]
239 pub const fn with_trigger_type(mut self, trigger_type: TriggerType) -> Self {
240 self.trigger_type = Some(trigger_type);
241 self
242 }
243
244 #[must_use]
246 pub const fn with_limit_offset(mut self, limit_offset: Decimal) -> Self {
247 self.limit_offset = Some(limit_offset);
248 self
249 }
250
251 #[must_use]
253 pub const fn with_trailing_offset(mut self, trailing_offset: Decimal) -> Self {
254 self.trailing_offset = Some(trailing_offset);
255 self
256 }
257
258 #[must_use]
260 pub const fn with_trailing_offset_type(
261 mut self,
262 trailing_offset_type: TrailingOffsetType,
263 ) -> Self {
264 self.trailing_offset_type = trailing_offset_type;
265 self
266 }
267
268 #[must_use]
270 pub const fn with_display_qty(mut self, display_qty: Quantity) -> Self {
271 self.display_qty = Some(display_qty);
272 self
273 }
274
275 #[must_use]
277 pub const fn with_expire_time(mut self, expire_time: UnixNanos) -> Self {
278 self.expire_time = Some(expire_time);
279 self
280 }
281
282 #[must_use]
284 pub const fn with_post_only(mut self, post_only: bool) -> Self {
285 self.post_only = post_only;
286 self
287 }
288
289 #[must_use]
291 pub const fn with_reduce_only(mut self, reduce_only: bool) -> Self {
292 self.reduce_only = reduce_only;
293 self
294 }
295
296 #[must_use]
298 pub fn with_cancel_reason(mut self, cancel_reason: String) -> Self {
299 self.cancel_reason = Some(cancel_reason);
300 self
301 }
302
303 #[must_use]
305 pub const fn with_ts_triggered(mut self, ts_triggered: UnixNanos) -> Self {
306 self.ts_triggered = Some(ts_triggered);
307 self
308 }
309
310 #[must_use]
312 pub const fn with_contingency_type(mut self, contingency_type: ContingencyType) -> Self {
313 self.contingency_type = contingency_type;
314 self
315 }
316}
317
318impl Display for OrderStatusReport {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 write!(
321 f,
322 "OrderStatusReport(\
323 account_id={}, \
324 instrument_id={}, \
325 venue_order_id={}, \
326 order_side={}, \
327 order_type={}, \
328 time_in_force={}, \
329 order_status={}, \
330 quantity={}, \
331 filled_qty={}, \
332 report_id={}, \
333 ts_accepted={}, \
334 ts_last={}, \
335 ts_init={}, \
336 client_order_id={:?}, \
337 order_list_id={:?}, \
338 venue_position_id={:?}, \
339 linked_order_ids={:?}, \
340 parent_order_id={:?}, \
341 contingency_type={}, \
342 expire_time={:?}, \
343 price={:?}, \
344 trigger_price={:?}, \
345 trigger_type={:?}, \
346 limit_offset={:?}, \
347 trailing_offset={:?}, \
348 trailing_offset_type={}, \
349 avg_px={:?}, \
350 display_qty={:?}, \
351 post_only={}, \
352 reduce_only={}, \
353 cancel_reason={:?}, \
354 ts_triggered={:?}\
355 )",
356 self.account_id,
357 self.instrument_id,
358 self.venue_order_id,
359 self.order_side,
360 self.order_type,
361 self.time_in_force,
362 self.order_status,
363 self.quantity,
364 self.filled_qty,
365 self.report_id,
366 self.ts_accepted,
367 self.ts_last,
368 self.ts_init,
369 self.client_order_id,
370 self.order_list_id,
371 self.venue_position_id,
372 self.linked_order_ids,
373 self.parent_order_id,
374 self.contingency_type,
375 self.expire_time,
376 self.price,
377 self.trigger_price,
378 self.trigger_type,
379 self.limit_offset,
380 self.trailing_offset,
381 self.trailing_offset_type,
382 self.avg_px,
383 self.display_qty,
384 self.post_only,
385 self.reduce_only,
386 self.cancel_reason,
387 self.ts_triggered,
388 )
389 }
390}
391
392#[cfg(test)]
396mod tests {
397 use nautilus_core::UnixNanos;
398 use rstest::*;
399 use rust_decimal::Decimal;
400
401 use super::*;
402 use crate::{
403 enums::{
404 ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType,
405 TriggerType,
406 },
407 identifiers::{
408 AccountId, ClientOrderId, InstrumentId, OrderListId, PositionId, VenueOrderId,
409 },
410 types::{Price, Quantity},
411 };
412
413 fn test_order_status_report() -> OrderStatusReport {
414 OrderStatusReport::new(
415 AccountId::from("SIM-001"),
416 InstrumentId::from("AUDUSD.SIM"),
417 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
418 VenueOrderId::from("1"),
419 OrderSide::Buy,
420 OrderType::Limit,
421 TimeInForce::Gtc,
422 OrderStatus::Accepted,
423 Quantity::from("100"),
424 Quantity::from("0"),
425 UnixNanos::from(1_000_000_000),
426 UnixNanos::from(2_000_000_000),
427 UnixNanos::from(3_000_000_000),
428 None,
429 )
430 }
431
432 #[rstest]
433 fn test_order_status_report_new() {
434 let report = test_order_status_report();
435
436 assert_eq!(report.account_id, AccountId::from("SIM-001"));
437 assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
438 assert_eq!(
439 report.client_order_id,
440 Some(ClientOrderId::from("O-19700101-000000-001-001-1"))
441 );
442 assert_eq!(report.venue_order_id, VenueOrderId::from("1"));
443 assert_eq!(report.order_side, OrderSide::Buy);
444 assert_eq!(report.order_type, OrderType::Limit);
445 assert_eq!(report.time_in_force, TimeInForce::Gtc);
446 assert_eq!(report.order_status, OrderStatus::Accepted);
447 assert_eq!(report.quantity, Quantity::from("100"));
448 assert_eq!(report.filled_qty, Quantity::from("0"));
449 assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
450 assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
451 assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
452
453 assert_eq!(report.order_list_id, None);
455 assert_eq!(report.venue_position_id, None);
456 assert_eq!(report.linked_order_ids, None);
457 assert_eq!(report.parent_order_id, None);
458 assert_eq!(report.contingency_type, ContingencyType::default());
459 assert_eq!(report.expire_time, None);
460 assert_eq!(report.price, None);
461 assert_eq!(report.trigger_price, None);
462 assert_eq!(report.trigger_type, None);
463 assert_eq!(report.limit_offset, None);
464 assert_eq!(report.trailing_offset, None);
465 assert_eq!(report.trailing_offset_type, TrailingOffsetType::default());
466 assert_eq!(report.avg_px, None);
467 assert_eq!(report.display_qty, None);
468 assert!(!report.post_only);
469 assert!(!report.reduce_only);
470 assert_eq!(report.cancel_reason, None);
471 assert_eq!(report.ts_triggered, None);
472 }
473
474 #[rstest]
475 fn test_order_status_report_with_generated_report_id() {
476 let report = OrderStatusReport::new(
477 AccountId::from("SIM-001"),
478 InstrumentId::from("AUDUSD.SIM"),
479 None,
480 VenueOrderId::from("1"),
481 OrderSide::Buy,
482 OrderType::Market,
483 TimeInForce::Ioc,
484 OrderStatus::Filled,
485 Quantity::from("100"),
486 Quantity::from("100"),
487 UnixNanos::from(1_000_000_000),
488 UnixNanos::from(2_000_000_000),
489 UnixNanos::from(3_000_000_000),
490 None, );
492
493 assert_ne!(
495 report.report_id.to_string(),
496 "00000000-0000-0000-0000-000000000000"
497 );
498 }
499
500 #[rstest]
501 fn test_order_status_report_builder_methods() -> anyhow::Result<()> {
502 let report = test_order_status_report()
503 .with_client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
504 .with_order_list_id(OrderListId::from("OL-001"))
505 .with_venue_position_id(PositionId::from("P-001"))
506 .with_parent_order_id(ClientOrderId::from("O-PARENT"))
507 .with_price(Price::from("1.00000"))
508 .with_avg_px(1.00001)?
509 .with_trigger_price(Price::from("0.99000"))
510 .with_trigger_type(TriggerType::Default)
511 .with_limit_offset(Decimal::from_f64_retain(0.0001).unwrap())
512 .with_trailing_offset(Decimal::from_f64_retain(0.0002).unwrap())
513 .with_trailing_offset_type(TrailingOffsetType::BasisPoints)
514 .with_display_qty(Quantity::from("50"))
515 .with_expire_time(UnixNanos::from(4_000_000_000))
516 .with_post_only(true)
517 .with_reduce_only(true)
518 .with_cancel_reason("User requested".to_string())
519 .with_ts_triggered(UnixNanos::from(1_500_000_000))
520 .with_contingency_type(ContingencyType::Oco);
521
522 assert_eq!(
523 report.client_order_id,
524 Some(ClientOrderId::from("O-19700101-000000-001-001-2"))
525 );
526 assert_eq!(report.order_list_id, Some(OrderListId::from("OL-001")));
527 assert_eq!(report.venue_position_id, Some(PositionId::from("P-001")));
528 assert_eq!(
529 report.parent_order_id,
530 Some(ClientOrderId::from("O-PARENT"))
531 );
532 assert_eq!(report.price, Some(Price::from("1.00000")));
533 assert_eq!(
534 report.avg_px,
535 Some(Decimal::from_f64_retain(1.00001).unwrap())
536 );
537 assert_eq!(report.trigger_price, Some(Price::from("0.99000")));
538 assert_eq!(report.trigger_type, Some(TriggerType::Default));
539 assert_eq!(
540 report.limit_offset,
541 Some(Decimal::from_f64_retain(0.0001).unwrap())
542 );
543 assert_eq!(
544 report.trailing_offset,
545 Some(Decimal::from_f64_retain(0.0002).unwrap())
546 );
547 assert_eq!(report.trailing_offset_type, TrailingOffsetType::BasisPoints);
548 assert_eq!(report.display_qty, Some(Quantity::from("50")));
549 assert_eq!(report.expire_time, Some(UnixNanos::from(4_000_000_000)));
550 assert!(report.post_only);
551 assert!(report.reduce_only);
552 assert_eq!(report.cancel_reason, Some("User requested".to_string()));
553 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
554 assert_eq!(report.contingency_type, ContingencyType::Oco);
555 Ok(())
556 }
557
558 #[rstest]
559 fn test_display() {
560 let report = test_order_status_report();
561 let display_str = format!("{report}");
562
563 assert!(display_str.contains("OrderStatusReport"));
564 assert!(display_str.contains("SIM-001"));
565 assert!(display_str.contains("AUDUSD.SIM"));
566 assert!(display_str.contains("BUY"));
567 assert!(display_str.contains("LIMIT"));
568 assert!(display_str.contains("GTC"));
569 assert!(display_str.contains("ACCEPTED"));
570 assert!(display_str.contains("100"));
571 }
572
573 #[rstest]
574 fn test_clone_and_equality() {
575 let report1 = test_order_status_report();
576 let report2 = report1.clone();
577
578 assert_eq!(report1, report2);
579 }
580
581 #[rstest]
582 fn test_serialization_roundtrip() {
583 let original = test_order_status_report();
584
585 let json = serde_json::to_string(&original).unwrap();
587 let deserialized: OrderStatusReport = serde_json::from_str(&json).unwrap();
588 assert_eq!(original, deserialized);
589 }
590
591 #[rstest]
592 fn test_order_status_report_different_order_types() {
593 let market_report = OrderStatusReport::new(
594 AccountId::from("SIM-001"),
595 InstrumentId::from("AUDUSD.SIM"),
596 None,
597 VenueOrderId::from("1"),
598 OrderSide::Buy,
599 OrderType::Market,
600 TimeInForce::Ioc,
601 OrderStatus::Filled,
602 Quantity::from("100"),
603 Quantity::from("100"),
604 UnixNanos::from(1_000_000_000),
605 UnixNanos::from(2_000_000_000),
606 UnixNanos::from(3_000_000_000),
607 None,
608 );
609
610 let stop_report = OrderStatusReport::new(
611 AccountId::from("SIM-001"),
612 InstrumentId::from("AUDUSD.SIM"),
613 None,
614 VenueOrderId::from("2"),
615 OrderSide::Sell,
616 OrderType::StopMarket,
617 TimeInForce::Gtc,
618 OrderStatus::Accepted,
619 Quantity::from("50"),
620 Quantity::from("0"),
621 UnixNanos::from(1_000_000_000),
622 UnixNanos::from(2_000_000_000),
623 UnixNanos::from(3_000_000_000),
624 None,
625 );
626
627 assert_eq!(market_report.order_type, OrderType::Market);
628 assert_eq!(stop_report.order_type, OrderType::StopMarket);
629 assert_ne!(market_report, stop_report);
630 }
631
632 #[rstest]
633 fn test_order_status_report_different_statuses() {
634 let accepted_report = test_order_status_report();
635
636 let filled_report = OrderStatusReport::new(
637 AccountId::from("SIM-001"),
638 InstrumentId::from("AUDUSD.SIM"),
639 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
640 VenueOrderId::from("1"),
641 OrderSide::Buy,
642 OrderType::Limit,
643 TimeInForce::Gtc,
644 OrderStatus::Filled,
645 Quantity::from("100"),
646 Quantity::from("100"), UnixNanos::from(1_000_000_000),
648 UnixNanos::from(2_000_000_000),
649 UnixNanos::from(3_000_000_000),
650 None,
651 );
652
653 assert_eq!(accepted_report.order_status, OrderStatus::Accepted);
654 assert_eq!(filled_report.order_status, OrderStatus::Filled);
655 assert_ne!(accepted_report, filled_report);
656 }
657
658 #[rstest]
659 fn test_order_status_report_with_optional_fields() -> anyhow::Result<()> {
660 let mut report = test_order_status_report();
661
662 assert_eq!(report.price, None);
664 assert_eq!(report.avg_px, None);
665 assert!(!report.post_only);
666 assert!(!report.reduce_only);
667
668 report = report
670 .with_price(Price::from("1.00000"))
671 .with_avg_px(1.00001)?
672 .with_post_only(true)
673 .with_reduce_only(true);
674
675 assert_eq!(report.price, Some(Price::from("1.00000")));
676 assert_eq!(
677 report.avg_px,
678 Some(Decimal::from_f64_retain(1.00001).unwrap())
679 );
680 assert!(report.post_only);
681 assert!(report.reduce_only);
682 Ok(())
683 }
684
685 #[rstest]
686 fn test_order_status_report_partial_fill() {
687 let partial_fill_report = OrderStatusReport::new(
688 AccountId::from("SIM-001"),
689 InstrumentId::from("AUDUSD.SIM"),
690 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
691 VenueOrderId::from("1"),
692 OrderSide::Buy,
693 OrderType::Limit,
694 TimeInForce::Gtc,
695 OrderStatus::PartiallyFilled,
696 Quantity::from("100"),
697 Quantity::from("30"), UnixNanos::from(1_000_000_000),
699 UnixNanos::from(2_000_000_000),
700 UnixNanos::from(3_000_000_000),
701 None,
702 );
703
704 assert_eq!(partial_fill_report.quantity, Quantity::from("100"));
705 assert_eq!(partial_fill_report.filled_qty, Quantity::from("30"));
706 assert_eq!(
707 partial_fill_report.order_status,
708 OrderStatus::PartiallyFilled
709 );
710 }
711
712 #[rstest]
713 fn test_order_status_report_with_all_timestamp_fields() {
714 let report = OrderStatusReport::new(
715 AccountId::from("SIM-001"),
716 InstrumentId::from("AUDUSD.SIM"),
717 None,
718 VenueOrderId::from("1"),
719 OrderSide::Buy,
720 OrderType::StopLimit,
721 TimeInForce::Gtc,
722 OrderStatus::Triggered,
723 Quantity::from("100"),
724 Quantity::from("0"),
725 UnixNanos::from(1_000_000_000), UnixNanos::from(2_000_000_000), UnixNanos::from(3_000_000_000), None,
729 )
730 .with_ts_triggered(UnixNanos::from(1_500_000_000));
731
732 assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
733 assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
734 assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
735 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
736 }
737}