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<f64>,
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 #[must_use]
208 pub const fn with_avg_px(mut self, avg_px: f64) -> Self {
209 self.avg_px = Some(avg_px);
210 self
211 }
212
213 #[must_use]
215 pub const fn with_trigger_price(mut self, trigger_price: Price) -> Self {
216 self.trigger_price = Some(trigger_price);
217 self
218 }
219
220 #[must_use]
222 pub const fn with_trigger_type(mut self, trigger_type: TriggerType) -> Self {
223 self.trigger_type = Some(trigger_type);
224 self
225 }
226
227 #[must_use]
229 pub const fn with_limit_offset(mut self, limit_offset: Decimal) -> Self {
230 self.limit_offset = Some(limit_offset);
231 self
232 }
233
234 #[must_use]
236 pub const fn with_trailing_offset(mut self, trailing_offset: Decimal) -> Self {
237 self.trailing_offset = Some(trailing_offset);
238 self
239 }
240
241 #[must_use]
243 pub const fn with_trailing_offset_type(
244 mut self,
245 trailing_offset_type: TrailingOffsetType,
246 ) -> Self {
247 self.trailing_offset_type = trailing_offset_type;
248 self
249 }
250
251 #[must_use]
253 pub const fn with_display_qty(mut self, display_qty: Quantity) -> Self {
254 self.display_qty = Some(display_qty);
255 self
256 }
257
258 #[must_use]
260 pub const fn with_expire_time(mut self, expire_time: UnixNanos) -> Self {
261 self.expire_time = Some(expire_time);
262 self
263 }
264
265 #[must_use]
267 pub const fn with_post_only(mut self, post_only: bool) -> Self {
268 self.post_only = post_only;
269 self
270 }
271
272 #[must_use]
274 pub const fn with_reduce_only(mut self, reduce_only: bool) -> Self {
275 self.reduce_only = reduce_only;
276 self
277 }
278
279 #[must_use]
281 pub fn with_cancel_reason(mut self, cancel_reason: String) -> Self {
282 self.cancel_reason = Some(cancel_reason);
283 self
284 }
285
286 #[must_use]
288 pub const fn with_ts_triggered(mut self, ts_triggered: UnixNanos) -> Self {
289 self.ts_triggered = Some(ts_triggered);
290 self
291 }
292
293 #[must_use]
295 pub const fn with_contingency_type(mut self, contingency_type: ContingencyType) -> Self {
296 self.contingency_type = contingency_type;
297 self
298 }
299}
300
301impl Display for OrderStatusReport {
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 write!(
304 f,
305 "OrderStatusReport(\
306 account_id={}, \
307 instrument_id={}, \
308 venue_order_id={}, \
309 order_side={}, \
310 order_type={}, \
311 time_in_force={}, \
312 order_status={}, \
313 quantity={}, \
314 filled_qty={}, \
315 report_id={}, \
316 ts_accepted={}, \
317 ts_last={}, \
318 ts_init={}, \
319 client_order_id={:?}, \
320 order_list_id={:?}, \
321 venue_position_id={:?}, \
322 linked_order_ids={:?}, \
323 parent_order_id={:?}, \
324 contingency_type={}, \
325 expire_time={:?}, \
326 price={:?}, \
327 trigger_price={:?}, \
328 trigger_type={:?}, \
329 limit_offset={:?}, \
330 trailing_offset={:?}, \
331 trailing_offset_type={}, \
332 avg_px={:?}, \
333 display_qty={:?}, \
334 post_only={}, \
335 reduce_only={}, \
336 cancel_reason={:?}, \
337 ts_triggered={:?}\
338 )",
339 self.account_id,
340 self.instrument_id,
341 self.venue_order_id,
342 self.order_side,
343 self.order_type,
344 self.time_in_force,
345 self.order_status,
346 self.quantity,
347 self.filled_qty,
348 self.report_id,
349 self.ts_accepted,
350 self.ts_last,
351 self.ts_init,
352 self.client_order_id,
353 self.order_list_id,
354 self.venue_position_id,
355 self.linked_order_ids,
356 self.parent_order_id,
357 self.contingency_type,
358 self.expire_time,
359 self.price,
360 self.trigger_price,
361 self.trigger_type,
362 self.limit_offset,
363 self.trailing_offset,
364 self.trailing_offset_type,
365 self.avg_px,
366 self.display_qty,
367 self.post_only,
368 self.reduce_only,
369 self.cancel_reason,
370 self.ts_triggered,
371 )
372 }
373}
374
375#[cfg(test)]
379mod tests {
380 use nautilus_core::UnixNanos;
381 use rstest::*;
382 use rust_decimal::Decimal;
383
384 use super::*;
385 use crate::{
386 enums::{
387 ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType,
388 TriggerType,
389 },
390 identifiers::{
391 AccountId, ClientOrderId, InstrumentId, OrderListId, PositionId, VenueOrderId,
392 },
393 types::{Price, Quantity},
394 };
395
396 fn test_order_status_report() -> OrderStatusReport {
397 OrderStatusReport::new(
398 AccountId::from("SIM-001"),
399 InstrumentId::from("AUDUSD.SIM"),
400 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
401 VenueOrderId::from("1"),
402 OrderSide::Buy,
403 OrderType::Limit,
404 TimeInForce::Gtc,
405 OrderStatus::Accepted,
406 Quantity::from("100"),
407 Quantity::from("0"),
408 UnixNanos::from(1_000_000_000),
409 UnixNanos::from(2_000_000_000),
410 UnixNanos::from(3_000_000_000),
411 None,
412 )
413 }
414
415 #[rstest]
416 fn test_order_status_report_new() {
417 let report = test_order_status_report();
418
419 assert_eq!(report.account_id, AccountId::from("SIM-001"));
420 assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
421 assert_eq!(
422 report.client_order_id,
423 Some(ClientOrderId::from("O-19700101-000000-001-001-1"))
424 );
425 assert_eq!(report.venue_order_id, VenueOrderId::from("1"));
426 assert_eq!(report.order_side, OrderSide::Buy);
427 assert_eq!(report.order_type, OrderType::Limit);
428 assert_eq!(report.time_in_force, TimeInForce::Gtc);
429 assert_eq!(report.order_status, OrderStatus::Accepted);
430 assert_eq!(report.quantity, Quantity::from("100"));
431 assert_eq!(report.filled_qty, Quantity::from("0"));
432 assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
433 assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
434 assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
435
436 assert_eq!(report.order_list_id, None);
438 assert_eq!(report.venue_position_id, None);
439 assert_eq!(report.linked_order_ids, None);
440 assert_eq!(report.parent_order_id, None);
441 assert_eq!(report.contingency_type, ContingencyType::default());
442 assert_eq!(report.expire_time, None);
443 assert_eq!(report.price, None);
444 assert_eq!(report.trigger_price, None);
445 assert_eq!(report.trigger_type, None);
446 assert_eq!(report.limit_offset, None);
447 assert_eq!(report.trailing_offset, None);
448 assert_eq!(report.trailing_offset_type, TrailingOffsetType::default());
449 assert_eq!(report.avg_px, None);
450 assert_eq!(report.display_qty, None);
451 assert!(!report.post_only);
452 assert!(!report.reduce_only);
453 assert_eq!(report.cancel_reason, None);
454 assert_eq!(report.ts_triggered, None);
455 }
456
457 #[rstest]
458 fn test_order_status_report_with_generated_report_id() {
459 let report = OrderStatusReport::new(
460 AccountId::from("SIM-001"),
461 InstrumentId::from("AUDUSD.SIM"),
462 None,
463 VenueOrderId::from("1"),
464 OrderSide::Buy,
465 OrderType::Market,
466 TimeInForce::Ioc,
467 OrderStatus::Filled,
468 Quantity::from("100"),
469 Quantity::from("100"),
470 UnixNanos::from(1_000_000_000),
471 UnixNanos::from(2_000_000_000),
472 UnixNanos::from(3_000_000_000),
473 None, );
475
476 assert_ne!(
478 report.report_id.to_string(),
479 "00000000-0000-0000-0000-000000000000"
480 );
481 }
482
483 #[rstest]
484 fn test_order_status_report_builder_methods() {
485 let report = test_order_status_report()
486 .with_client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
487 .with_order_list_id(OrderListId::from("OL-001"))
488 .with_venue_position_id(PositionId::from("P-001"))
489 .with_parent_order_id(ClientOrderId::from("O-PARENT"))
490 .with_price(Price::from("1.00000"))
491 .with_avg_px(1.00001)
492 .with_trigger_price(Price::from("0.99000"))
493 .with_trigger_type(TriggerType::Default)
494 .with_limit_offset(Decimal::from_f64_retain(0.0001).unwrap())
495 .with_trailing_offset(Decimal::from_f64_retain(0.0002).unwrap())
496 .with_trailing_offset_type(TrailingOffsetType::BasisPoints)
497 .with_display_qty(Quantity::from("50"))
498 .with_expire_time(UnixNanos::from(4_000_000_000))
499 .with_post_only(true)
500 .with_reduce_only(true)
501 .with_cancel_reason("User requested".to_string())
502 .with_ts_triggered(UnixNanos::from(1_500_000_000))
503 .with_contingency_type(ContingencyType::Oco);
504
505 assert_eq!(
506 report.client_order_id,
507 Some(ClientOrderId::from("O-19700101-000000-001-001-2"))
508 );
509 assert_eq!(report.order_list_id, Some(OrderListId::from("OL-001")));
510 assert_eq!(report.venue_position_id, Some(PositionId::from("P-001")));
511 assert_eq!(
512 report.parent_order_id,
513 Some(ClientOrderId::from("O-PARENT"))
514 );
515 assert_eq!(report.price, Some(Price::from("1.00000")));
516 assert_eq!(report.avg_px, Some(1.00001));
517 assert_eq!(report.trigger_price, Some(Price::from("0.99000")));
518 assert_eq!(report.trigger_type, Some(TriggerType::Default));
519 assert_eq!(
520 report.limit_offset,
521 Some(Decimal::from_f64_retain(0.0001).unwrap())
522 );
523 assert_eq!(
524 report.trailing_offset,
525 Some(Decimal::from_f64_retain(0.0002).unwrap())
526 );
527 assert_eq!(report.trailing_offset_type, TrailingOffsetType::BasisPoints);
528 assert_eq!(report.display_qty, Some(Quantity::from("50")));
529 assert_eq!(report.expire_time, Some(UnixNanos::from(4_000_000_000)));
530 assert!(report.post_only);
531 assert!(report.reduce_only);
532 assert_eq!(report.cancel_reason, Some("User requested".to_string()));
533 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
534 assert_eq!(report.contingency_type, ContingencyType::Oco);
535 }
536
537 #[rstest]
538 fn test_display() {
539 let report = test_order_status_report();
540 let display_str = format!("{report}");
541
542 assert!(display_str.contains("OrderStatusReport"));
543 assert!(display_str.contains("SIM-001"));
544 assert!(display_str.contains("AUDUSD.SIM"));
545 assert!(display_str.contains("BUY"));
546 assert!(display_str.contains("LIMIT"));
547 assert!(display_str.contains("GTC"));
548 assert!(display_str.contains("ACCEPTED"));
549 assert!(display_str.contains("100"));
550 }
551
552 #[rstest]
553 fn test_clone_and_equality() {
554 let report1 = test_order_status_report();
555 let report2 = report1.clone();
556
557 assert_eq!(report1, report2);
558 }
559
560 #[rstest]
561 fn test_serialization_roundtrip() {
562 let original = test_order_status_report();
563
564 let json = serde_json::to_string(&original).unwrap();
566 let deserialized: OrderStatusReport = serde_json::from_str(&json).unwrap();
567 assert_eq!(original, deserialized);
568 }
569
570 #[rstest]
571 fn test_order_status_report_different_order_types() {
572 let market_report = OrderStatusReport::new(
573 AccountId::from("SIM-001"),
574 InstrumentId::from("AUDUSD.SIM"),
575 None,
576 VenueOrderId::from("1"),
577 OrderSide::Buy,
578 OrderType::Market,
579 TimeInForce::Ioc,
580 OrderStatus::Filled,
581 Quantity::from("100"),
582 Quantity::from("100"),
583 UnixNanos::from(1_000_000_000),
584 UnixNanos::from(2_000_000_000),
585 UnixNanos::from(3_000_000_000),
586 None,
587 );
588
589 let stop_report = OrderStatusReport::new(
590 AccountId::from("SIM-001"),
591 InstrumentId::from("AUDUSD.SIM"),
592 None,
593 VenueOrderId::from("2"),
594 OrderSide::Sell,
595 OrderType::StopMarket,
596 TimeInForce::Gtc,
597 OrderStatus::Accepted,
598 Quantity::from("50"),
599 Quantity::from("0"),
600 UnixNanos::from(1_000_000_000),
601 UnixNanos::from(2_000_000_000),
602 UnixNanos::from(3_000_000_000),
603 None,
604 );
605
606 assert_eq!(market_report.order_type, OrderType::Market);
607 assert_eq!(stop_report.order_type, OrderType::StopMarket);
608 assert_ne!(market_report, stop_report);
609 }
610
611 #[rstest]
612 fn test_order_status_report_different_statuses() {
613 let accepted_report = test_order_status_report();
614
615 let filled_report = OrderStatusReport::new(
616 AccountId::from("SIM-001"),
617 InstrumentId::from("AUDUSD.SIM"),
618 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
619 VenueOrderId::from("1"),
620 OrderSide::Buy,
621 OrderType::Limit,
622 TimeInForce::Gtc,
623 OrderStatus::Filled,
624 Quantity::from("100"),
625 Quantity::from("100"), UnixNanos::from(1_000_000_000),
627 UnixNanos::from(2_000_000_000),
628 UnixNanos::from(3_000_000_000),
629 None,
630 );
631
632 assert_eq!(accepted_report.order_status, OrderStatus::Accepted);
633 assert_eq!(filled_report.order_status, OrderStatus::Filled);
634 assert_ne!(accepted_report, filled_report);
635 }
636
637 #[rstest]
638 fn test_order_status_report_with_optional_fields() {
639 let mut report = test_order_status_report();
640
641 assert_eq!(report.price, None);
643 assert_eq!(report.avg_px, None);
644 assert!(!report.post_only);
645 assert!(!report.reduce_only);
646
647 report = report
649 .with_price(Price::from("1.00000"))
650 .with_avg_px(1.00001)
651 .with_post_only(true)
652 .with_reduce_only(true);
653
654 assert_eq!(report.price, Some(Price::from("1.00000")));
655 assert_eq!(report.avg_px, Some(1.00001));
656 assert!(report.post_only);
657 assert!(report.reduce_only);
658 }
659
660 #[rstest]
661 fn test_order_status_report_partial_fill() {
662 let partial_fill_report = OrderStatusReport::new(
663 AccountId::from("SIM-001"),
664 InstrumentId::from("AUDUSD.SIM"),
665 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
666 VenueOrderId::from("1"),
667 OrderSide::Buy,
668 OrderType::Limit,
669 TimeInForce::Gtc,
670 OrderStatus::PartiallyFilled,
671 Quantity::from("100"),
672 Quantity::from("30"), UnixNanos::from(1_000_000_000),
674 UnixNanos::from(2_000_000_000),
675 UnixNanos::from(3_000_000_000),
676 None,
677 );
678
679 assert_eq!(partial_fill_report.quantity, Quantity::from("100"));
680 assert_eq!(partial_fill_report.filled_qty, Quantity::from("30"));
681 assert_eq!(
682 partial_fill_report.order_status,
683 OrderStatus::PartiallyFilled
684 );
685 }
686
687 #[rstest]
688 fn test_order_status_report_with_all_timestamp_fields() {
689 let report = OrderStatusReport::new(
690 AccountId::from("SIM-001"),
691 InstrumentId::from("AUDUSD.SIM"),
692 None,
693 VenueOrderId::from("1"),
694 OrderSide::Buy,
695 OrderType::StopLimit,
696 TimeInForce::Gtc,
697 OrderStatus::Triggered,
698 Quantity::from("100"),
699 Quantity::from("0"),
700 UnixNanos::from(1_000_000_000), UnixNanos::from(2_000_000_000), UnixNanos::from(3_000_000_000), None,
704 )
705 .with_ts_triggered(UnixNanos::from(1_500_000_000));
706
707 assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
708 assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
709 assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
710 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
711 }
712}