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 contingency_type: ContingencyType,
73 pub expire_time: Option<UnixNanos>,
75 pub price: Option<Price>,
77 pub trigger_price: Option<Price>,
79 pub trigger_type: Option<TriggerType>,
81 pub limit_offset: Option<Decimal>,
83 pub trailing_offset: Option<Decimal>,
85 pub trailing_offset_type: TrailingOffsetType,
87 pub avg_px: Option<f64>,
89 pub display_qty: Option<Quantity>,
91 pub post_only: bool,
93 pub reduce_only: bool,
95 pub cancel_reason: Option<String>,
97 pub ts_triggered: Option<UnixNanos>,
99}
100
101impl OrderStatusReport {
102 #[allow(clippy::too_many_arguments)]
104 #[must_use]
105 pub fn new(
106 account_id: AccountId,
107 instrument_id: InstrumentId,
108 client_order_id: Option<ClientOrderId>,
109 venue_order_id: VenueOrderId,
110 order_side: OrderSide,
111 order_type: OrderType,
112 time_in_force: TimeInForce,
113 order_status: OrderStatus,
114 quantity: Quantity,
115 filled_qty: Quantity,
116 ts_accepted: UnixNanos,
117 ts_last: UnixNanos,
118 ts_init: UnixNanos,
119 report_id: Option<UUID4>,
120 ) -> Self {
121 Self {
122 account_id,
123 instrument_id,
124 client_order_id,
125 venue_order_id,
126 order_side,
127 order_type,
128 time_in_force,
129 order_status,
130 quantity,
131 filled_qty,
132 report_id: report_id.unwrap_or_default(),
133 ts_accepted,
134 ts_last,
135 ts_init,
136 order_list_id: None,
137 venue_position_id: None,
138 contingency_type: ContingencyType::default(),
139 expire_time: None,
140 price: None,
141 trigger_price: None,
142 trigger_type: None,
143 limit_offset: None,
144 trailing_offset: None,
145 trailing_offset_type: TrailingOffsetType::default(),
146 avg_px: None,
147 display_qty: None,
148 post_only: false,
149 reduce_only: false,
150 cancel_reason: None,
151 ts_triggered: None,
152 }
153 }
154
155 #[must_use]
157 pub const fn with_client_order_id(mut self, client_order_id: ClientOrderId) -> Self {
158 self.client_order_id = Some(client_order_id);
159 self
160 }
161
162 #[must_use]
164 pub const fn with_order_list_id(mut self, order_list_id: OrderListId) -> Self {
165 self.order_list_id = Some(order_list_id);
166 self
167 }
168
169 #[must_use]
171 pub const fn with_venue_position_id(mut self, venue_position_id: PositionId) -> Self {
172 self.venue_position_id = Some(venue_position_id);
173 self
174 }
175
176 #[must_use]
178 pub const fn with_price(mut self, price: Price) -> Self {
179 self.price = Some(price);
180 self
181 }
182
183 #[must_use]
185 pub const fn with_avg_px(mut self, avg_px: f64) -> Self {
186 self.avg_px = Some(avg_px);
187 self
188 }
189
190 #[must_use]
192 pub const fn with_trigger_price(mut self, trigger_price: Price) -> Self {
193 self.trigger_price = Some(trigger_price);
194 self
195 }
196
197 #[must_use]
199 pub const fn with_trigger_type(mut self, trigger_type: TriggerType) -> Self {
200 self.trigger_type = Some(trigger_type);
201 self
202 }
203
204 #[must_use]
206 pub const fn with_limit_offset(mut self, limit_offset: Decimal) -> Self {
207 self.limit_offset = Some(limit_offset);
208 self
209 }
210
211 #[must_use]
213 pub const fn with_trailing_offset(mut self, trailing_offset: Decimal) -> Self {
214 self.trailing_offset = Some(trailing_offset);
215 self
216 }
217
218 #[must_use]
220 pub const fn with_trailing_offset_type(
221 mut self,
222 trailing_offset_type: TrailingOffsetType,
223 ) -> Self {
224 self.trailing_offset_type = trailing_offset_type;
225 self
226 }
227
228 #[must_use]
230 pub const fn with_display_qty(mut self, display_qty: Quantity) -> Self {
231 self.display_qty = Some(display_qty);
232 self
233 }
234
235 #[must_use]
237 pub const fn with_expire_time(mut self, expire_time: UnixNanos) -> Self {
238 self.expire_time = Some(expire_time);
239 self
240 }
241
242 #[must_use]
244 pub const fn with_post_only(mut self, post_only: bool) -> Self {
245 self.post_only = post_only;
246 self
247 }
248
249 #[must_use]
251 pub const fn with_reduce_only(mut self, reduce_only: bool) -> Self {
252 self.reduce_only = reduce_only;
253 self
254 }
255
256 #[must_use]
258 pub fn with_cancel_reason(mut self, cancel_reason: String) -> Self {
259 self.cancel_reason = Some(cancel_reason);
260 self
261 }
262
263 #[must_use]
265 pub const fn with_ts_triggered(mut self, ts_triggered: UnixNanos) -> Self {
266 self.ts_triggered = Some(ts_triggered);
267 self
268 }
269
270 #[must_use]
272 pub const fn with_contingency_type(mut self, contingency_type: ContingencyType) -> Self {
273 self.contingency_type = contingency_type;
274 self
275 }
276}
277
278impl Display for OrderStatusReport {
279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280 write!(
281 f,
282 "OrderStatusReport(\
283 account_id={}, \
284 instrument_id={}, \
285 venue_order_id={}, \
286 order_side={}, \
287 order_type={}, \
288 time_in_force={}, \
289 order_status={}, \
290 quantity={}, \
291 filled_qty={}, \
292 report_id={}, \
293 ts_accepted={}, \
294 ts_last={}, \
295 ts_init={}, \
296 client_order_id={:?}, \
297 order_list_id={:?}, \
298 venue_position_id={:?}, \
299 contingency_type={}, \
300 expire_time={:?}, \
301 price={:?}, \
302 trigger_price={:?}, \
303 trigger_type={:?}, \
304 limit_offset={:?}, \
305 trailing_offset={:?}, \
306 trailing_offset_type={}, \
307 avg_px={:?}, \
308 display_qty={:?}, \
309 post_only={}, \
310 reduce_only={}, \
311 cancel_reason={:?}, \
312 ts_triggered={:?}\
313 )",
314 self.account_id,
315 self.instrument_id,
316 self.venue_order_id,
317 self.order_side,
318 self.order_type,
319 self.time_in_force,
320 self.order_status,
321 self.quantity,
322 self.filled_qty,
323 self.report_id,
324 self.ts_accepted,
325 self.ts_last,
326 self.ts_init,
327 self.client_order_id,
328 self.order_list_id,
329 self.venue_position_id,
330 self.contingency_type,
331 self.expire_time,
332 self.price,
333 self.trigger_price,
334 self.trigger_type,
335 self.limit_offset,
336 self.trailing_offset,
337 self.trailing_offset_type,
338 self.avg_px,
339 self.display_qty,
340 self.post_only,
341 self.reduce_only,
342 self.cancel_reason,
343 self.ts_triggered,
344 )
345 }
346}
347
348#[cfg(test)]
352mod tests {
353 use nautilus_core::UnixNanos;
354 use rstest::*;
355 use rust_decimal::Decimal;
356
357 use super::*;
358 use crate::{
359 enums::{
360 ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType,
361 TriggerType,
362 },
363 identifiers::{
364 AccountId, ClientOrderId, InstrumentId, OrderListId, PositionId, VenueOrderId,
365 },
366 types::{Price, Quantity},
367 };
368
369 fn test_order_status_report() -> OrderStatusReport {
370 OrderStatusReport::new(
371 AccountId::from("SIM-001"),
372 InstrumentId::from("AUDUSD.SIM"),
373 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
374 VenueOrderId::from("1"),
375 OrderSide::Buy,
376 OrderType::Limit,
377 TimeInForce::Gtc,
378 OrderStatus::Accepted,
379 Quantity::from("100"),
380 Quantity::from("0"),
381 UnixNanos::from(1_000_000_000),
382 UnixNanos::from(2_000_000_000),
383 UnixNanos::from(3_000_000_000),
384 None,
385 )
386 }
387
388 #[rstest]
389 fn test_order_status_report_new() {
390 let report = test_order_status_report();
391
392 assert_eq!(report.account_id, AccountId::from("SIM-001"));
393 assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
394 assert_eq!(
395 report.client_order_id,
396 Some(ClientOrderId::from("O-19700101-000000-001-001-1"))
397 );
398 assert_eq!(report.venue_order_id, VenueOrderId::from("1"));
399 assert_eq!(report.order_side, OrderSide::Buy);
400 assert_eq!(report.order_type, OrderType::Limit);
401 assert_eq!(report.time_in_force, TimeInForce::Gtc);
402 assert_eq!(report.order_status, OrderStatus::Accepted);
403 assert_eq!(report.quantity, Quantity::from("100"));
404 assert_eq!(report.filled_qty, Quantity::from("0"));
405 assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
406 assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
407 assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
408
409 assert_eq!(report.order_list_id, None);
411 assert_eq!(report.venue_position_id, None);
412 assert_eq!(report.contingency_type, ContingencyType::default());
413 assert_eq!(report.expire_time, None);
414 assert_eq!(report.price, None);
415 assert_eq!(report.trigger_price, None);
416 assert_eq!(report.trigger_type, None);
417 assert_eq!(report.limit_offset, None);
418 assert_eq!(report.trailing_offset, None);
419 assert_eq!(report.trailing_offset_type, TrailingOffsetType::default());
420 assert_eq!(report.avg_px, None);
421 assert_eq!(report.display_qty, None);
422 assert!(!report.post_only);
423 assert!(!report.reduce_only);
424 assert_eq!(report.cancel_reason, None);
425 assert_eq!(report.ts_triggered, None);
426 }
427
428 #[rstest]
429 fn test_order_status_report_with_generated_report_id() {
430 let report = OrderStatusReport::new(
431 AccountId::from("SIM-001"),
432 InstrumentId::from("AUDUSD.SIM"),
433 None,
434 VenueOrderId::from("1"),
435 OrderSide::Buy,
436 OrderType::Market,
437 TimeInForce::Ioc,
438 OrderStatus::Filled,
439 Quantity::from("100"),
440 Quantity::from("100"),
441 UnixNanos::from(1_000_000_000),
442 UnixNanos::from(2_000_000_000),
443 UnixNanos::from(3_000_000_000),
444 None, );
446
447 assert_ne!(
449 report.report_id.to_string(),
450 "00000000-0000-0000-0000-000000000000"
451 );
452 }
453
454 #[rstest]
455 fn test_order_status_report_builder_methods() {
456 let report = test_order_status_report()
457 .with_client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
458 .with_order_list_id(OrderListId::from("OL-001"))
459 .with_venue_position_id(PositionId::from("P-001"))
460 .with_price(Price::from("1.00000"))
461 .with_avg_px(1.00001)
462 .with_trigger_price(Price::from("0.99000"))
463 .with_trigger_type(TriggerType::Default)
464 .with_limit_offset(Decimal::from_f64_retain(0.0001).unwrap())
465 .with_trailing_offset(Decimal::from_f64_retain(0.0002).unwrap())
466 .with_trailing_offset_type(TrailingOffsetType::BasisPoints)
467 .with_display_qty(Quantity::from("50"))
468 .with_expire_time(UnixNanos::from(4_000_000_000))
469 .with_post_only(true)
470 .with_reduce_only(true)
471 .with_cancel_reason("User requested".to_string())
472 .with_ts_triggered(UnixNanos::from(1_500_000_000))
473 .with_contingency_type(ContingencyType::Oco);
474
475 assert_eq!(
476 report.client_order_id,
477 Some(ClientOrderId::from("O-19700101-000000-001-001-2"))
478 );
479 assert_eq!(report.order_list_id, Some(OrderListId::from("OL-001")));
480 assert_eq!(report.venue_position_id, Some(PositionId::from("P-001")));
481 assert_eq!(report.price, Some(Price::from("1.00000")));
482 assert_eq!(report.avg_px, Some(1.00001));
483 assert_eq!(report.trigger_price, Some(Price::from("0.99000")));
484 assert_eq!(report.trigger_type, Some(TriggerType::Default));
485 assert_eq!(
486 report.limit_offset,
487 Some(Decimal::from_f64_retain(0.0001).unwrap())
488 );
489 assert_eq!(
490 report.trailing_offset,
491 Some(Decimal::from_f64_retain(0.0002).unwrap())
492 );
493 assert_eq!(report.trailing_offset_type, TrailingOffsetType::BasisPoints);
494 assert_eq!(report.display_qty, Some(Quantity::from("50")));
495 assert_eq!(report.expire_time, Some(UnixNanos::from(4_000_000_000)));
496 assert!(report.post_only);
497 assert!(report.reduce_only);
498 assert_eq!(report.cancel_reason, Some("User requested".to_string()));
499 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
500 assert_eq!(report.contingency_type, ContingencyType::Oco);
501 }
502
503 #[rstest]
504 fn test_display() {
505 let report = test_order_status_report();
506 let display_str = format!("{report}");
507
508 assert!(display_str.contains("OrderStatusReport"));
509 assert!(display_str.contains("SIM-001"));
510 assert!(display_str.contains("AUDUSD.SIM"));
511 assert!(display_str.contains("BUY"));
512 assert!(display_str.contains("LIMIT"));
513 assert!(display_str.contains("GTC"));
514 assert!(display_str.contains("ACCEPTED"));
515 assert!(display_str.contains("100"));
516 }
517
518 #[rstest]
519 fn test_clone_and_equality() {
520 let report1 = test_order_status_report();
521 let report2 = report1.clone();
522
523 assert_eq!(report1, report2);
524 }
525
526 #[rstest]
527 fn test_serialization_roundtrip() {
528 let original = test_order_status_report();
529
530 let json = serde_json::to_string(&original).unwrap();
532 let deserialized: OrderStatusReport = serde_json::from_str(&json).unwrap();
533 assert_eq!(original, deserialized);
534 }
535
536 #[rstest]
537 fn test_order_status_report_different_order_types() {
538 let market_report = OrderStatusReport::new(
539 AccountId::from("SIM-001"),
540 InstrumentId::from("AUDUSD.SIM"),
541 None,
542 VenueOrderId::from("1"),
543 OrderSide::Buy,
544 OrderType::Market,
545 TimeInForce::Ioc,
546 OrderStatus::Filled,
547 Quantity::from("100"),
548 Quantity::from("100"),
549 UnixNanos::from(1_000_000_000),
550 UnixNanos::from(2_000_000_000),
551 UnixNanos::from(3_000_000_000),
552 None,
553 );
554
555 let stop_report = OrderStatusReport::new(
556 AccountId::from("SIM-001"),
557 InstrumentId::from("AUDUSD.SIM"),
558 None,
559 VenueOrderId::from("2"),
560 OrderSide::Sell,
561 OrderType::StopMarket,
562 TimeInForce::Gtc,
563 OrderStatus::Accepted,
564 Quantity::from("50"),
565 Quantity::from("0"),
566 UnixNanos::from(1_000_000_000),
567 UnixNanos::from(2_000_000_000),
568 UnixNanos::from(3_000_000_000),
569 None,
570 );
571
572 assert_eq!(market_report.order_type, OrderType::Market);
573 assert_eq!(stop_report.order_type, OrderType::StopMarket);
574 assert_ne!(market_report, stop_report);
575 }
576
577 #[rstest]
578 fn test_order_status_report_different_statuses() {
579 let accepted_report = test_order_status_report();
580
581 let filled_report = OrderStatusReport::new(
582 AccountId::from("SIM-001"),
583 InstrumentId::from("AUDUSD.SIM"),
584 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
585 VenueOrderId::from("1"),
586 OrderSide::Buy,
587 OrderType::Limit,
588 TimeInForce::Gtc,
589 OrderStatus::Filled,
590 Quantity::from("100"),
591 Quantity::from("100"), UnixNanos::from(1_000_000_000),
593 UnixNanos::from(2_000_000_000),
594 UnixNanos::from(3_000_000_000),
595 None,
596 );
597
598 assert_eq!(accepted_report.order_status, OrderStatus::Accepted);
599 assert_eq!(filled_report.order_status, OrderStatus::Filled);
600 assert_ne!(accepted_report, filled_report);
601 }
602
603 #[rstest]
604 fn test_order_status_report_with_optional_fields() {
605 let mut report = test_order_status_report();
606
607 assert_eq!(report.price, None);
609 assert_eq!(report.avg_px, None);
610 assert!(!report.post_only);
611 assert!(!report.reduce_only);
612
613 report = report
615 .with_price(Price::from("1.00000"))
616 .with_avg_px(1.00001)
617 .with_post_only(true)
618 .with_reduce_only(true);
619
620 assert_eq!(report.price, Some(Price::from("1.00000")));
621 assert_eq!(report.avg_px, Some(1.00001));
622 assert!(report.post_only);
623 assert!(report.reduce_only);
624 }
625
626 #[rstest]
627 fn test_order_status_report_partial_fill() {
628 let partial_fill_report = OrderStatusReport::new(
629 AccountId::from("SIM-001"),
630 InstrumentId::from("AUDUSD.SIM"),
631 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
632 VenueOrderId::from("1"),
633 OrderSide::Buy,
634 OrderType::Limit,
635 TimeInForce::Gtc,
636 OrderStatus::PartiallyFilled,
637 Quantity::from("100"),
638 Quantity::from("30"), UnixNanos::from(1_000_000_000),
640 UnixNanos::from(2_000_000_000),
641 UnixNanos::from(3_000_000_000),
642 None,
643 );
644
645 assert_eq!(partial_fill_report.quantity, Quantity::from("100"));
646 assert_eq!(partial_fill_report.filled_qty, Quantity::from("30"));
647 assert_eq!(
648 partial_fill_report.order_status,
649 OrderStatus::PartiallyFilled
650 );
651 }
652
653 #[rstest]
654 fn test_order_status_report_with_all_timestamp_fields() {
655 let report = OrderStatusReport::new(
656 AccountId::from("SIM-001"),
657 InstrumentId::from("AUDUSD.SIM"),
658 None,
659 VenueOrderId::from("1"),
660 OrderSide::Buy,
661 OrderType::StopLimit,
662 TimeInForce::Gtc,
663 OrderStatus::Triggered,
664 Quantity::from("100"),
665 Quantity::from("0"),
666 UnixNanos::from(1_000_000_000), UnixNanos::from(2_000_000_000), UnixNanos::from(3_000_000_000), None,
670 )
671 .with_ts_triggered(UnixNanos::from(1_500_000_000));
672
673 assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
674 assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
675 assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
676 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
677 }
678}