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 orders::Order,
29 types::{Price, Quantity},
30};
31
32#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
34#[serde(tag = "type")]
35#[cfg_attr(
36 feature = "python",
37 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
38)]
39pub struct OrderStatusReport {
40 pub account_id: AccountId,
42 pub instrument_id: InstrumentId,
44 pub client_order_id: Option<ClientOrderId>,
46 pub venue_order_id: VenueOrderId,
48 pub order_side: OrderSide,
50 pub order_type: OrderType,
52 pub time_in_force: TimeInForce,
54 pub order_status: OrderStatus,
56 pub quantity: Quantity,
58 pub filled_qty: Quantity,
60 pub report_id: UUID4,
62 pub ts_accepted: UnixNanos,
64 pub ts_last: UnixNanos,
66 pub ts_init: UnixNanos,
68 pub order_list_id: Option<OrderListId>,
70 pub venue_position_id: Option<PositionId>,
72 pub linked_order_ids: Option<Vec<ClientOrderId>>,
74 pub parent_order_id: Option<ClientOrderId>,
76 pub contingency_type: ContingencyType,
78 pub expire_time: Option<UnixNanos>,
80 pub price: Option<Price>,
82 pub trigger_price: Option<Price>,
84 pub trigger_type: Option<TriggerType>,
86 pub limit_offset: Option<Decimal>,
88 pub trailing_offset: Option<Decimal>,
90 pub trailing_offset_type: TrailingOffsetType,
92 pub avg_px: Option<Decimal>,
94 pub display_qty: Option<Quantity>,
96 pub post_only: bool,
98 pub reduce_only: bool,
100 pub cancel_reason: Option<String>,
102 pub ts_triggered: Option<UnixNanos>,
104}
105
106impl OrderStatusReport {
107 #[allow(clippy::too_many_arguments)]
109 #[must_use]
110 pub fn new(
111 account_id: AccountId,
112 instrument_id: InstrumentId,
113 client_order_id: Option<ClientOrderId>,
114 venue_order_id: VenueOrderId,
115 order_side: OrderSide,
116 order_type: OrderType,
117 time_in_force: TimeInForce,
118 order_status: OrderStatus,
119 quantity: Quantity,
120 filled_qty: Quantity,
121 ts_accepted: UnixNanos,
122 ts_last: UnixNanos,
123 ts_init: UnixNanos,
124 report_id: Option<UUID4>,
125 ) -> Self {
126 Self {
127 account_id,
128 instrument_id,
129 client_order_id,
130 venue_order_id,
131 order_side,
132 order_type,
133 time_in_force,
134 order_status,
135 quantity,
136 filled_qty,
137 report_id: report_id.unwrap_or_default(),
138 ts_accepted,
139 ts_last,
140 ts_init,
141 order_list_id: None,
142 venue_position_id: None,
143 linked_order_ids: None,
144 parent_order_id: None,
145 contingency_type: ContingencyType::default(),
146 expire_time: None,
147 price: None,
148 trigger_price: None,
149 trigger_type: None,
150 limit_offset: None,
151 trailing_offset: None,
152 trailing_offset_type: TrailingOffsetType::default(),
153 avg_px: None,
154 display_qty: None,
155 post_only: false,
156 reduce_only: false,
157 cancel_reason: None,
158 ts_triggered: None,
159 }
160 }
161
162 #[must_use]
164 pub const fn with_client_order_id(mut self, client_order_id: ClientOrderId) -> Self {
165 self.client_order_id = Some(client_order_id);
166 self
167 }
168
169 #[must_use]
171 pub const fn with_order_list_id(mut self, order_list_id: OrderListId) -> Self {
172 self.order_list_id = Some(order_list_id);
173 self
174 }
175
176 #[must_use]
178 pub fn with_linked_order_ids(
179 mut self,
180 linked_order_ids: impl IntoIterator<Item = ClientOrderId>,
181 ) -> Self {
182 self.linked_order_ids = Some(linked_order_ids.into_iter().collect());
183 self
184 }
185
186 #[must_use]
188 pub const fn with_parent_order_id(mut self, parent_order_id: ClientOrderId) -> Self {
189 self.parent_order_id = Some(parent_order_id);
190 self
191 }
192
193 #[must_use]
195 pub const fn with_venue_position_id(mut self, venue_position_id: PositionId) -> Self {
196 self.venue_position_id = Some(venue_position_id);
197 self
198 }
199
200 #[must_use]
202 pub const fn with_price(mut self, price: Price) -> Self {
203 self.price = Some(price);
204 self
205 }
206
207 pub fn with_avg_px(mut self, avg_px: f64) -> anyhow::Result<Self> {
213 if !avg_px.is_finite() {
214 anyhow::bail!(
215 "avg_px must be finite, was: {} (is_nan: {}, is_infinite: {})",
216 avg_px,
217 avg_px.is_nan(),
218 avg_px.is_infinite()
219 );
220 }
221
222 self.avg_px = Some(Decimal::from_f64_retain(avg_px).ok_or_else(|| {
223 anyhow::anyhow!(
224 "Failed to convert avg_px to Decimal: {avg_px} (possible overflow/underflow)"
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 #[must_use]
324 pub fn is_order_updated(&self, order: &impl Order) -> bool {
325 if order.has_price()
326 && let Some(report_price) = self.price
327 && let Some(order_price) = order.price()
328 && order_price != report_price
329 {
330 return true;
331 }
332
333 if let Some(order_trigger_price) = order.trigger_price()
334 && let Some(report_trigger_price) = self.trigger_price
335 && order_trigger_price != report_trigger_price
336 {
337 return true;
338 }
339
340 order.quantity() != self.quantity
341 }
342}
343
344impl Display for OrderStatusReport {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 write!(
347 f,
348 "OrderStatusReport(\
349 account_id={}, \
350 instrument_id={}, \
351 venue_order_id={}, \
352 order_side={}, \
353 order_type={}, \
354 time_in_force={}, \
355 order_status={}, \
356 quantity={}, \
357 filled_qty={}, \
358 report_id={}, \
359 ts_accepted={}, \
360 ts_last={}, \
361 ts_init={}, \
362 client_order_id={:?}, \
363 order_list_id={:?}, \
364 venue_position_id={:?}, \
365 linked_order_ids={:?}, \
366 parent_order_id={:?}, \
367 contingency_type={}, \
368 expire_time={:?}, \
369 price={:?}, \
370 trigger_price={:?}, \
371 trigger_type={:?}, \
372 limit_offset={:?}, \
373 trailing_offset={:?}, \
374 trailing_offset_type={}, \
375 avg_px={:?}, \
376 display_qty={:?}, \
377 post_only={}, \
378 reduce_only={}, \
379 cancel_reason={:?}, \
380 ts_triggered={:?}\
381 )",
382 self.account_id,
383 self.instrument_id,
384 self.venue_order_id,
385 self.order_side,
386 self.order_type,
387 self.time_in_force,
388 self.order_status,
389 self.quantity,
390 self.filled_qty,
391 self.report_id,
392 self.ts_accepted,
393 self.ts_last,
394 self.ts_init,
395 self.client_order_id,
396 self.order_list_id,
397 self.venue_position_id,
398 self.linked_order_ids,
399 self.parent_order_id,
400 self.contingency_type,
401 self.expire_time,
402 self.price,
403 self.trigger_price,
404 self.trigger_type,
405 self.limit_offset,
406 self.trailing_offset,
407 self.trailing_offset_type,
408 self.avg_px,
409 self.display_qty,
410 self.post_only,
411 self.reduce_only,
412 self.cancel_reason,
413 self.ts_triggered,
414 )
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use nautilus_core::UnixNanos;
421 use rstest::*;
422 use rust_decimal::Decimal;
423
424 use super::*;
425 use crate::{
426 enums::{
427 ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType,
428 TriggerType,
429 },
430 identifiers::{
431 AccountId, ClientOrderId, InstrumentId, OrderListId, PositionId, VenueOrderId,
432 },
433 orders::builder::OrderTestBuilder,
434 types::{Price, Quantity},
435 };
436
437 fn test_order_status_report() -> OrderStatusReport {
438 OrderStatusReport::new(
439 AccountId::from("SIM-001"),
440 InstrumentId::from("AUDUSD.SIM"),
441 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
442 VenueOrderId::from("1"),
443 OrderSide::Buy,
444 OrderType::Limit,
445 TimeInForce::Gtc,
446 OrderStatus::Accepted,
447 Quantity::from("100"),
448 Quantity::from("0"),
449 UnixNanos::from(1_000_000_000),
450 UnixNanos::from(2_000_000_000),
451 UnixNanos::from(3_000_000_000),
452 None,
453 )
454 }
455
456 #[rstest]
457 fn test_order_status_report_new() {
458 let report = test_order_status_report();
459
460 assert_eq!(report.account_id, AccountId::from("SIM-001"));
461 assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
462 assert_eq!(
463 report.client_order_id,
464 Some(ClientOrderId::from("O-19700101-000000-001-001-1"))
465 );
466 assert_eq!(report.venue_order_id, VenueOrderId::from("1"));
467 assert_eq!(report.order_side, OrderSide::Buy);
468 assert_eq!(report.order_type, OrderType::Limit);
469 assert_eq!(report.time_in_force, TimeInForce::Gtc);
470 assert_eq!(report.order_status, OrderStatus::Accepted);
471 assert_eq!(report.quantity, Quantity::from("100"));
472 assert_eq!(report.filled_qty, Quantity::from("0"));
473 assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
474 assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
475 assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
476
477 assert_eq!(report.order_list_id, None);
479 assert_eq!(report.venue_position_id, None);
480 assert_eq!(report.linked_order_ids, None);
481 assert_eq!(report.parent_order_id, None);
482 assert_eq!(report.contingency_type, ContingencyType::default());
483 assert_eq!(report.expire_time, None);
484 assert_eq!(report.price, None);
485 assert_eq!(report.trigger_price, None);
486 assert_eq!(report.trigger_type, None);
487 assert_eq!(report.limit_offset, None);
488 assert_eq!(report.trailing_offset, None);
489 assert_eq!(report.trailing_offset_type, TrailingOffsetType::default());
490 assert_eq!(report.avg_px, None);
491 assert_eq!(report.display_qty, None);
492 assert!(!report.post_only);
493 assert!(!report.reduce_only);
494 assert_eq!(report.cancel_reason, None);
495 assert_eq!(report.ts_triggered, None);
496 }
497
498 #[rstest]
499 fn test_order_status_report_with_generated_report_id() {
500 let report = OrderStatusReport::new(
501 AccountId::from("SIM-001"),
502 InstrumentId::from("AUDUSD.SIM"),
503 None,
504 VenueOrderId::from("1"),
505 OrderSide::Buy,
506 OrderType::Market,
507 TimeInForce::Ioc,
508 OrderStatus::Filled,
509 Quantity::from("100"),
510 Quantity::from("100"),
511 UnixNanos::from(1_000_000_000),
512 UnixNanos::from(2_000_000_000),
513 UnixNanos::from(3_000_000_000),
514 None, );
516
517 assert_ne!(
519 report.report_id.to_string(),
520 "00000000-0000-0000-0000-000000000000"
521 );
522 }
523
524 #[rstest]
525 #[allow(clippy::panic_in_result_fn)]
526 fn test_order_status_report_builder_methods() -> anyhow::Result<()> {
527 let report = test_order_status_report()
528 .with_client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
529 .with_order_list_id(OrderListId::from("OL-001"))
530 .with_venue_position_id(PositionId::from("P-001"))
531 .with_parent_order_id(ClientOrderId::from("O-PARENT"))
532 .with_price(Price::from("1.00000"))
533 .with_avg_px(1.00001)?
534 .with_trigger_price(Price::from("0.99000"))
535 .with_trigger_type(TriggerType::Default)
536 .with_limit_offset(Decimal::from_f64_retain(0.0001).unwrap())
537 .with_trailing_offset(Decimal::from_f64_retain(0.0002).unwrap())
538 .with_trailing_offset_type(TrailingOffsetType::BasisPoints)
539 .with_display_qty(Quantity::from("50"))
540 .with_expire_time(UnixNanos::from(4_000_000_000))
541 .with_post_only(true)
542 .with_reduce_only(true)
543 .with_cancel_reason("User requested".to_string())
544 .with_ts_triggered(UnixNanos::from(1_500_000_000))
545 .with_contingency_type(ContingencyType::Oco);
546
547 assert_eq!(
548 report.client_order_id,
549 Some(ClientOrderId::from("O-19700101-000000-001-001-2"))
550 );
551 assert_eq!(report.order_list_id, Some(OrderListId::from("OL-001")));
552 assert_eq!(report.venue_position_id, Some(PositionId::from("P-001")));
553 assert_eq!(
554 report.parent_order_id,
555 Some(ClientOrderId::from("O-PARENT"))
556 );
557 assert_eq!(report.price, Some(Price::from("1.00000")));
558 assert_eq!(
559 report.avg_px,
560 Some(Decimal::from_f64_retain(1.00001).unwrap())
561 );
562 assert_eq!(report.trigger_price, Some(Price::from("0.99000")));
563 assert_eq!(report.trigger_type, Some(TriggerType::Default));
564 assert_eq!(
565 report.limit_offset,
566 Some(Decimal::from_f64_retain(0.0001).unwrap())
567 );
568 assert_eq!(
569 report.trailing_offset,
570 Some(Decimal::from_f64_retain(0.0002).unwrap())
571 );
572 assert_eq!(report.trailing_offset_type, TrailingOffsetType::BasisPoints);
573 assert_eq!(report.display_qty, Some(Quantity::from("50")));
574 assert_eq!(report.expire_time, Some(UnixNanos::from(4_000_000_000)));
575 assert!(report.post_only);
576 assert!(report.reduce_only);
577 assert_eq!(report.cancel_reason, Some("User requested".to_string()));
578 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
579 assert_eq!(report.contingency_type, ContingencyType::Oco);
580 Ok(())
581 }
582
583 #[rstest]
584 fn test_display() {
585 let report = test_order_status_report();
586 let display_str = format!("{report}");
587
588 assert!(display_str.contains("OrderStatusReport"));
589 assert!(display_str.contains("SIM-001"));
590 assert!(display_str.contains("AUDUSD.SIM"));
591 assert!(display_str.contains("BUY"));
592 assert!(display_str.contains("LIMIT"));
593 assert!(display_str.contains("GTC"));
594 assert!(display_str.contains("ACCEPTED"));
595 assert!(display_str.contains("100"));
596 }
597
598 #[rstest]
599 fn test_clone_and_equality() {
600 let report1 = test_order_status_report();
601 let report2 = report1.clone();
602
603 assert_eq!(report1, report2);
604 }
605
606 #[rstest]
607 fn test_serialization_roundtrip() {
608 let original = test_order_status_report();
609
610 let json = serde_json::to_string(&original).unwrap();
612 let deserialized: OrderStatusReport = serde_json::from_str(&json).unwrap();
613 assert_eq!(original, deserialized);
614 }
615
616 #[rstest]
617 fn test_order_status_report_different_order_types() {
618 let market_report = OrderStatusReport::new(
619 AccountId::from("SIM-001"),
620 InstrumentId::from("AUDUSD.SIM"),
621 None,
622 VenueOrderId::from("1"),
623 OrderSide::Buy,
624 OrderType::Market,
625 TimeInForce::Ioc,
626 OrderStatus::Filled,
627 Quantity::from("100"),
628 Quantity::from("100"),
629 UnixNanos::from(1_000_000_000),
630 UnixNanos::from(2_000_000_000),
631 UnixNanos::from(3_000_000_000),
632 None,
633 );
634
635 let stop_report = OrderStatusReport::new(
636 AccountId::from("SIM-001"),
637 InstrumentId::from("AUDUSD.SIM"),
638 None,
639 VenueOrderId::from("2"),
640 OrderSide::Sell,
641 OrderType::StopMarket,
642 TimeInForce::Gtc,
643 OrderStatus::Accepted,
644 Quantity::from("50"),
645 Quantity::from("0"),
646 UnixNanos::from(1_000_000_000),
647 UnixNanos::from(2_000_000_000),
648 UnixNanos::from(3_000_000_000),
649 None,
650 );
651
652 assert_eq!(market_report.order_type, OrderType::Market);
653 assert_eq!(stop_report.order_type, OrderType::StopMarket);
654 assert_ne!(market_report, stop_report);
655 }
656
657 #[rstest]
658 fn test_order_status_report_different_statuses() {
659 let accepted_report = test_order_status_report();
660
661 let filled_report = OrderStatusReport::new(
662 AccountId::from("SIM-001"),
663 InstrumentId::from("AUDUSD.SIM"),
664 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
665 VenueOrderId::from("1"),
666 OrderSide::Buy,
667 OrderType::Limit,
668 TimeInForce::Gtc,
669 OrderStatus::Filled,
670 Quantity::from("100"),
671 Quantity::from("100"), UnixNanos::from(1_000_000_000),
673 UnixNanos::from(2_000_000_000),
674 UnixNanos::from(3_000_000_000),
675 None,
676 );
677
678 assert_eq!(accepted_report.order_status, OrderStatus::Accepted);
679 assert_eq!(filled_report.order_status, OrderStatus::Filled);
680 assert_ne!(accepted_report, filled_report);
681 }
682
683 #[rstest]
684 #[allow(clippy::panic_in_result_fn)]
685 fn test_order_status_report_with_optional_fields() -> anyhow::Result<()> {
686 let mut report = test_order_status_report();
687
688 assert_eq!(report.price, None);
690 assert_eq!(report.avg_px, None);
691 assert!(!report.post_only);
692 assert!(!report.reduce_only);
693
694 report = report
696 .with_price(Price::from("1.00000"))
697 .with_avg_px(1.00001)?
698 .with_post_only(true)
699 .with_reduce_only(true);
700
701 assert_eq!(report.price, Some(Price::from("1.00000")));
702 assert_eq!(
703 report.avg_px,
704 Some(Decimal::from_f64_retain(1.00001).unwrap())
705 );
706 assert!(report.post_only);
707 assert!(report.reduce_only);
708 Ok(())
709 }
710
711 #[rstest]
712 fn test_order_status_report_partial_fill() {
713 let partial_fill_report = OrderStatusReport::new(
714 AccountId::from("SIM-001"),
715 InstrumentId::from("AUDUSD.SIM"),
716 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
717 VenueOrderId::from("1"),
718 OrderSide::Buy,
719 OrderType::Limit,
720 TimeInForce::Gtc,
721 OrderStatus::PartiallyFilled,
722 Quantity::from("100"),
723 Quantity::from("30"), UnixNanos::from(1_000_000_000),
725 UnixNanos::from(2_000_000_000),
726 UnixNanos::from(3_000_000_000),
727 None,
728 );
729
730 assert_eq!(partial_fill_report.quantity, Quantity::from("100"));
731 assert_eq!(partial_fill_report.filled_qty, Quantity::from("30"));
732 assert_eq!(
733 partial_fill_report.order_status,
734 OrderStatus::PartiallyFilled
735 );
736 }
737
738 #[rstest]
739 fn test_order_status_report_with_all_timestamp_fields() {
740 let report = OrderStatusReport::new(
741 AccountId::from("SIM-001"),
742 InstrumentId::from("AUDUSD.SIM"),
743 None,
744 VenueOrderId::from("1"),
745 OrderSide::Buy,
746 OrderType::StopLimit,
747 TimeInForce::Gtc,
748 OrderStatus::Triggered,
749 Quantity::from("100"),
750 Quantity::from("0"),
751 UnixNanos::from(1_000_000_000), UnixNanos::from(2_000_000_000), UnixNanos::from(3_000_000_000), None,
755 )
756 .with_ts_triggered(UnixNanos::from(1_500_000_000));
757
758 assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
759 assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
760 assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
761 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
762 }
763
764 #[rstest]
765 fn test_is_order_updated_returns_true_when_price_differs() {
766 let order = OrderTestBuilder::new(OrderType::Limit)
767 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
768 .quantity(Quantity::from(100))
769 .price(Price::from("1.00000"))
770 .build();
771
772 let report = OrderStatusReport::new(
773 AccountId::from("SIM-001"),
774 InstrumentId::from("AUDUSD.SIM"),
775 None,
776 VenueOrderId::from("1"),
777 OrderSide::Buy,
778 OrderType::Limit,
779 TimeInForce::Gtc,
780 OrderStatus::Accepted,
781 Quantity::from("100"),
782 Quantity::from("0"),
783 UnixNanos::from(1_000_000_000),
784 UnixNanos::from(2_000_000_000),
785 UnixNanos::from(3_000_000_000),
786 None,
787 )
788 .with_price(Price::from("1.00100")); assert!(report.is_order_updated(&order));
791 }
792
793 #[rstest]
794 fn test_is_order_updated_returns_true_when_trigger_price_differs() {
795 let order = OrderTestBuilder::new(OrderType::StopMarket)
796 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
797 .quantity(Quantity::from(100))
798 .trigger_price(Price::from("0.99000"))
799 .build();
800
801 let report = OrderStatusReport::new(
802 AccountId::from("SIM-001"),
803 InstrumentId::from("AUDUSD.SIM"),
804 None,
805 VenueOrderId::from("1"),
806 OrderSide::Buy,
807 OrderType::StopMarket,
808 TimeInForce::Gtc,
809 OrderStatus::Accepted,
810 Quantity::from("100"),
811 Quantity::from("0"),
812 UnixNanos::from(1_000_000_000),
813 UnixNanos::from(2_000_000_000),
814 UnixNanos::from(3_000_000_000),
815 None,
816 )
817 .with_trigger_price(Price::from("0.99100")); assert!(report.is_order_updated(&order));
820 }
821
822 #[rstest]
823 fn test_is_order_updated_returns_true_when_quantity_differs() {
824 let order = OrderTestBuilder::new(OrderType::Limit)
825 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
826 .quantity(Quantity::from(100))
827 .price(Price::from("1.00000"))
828 .build();
829
830 let report = OrderStatusReport::new(
831 AccountId::from("SIM-001"),
832 InstrumentId::from("AUDUSD.SIM"),
833 None,
834 VenueOrderId::from("1"),
835 OrderSide::Buy,
836 OrderType::Limit,
837 TimeInForce::Gtc,
838 OrderStatus::Accepted,
839 Quantity::from("200"), Quantity::from("0"),
841 UnixNanos::from(1_000_000_000),
842 UnixNanos::from(2_000_000_000),
843 UnixNanos::from(3_000_000_000),
844 None,
845 )
846 .with_price(Price::from("1.00000"));
847
848 assert!(report.is_order_updated(&order));
849 }
850
851 #[rstest]
852 fn test_is_order_updated_returns_false_when_all_match() {
853 let order = OrderTestBuilder::new(OrderType::Limit)
854 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
855 .quantity(Quantity::from(100))
856 .price(Price::from("1.00000"))
857 .build();
858
859 let report = OrderStatusReport::new(
860 AccountId::from("SIM-001"),
861 InstrumentId::from("AUDUSD.SIM"),
862 None,
863 VenueOrderId::from("1"),
864 OrderSide::Buy,
865 OrderType::Limit,
866 TimeInForce::Gtc,
867 OrderStatus::Accepted,
868 Quantity::from("100"), Quantity::from("0"),
870 UnixNanos::from(1_000_000_000),
871 UnixNanos::from(2_000_000_000),
872 UnixNanos::from(3_000_000_000),
873 None,
874 )
875 .with_price(Price::from("1.00000")); assert!(!report.is_order_updated(&order));
878 }
879
880 #[rstest]
881 fn test_is_order_updated_returns_false_when_order_has_no_price() {
882 let order = OrderTestBuilder::new(OrderType::Market)
884 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
885 .quantity(Quantity::from(100))
886 .build();
887
888 let report = OrderStatusReport::new(
889 AccountId::from("SIM-001"),
890 InstrumentId::from("AUDUSD.SIM"),
891 None,
892 VenueOrderId::from("1"),
893 OrderSide::Buy,
894 OrderType::Market,
895 TimeInForce::Ioc,
896 OrderStatus::Accepted,
897 Quantity::from("100"), Quantity::from("0"),
899 UnixNanos::from(1_000_000_000),
900 UnixNanos::from(2_000_000_000),
901 UnixNanos::from(3_000_000_000),
902 None,
903 )
904 .with_price(Price::from("1.00000")); assert!(!report.is_order_updated(&order));
907 }
908
909 #[rstest]
910 fn test_is_order_updated_stop_limit_order_with_both_prices() {
911 let order = OrderTestBuilder::new(OrderType::StopLimit)
912 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
913 .quantity(Quantity::from(100))
914 .price(Price::from("1.00000"))
915 .trigger_price(Price::from("0.99000"))
916 .build();
917
918 let report_same = OrderStatusReport::new(
920 AccountId::from("SIM-001"),
921 InstrumentId::from("AUDUSD.SIM"),
922 None,
923 VenueOrderId::from("1"),
924 OrderSide::Buy,
925 OrderType::StopLimit,
926 TimeInForce::Gtc,
927 OrderStatus::Accepted,
928 Quantity::from("100"),
929 Quantity::from("0"),
930 UnixNanos::from(1_000_000_000),
931 UnixNanos::from(2_000_000_000),
932 UnixNanos::from(3_000_000_000),
933 None,
934 )
935 .with_price(Price::from("1.00000"))
936 .with_trigger_price(Price::from("0.99000"));
937
938 assert!(!report_same.is_order_updated(&order));
939
940 let report_diff_price = OrderStatusReport::new(
942 AccountId::from("SIM-001"),
943 InstrumentId::from("AUDUSD.SIM"),
944 None,
945 VenueOrderId::from("1"),
946 OrderSide::Buy,
947 OrderType::StopLimit,
948 TimeInForce::Gtc,
949 OrderStatus::Accepted,
950 Quantity::from("100"),
951 Quantity::from("0"),
952 UnixNanos::from(1_000_000_000),
953 UnixNanos::from(2_000_000_000),
954 UnixNanos::from(3_000_000_000),
955 None,
956 )
957 .with_price(Price::from("1.00100")) .with_trigger_price(Price::from("0.99000"));
959
960 assert!(report_diff_price.is_order_updated(&order));
961
962 let report_diff_trigger = OrderStatusReport::new(
964 AccountId::from("SIM-001"),
965 InstrumentId::from("AUDUSD.SIM"),
966 None,
967 VenueOrderId::from("1"),
968 OrderSide::Buy,
969 OrderType::StopLimit,
970 TimeInForce::Gtc,
971 OrderStatus::Accepted,
972 Quantity::from("100"),
973 Quantity::from("0"),
974 UnixNanos::from(1_000_000_000),
975 UnixNanos::from(2_000_000_000),
976 UnixNanos::from(3_000_000_000),
977 None,
978 )
979 .with_price(Price::from("1.00000"))
980 .with_trigger_price(Price::from("0.99100")); assert!(report_diff_trigger.is_order_updated(&order));
983 }
984}