1use std::{fmt::Display, str::FromStr};
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", from_py_object)
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 =
223 Some(Decimal::from_str(&avg_px.to_string()).map_err(|e| {
224 anyhow::anyhow!("Failed to convert avg_px to Decimal: {avg_px} ({e})")
225 })?);
226 Ok(self)
227 }
228
229 #[must_use]
231 pub const fn with_trigger_price(mut self, trigger_price: Price) -> Self {
232 self.trigger_price = Some(trigger_price);
233 self
234 }
235
236 #[must_use]
238 pub const fn with_trigger_type(mut self, trigger_type: TriggerType) -> Self {
239 self.trigger_type = Some(trigger_type);
240 self
241 }
242
243 #[must_use]
245 pub const fn with_limit_offset(mut self, limit_offset: Decimal) -> Self {
246 self.limit_offset = Some(limit_offset);
247 self
248 }
249
250 #[must_use]
252 pub const fn with_trailing_offset(mut self, trailing_offset: Decimal) -> Self {
253 self.trailing_offset = Some(trailing_offset);
254 self
255 }
256
257 #[must_use]
259 pub const fn with_trailing_offset_type(
260 mut self,
261 trailing_offset_type: TrailingOffsetType,
262 ) -> Self {
263 self.trailing_offset_type = trailing_offset_type;
264 self
265 }
266
267 #[must_use]
269 pub const fn with_display_qty(mut self, display_qty: Quantity) -> Self {
270 self.display_qty = Some(display_qty);
271 self
272 }
273
274 #[must_use]
276 pub const fn with_expire_time(mut self, expire_time: UnixNanos) -> Self {
277 self.expire_time = Some(expire_time);
278 self
279 }
280
281 #[must_use]
283 pub const fn with_post_only(mut self, post_only: bool) -> Self {
284 self.post_only = post_only;
285 self
286 }
287
288 #[must_use]
290 pub const fn with_reduce_only(mut self, reduce_only: bool) -> Self {
291 self.reduce_only = reduce_only;
292 self
293 }
294
295 #[must_use]
297 pub fn with_cancel_reason(mut self, cancel_reason: String) -> Self {
298 self.cancel_reason = Some(cancel_reason);
299 self
300 }
301
302 #[must_use]
304 pub const fn with_ts_triggered(mut self, ts_triggered: UnixNanos) -> Self {
305 self.ts_triggered = Some(ts_triggered);
306 self
307 }
308
309 #[must_use]
311 pub const fn with_contingency_type(mut self, contingency_type: ContingencyType) -> Self {
312 self.contingency_type = contingency_type;
313 self
314 }
315
316 #[must_use]
323 pub fn is_order_updated(&self, order: &impl Order) -> bool {
324 if order.has_price()
325 && let Some(report_price) = self.price
326 && let Some(order_price) = order.price()
327 && order_price != report_price
328 {
329 return true;
330 }
331
332 if let Some(order_trigger_price) = order.trigger_price()
333 && let Some(report_trigger_price) = self.trigger_price
334 && order_trigger_price != report_trigger_price
335 {
336 return true;
337 }
338
339 order.quantity() != self.quantity
340 }
341}
342
343impl Display for OrderStatusReport {
344 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345 write!(
346 f,
347 "OrderStatusReport(\
348 account_id={}, \
349 instrument_id={}, \
350 venue_order_id={}, \
351 order_side={}, \
352 order_type={}, \
353 time_in_force={}, \
354 order_status={}, \
355 quantity={}, \
356 filled_qty={}, \
357 report_id={}, \
358 ts_accepted={}, \
359 ts_last={}, \
360 ts_init={}, \
361 client_order_id={:?}, \
362 order_list_id={:?}, \
363 venue_position_id={:?}, \
364 linked_order_ids={:?}, \
365 parent_order_id={:?}, \
366 contingency_type={}, \
367 expire_time={:?}, \
368 price={:?}, \
369 trigger_price={:?}, \
370 trigger_type={:?}, \
371 limit_offset={:?}, \
372 trailing_offset={:?}, \
373 trailing_offset_type={}, \
374 avg_px={:?}, \
375 display_qty={:?}, \
376 post_only={}, \
377 reduce_only={}, \
378 cancel_reason={:?}, \
379 ts_triggered={:?}\
380 )",
381 self.account_id,
382 self.instrument_id,
383 self.venue_order_id,
384 self.order_side,
385 self.order_type,
386 self.time_in_force,
387 self.order_status,
388 self.quantity,
389 self.filled_qty,
390 self.report_id,
391 self.ts_accepted,
392 self.ts_last,
393 self.ts_init,
394 self.client_order_id,
395 self.order_list_id,
396 self.venue_position_id,
397 self.linked_order_ids,
398 self.parent_order_id,
399 self.contingency_type,
400 self.expire_time,
401 self.price,
402 self.trigger_price,
403 self.trigger_type,
404 self.limit_offset,
405 self.trailing_offset,
406 self.trailing_offset_type,
407 self.avg_px,
408 self.display_qty,
409 self.post_only,
410 self.reduce_only,
411 self.cancel_reason,
412 self.ts_triggered,
413 )
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use nautilus_core::UnixNanos;
420 use rstest::*;
421 use rust_decimal_macros::dec;
422
423 use super::*;
424 use crate::{
425 enums::{
426 ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType,
427 TriggerType,
428 },
429 identifiers::{
430 AccountId, ClientOrderId, InstrumentId, OrderListId, PositionId, VenueOrderId,
431 },
432 orders::builder::OrderTestBuilder,
433 types::{Price, Quantity},
434 };
435
436 fn test_order_status_report() -> OrderStatusReport {
437 OrderStatusReport::new(
438 AccountId::from("SIM-001"),
439 InstrumentId::from("AUDUSD.SIM"),
440 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
441 VenueOrderId::from("1"),
442 OrderSide::Buy,
443 OrderType::Limit,
444 TimeInForce::Gtc,
445 OrderStatus::Accepted,
446 Quantity::from("100"),
447 Quantity::from("0"),
448 UnixNanos::from(1_000_000_000),
449 UnixNanos::from(2_000_000_000),
450 UnixNanos::from(3_000_000_000),
451 None,
452 )
453 }
454
455 #[rstest]
456 fn test_order_status_report_new() {
457 let report = test_order_status_report();
458
459 assert_eq!(report.account_id, AccountId::from("SIM-001"));
460 assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
461 assert_eq!(
462 report.client_order_id,
463 Some(ClientOrderId::from("O-19700101-000000-001-001-1"))
464 );
465 assert_eq!(report.venue_order_id, VenueOrderId::from("1"));
466 assert_eq!(report.order_side, OrderSide::Buy);
467 assert_eq!(report.order_type, OrderType::Limit);
468 assert_eq!(report.time_in_force, TimeInForce::Gtc);
469 assert_eq!(report.order_status, OrderStatus::Accepted);
470 assert_eq!(report.quantity, Quantity::from("100"));
471 assert_eq!(report.filled_qty, Quantity::from("0"));
472 assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
473 assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
474 assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
475
476 assert_eq!(report.order_list_id, None);
478 assert_eq!(report.venue_position_id, None);
479 assert_eq!(report.linked_order_ids, None);
480 assert_eq!(report.parent_order_id, None);
481 assert_eq!(report.contingency_type, ContingencyType::default());
482 assert_eq!(report.expire_time, None);
483 assert_eq!(report.price, None);
484 assert_eq!(report.trigger_price, None);
485 assert_eq!(report.trigger_type, None);
486 assert_eq!(report.limit_offset, None);
487 assert_eq!(report.trailing_offset, None);
488 assert_eq!(report.trailing_offset_type, TrailingOffsetType::default());
489 assert_eq!(report.avg_px, None);
490 assert_eq!(report.display_qty, None);
491 assert!(!report.post_only);
492 assert!(!report.reduce_only);
493 assert_eq!(report.cancel_reason, None);
494 assert_eq!(report.ts_triggered, None);
495 }
496
497 #[rstest]
498 fn test_order_status_report_with_generated_report_id() {
499 let report = OrderStatusReport::new(
500 AccountId::from("SIM-001"),
501 InstrumentId::from("AUDUSD.SIM"),
502 None,
503 VenueOrderId::from("1"),
504 OrderSide::Buy,
505 OrderType::Market,
506 TimeInForce::Ioc,
507 OrderStatus::Filled,
508 Quantity::from("100"),
509 Quantity::from("100"),
510 UnixNanos::from(1_000_000_000),
511 UnixNanos::from(2_000_000_000),
512 UnixNanos::from(3_000_000_000),
513 None, );
515
516 assert_ne!(
518 report.report_id.to_string(),
519 "00000000-0000-0000-0000-000000000000"
520 );
521 }
522
523 #[rstest]
524 #[allow(clippy::panic_in_result_fn)]
525 fn test_order_status_report_builder_methods() -> anyhow::Result<()> {
526 let report = test_order_status_report()
527 .with_client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
528 .with_order_list_id(OrderListId::from("OL-001"))
529 .with_venue_position_id(PositionId::from("P-001"))
530 .with_parent_order_id(ClientOrderId::from("O-PARENT"))
531 .with_price(Price::from("1.00000"))
532 .with_avg_px(1.00001)?
533 .with_trigger_price(Price::from("0.99000"))
534 .with_trigger_type(TriggerType::Default)
535 .with_limit_offset(dec!(0.0001))
536 .with_trailing_offset(dec!(0.0002))
537 .with_trailing_offset_type(TrailingOffsetType::BasisPoints)
538 .with_display_qty(Quantity::from("50"))
539 .with_expire_time(UnixNanos::from(4_000_000_000))
540 .with_post_only(true)
541 .with_reduce_only(true)
542 .with_cancel_reason("User requested".to_string())
543 .with_ts_triggered(UnixNanos::from(1_500_000_000))
544 .with_contingency_type(ContingencyType::Oco);
545
546 assert_eq!(
547 report.client_order_id,
548 Some(ClientOrderId::from("O-19700101-000000-001-001-2"))
549 );
550 assert_eq!(report.order_list_id, Some(OrderListId::from("OL-001")));
551 assert_eq!(report.venue_position_id, Some(PositionId::from("P-001")));
552 assert_eq!(
553 report.parent_order_id,
554 Some(ClientOrderId::from("O-PARENT"))
555 );
556 assert_eq!(report.price, Some(Price::from("1.00000")));
557 assert_eq!(report.avg_px, Some(dec!(1.00001)));
558 assert_eq!(report.trigger_price, Some(Price::from("0.99000")));
559 assert_eq!(report.trigger_type, Some(TriggerType::Default));
560 assert_eq!(report.limit_offset, Some(dec!(0.0001)));
561 assert_eq!(report.trailing_offset, Some(dec!(0.0002)));
562 assert_eq!(report.trailing_offset_type, TrailingOffsetType::BasisPoints);
563 assert_eq!(report.display_qty, Some(Quantity::from("50")));
564 assert_eq!(report.expire_time, Some(UnixNanos::from(4_000_000_000)));
565 assert!(report.post_only);
566 assert!(report.reduce_only);
567 assert_eq!(report.cancel_reason, Some("User requested".to_string()));
568 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
569 assert_eq!(report.contingency_type, ContingencyType::Oco);
570 Ok(())
571 }
572
573 #[rstest]
574 fn test_display() {
575 let report = test_order_status_report();
576 let display_str = format!("{report}");
577
578 assert!(display_str.contains("OrderStatusReport"));
579 assert!(display_str.contains("SIM-001"));
580 assert!(display_str.contains("AUDUSD.SIM"));
581 assert!(display_str.contains("BUY"));
582 assert!(display_str.contains("LIMIT"));
583 assert!(display_str.contains("GTC"));
584 assert!(display_str.contains("ACCEPTED"));
585 assert!(display_str.contains("100"));
586 }
587
588 #[rstest]
589 fn test_clone_and_equality() {
590 let report1 = test_order_status_report();
591 let report2 = report1.clone();
592
593 assert_eq!(report1, report2);
594 }
595
596 #[rstest]
597 fn test_serialization_roundtrip() {
598 let original = test_order_status_report();
599
600 let json = serde_json::to_string(&original).unwrap();
602 let deserialized: OrderStatusReport = serde_json::from_str(&json).unwrap();
603 assert_eq!(original, deserialized);
604 }
605
606 #[rstest]
607 fn test_order_status_report_different_order_types() {
608 let market_report = OrderStatusReport::new(
609 AccountId::from("SIM-001"),
610 InstrumentId::from("AUDUSD.SIM"),
611 None,
612 VenueOrderId::from("1"),
613 OrderSide::Buy,
614 OrderType::Market,
615 TimeInForce::Ioc,
616 OrderStatus::Filled,
617 Quantity::from("100"),
618 Quantity::from("100"),
619 UnixNanos::from(1_000_000_000),
620 UnixNanos::from(2_000_000_000),
621 UnixNanos::from(3_000_000_000),
622 None,
623 );
624
625 let stop_report = OrderStatusReport::new(
626 AccountId::from("SIM-001"),
627 InstrumentId::from("AUDUSD.SIM"),
628 None,
629 VenueOrderId::from("2"),
630 OrderSide::Sell,
631 OrderType::StopMarket,
632 TimeInForce::Gtc,
633 OrderStatus::Accepted,
634 Quantity::from("50"),
635 Quantity::from("0"),
636 UnixNanos::from(1_000_000_000),
637 UnixNanos::from(2_000_000_000),
638 UnixNanos::from(3_000_000_000),
639 None,
640 );
641
642 assert_eq!(market_report.order_type, OrderType::Market);
643 assert_eq!(stop_report.order_type, OrderType::StopMarket);
644 assert_ne!(market_report, stop_report);
645 }
646
647 #[rstest]
648 fn test_order_status_report_different_statuses() {
649 let accepted_report = test_order_status_report();
650
651 let filled_report = OrderStatusReport::new(
652 AccountId::from("SIM-001"),
653 InstrumentId::from("AUDUSD.SIM"),
654 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
655 VenueOrderId::from("1"),
656 OrderSide::Buy,
657 OrderType::Limit,
658 TimeInForce::Gtc,
659 OrderStatus::Filled,
660 Quantity::from("100"),
661 Quantity::from("100"), UnixNanos::from(1_000_000_000),
663 UnixNanos::from(2_000_000_000),
664 UnixNanos::from(3_000_000_000),
665 None,
666 );
667
668 assert_eq!(accepted_report.order_status, OrderStatus::Accepted);
669 assert_eq!(filled_report.order_status, OrderStatus::Filled);
670 assert_ne!(accepted_report, filled_report);
671 }
672
673 #[rstest]
674 #[allow(clippy::panic_in_result_fn)]
675 fn test_order_status_report_with_optional_fields() -> anyhow::Result<()> {
676 let mut report = test_order_status_report();
677
678 assert_eq!(report.price, None);
680 assert_eq!(report.avg_px, None);
681 assert!(!report.post_only);
682 assert!(!report.reduce_only);
683
684 report = report
686 .with_price(Price::from("1.00000"))
687 .with_avg_px(1.00001)?
688 .with_post_only(true)
689 .with_reduce_only(true);
690
691 assert_eq!(report.price, Some(Price::from("1.00000")));
692 assert_eq!(report.avg_px, Some(dec!(1.00001)));
693 assert!(report.post_only);
694 assert!(report.reduce_only);
695 Ok(())
696 }
697
698 #[rstest]
699 fn test_order_status_report_partial_fill() {
700 let partial_fill_report = OrderStatusReport::new(
701 AccountId::from("SIM-001"),
702 InstrumentId::from("AUDUSD.SIM"),
703 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
704 VenueOrderId::from("1"),
705 OrderSide::Buy,
706 OrderType::Limit,
707 TimeInForce::Gtc,
708 OrderStatus::PartiallyFilled,
709 Quantity::from("100"),
710 Quantity::from("30"), UnixNanos::from(1_000_000_000),
712 UnixNanos::from(2_000_000_000),
713 UnixNanos::from(3_000_000_000),
714 None,
715 );
716
717 assert_eq!(partial_fill_report.quantity, Quantity::from("100"));
718 assert_eq!(partial_fill_report.filled_qty, Quantity::from("30"));
719 assert_eq!(
720 partial_fill_report.order_status,
721 OrderStatus::PartiallyFilled
722 );
723 }
724
725 #[rstest]
726 fn test_order_status_report_with_all_timestamp_fields() {
727 let report = OrderStatusReport::new(
728 AccountId::from("SIM-001"),
729 InstrumentId::from("AUDUSD.SIM"),
730 None,
731 VenueOrderId::from("1"),
732 OrderSide::Buy,
733 OrderType::StopLimit,
734 TimeInForce::Gtc,
735 OrderStatus::Triggered,
736 Quantity::from("100"),
737 Quantity::from("0"),
738 UnixNanos::from(1_000_000_000), UnixNanos::from(2_000_000_000), UnixNanos::from(3_000_000_000), None,
742 )
743 .with_ts_triggered(UnixNanos::from(1_500_000_000));
744
745 assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
746 assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
747 assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
748 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
749 }
750
751 #[rstest]
752 fn test_is_order_updated_returns_true_when_price_differs() {
753 let order = OrderTestBuilder::new(OrderType::Limit)
754 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
755 .quantity(Quantity::from(100))
756 .price(Price::from("1.00000"))
757 .build();
758
759 let report = OrderStatusReport::new(
760 AccountId::from("SIM-001"),
761 InstrumentId::from("AUDUSD.SIM"),
762 None,
763 VenueOrderId::from("1"),
764 OrderSide::Buy,
765 OrderType::Limit,
766 TimeInForce::Gtc,
767 OrderStatus::Accepted,
768 Quantity::from("100"),
769 Quantity::from("0"),
770 UnixNanos::from(1_000_000_000),
771 UnixNanos::from(2_000_000_000),
772 UnixNanos::from(3_000_000_000),
773 None,
774 )
775 .with_price(Price::from("1.00100")); assert!(report.is_order_updated(&order));
778 }
779
780 #[rstest]
781 fn test_is_order_updated_returns_true_when_trigger_price_differs() {
782 let order = OrderTestBuilder::new(OrderType::StopMarket)
783 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
784 .quantity(Quantity::from(100))
785 .trigger_price(Price::from("0.99000"))
786 .build();
787
788 let report = OrderStatusReport::new(
789 AccountId::from("SIM-001"),
790 InstrumentId::from("AUDUSD.SIM"),
791 None,
792 VenueOrderId::from("1"),
793 OrderSide::Buy,
794 OrderType::StopMarket,
795 TimeInForce::Gtc,
796 OrderStatus::Accepted,
797 Quantity::from("100"),
798 Quantity::from("0"),
799 UnixNanos::from(1_000_000_000),
800 UnixNanos::from(2_000_000_000),
801 UnixNanos::from(3_000_000_000),
802 None,
803 )
804 .with_trigger_price(Price::from("0.99100")); assert!(report.is_order_updated(&order));
807 }
808
809 #[rstest]
810 fn test_is_order_updated_returns_true_when_quantity_differs() {
811 let order = OrderTestBuilder::new(OrderType::Limit)
812 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
813 .quantity(Quantity::from(100))
814 .price(Price::from("1.00000"))
815 .build();
816
817 let report = OrderStatusReport::new(
818 AccountId::from("SIM-001"),
819 InstrumentId::from("AUDUSD.SIM"),
820 None,
821 VenueOrderId::from("1"),
822 OrderSide::Buy,
823 OrderType::Limit,
824 TimeInForce::Gtc,
825 OrderStatus::Accepted,
826 Quantity::from("200"), Quantity::from("0"),
828 UnixNanos::from(1_000_000_000),
829 UnixNanos::from(2_000_000_000),
830 UnixNanos::from(3_000_000_000),
831 None,
832 )
833 .with_price(Price::from("1.00000"));
834
835 assert!(report.is_order_updated(&order));
836 }
837
838 #[rstest]
839 fn test_is_order_updated_returns_false_when_all_match() {
840 let order = OrderTestBuilder::new(OrderType::Limit)
841 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
842 .quantity(Quantity::from(100))
843 .price(Price::from("1.00000"))
844 .build();
845
846 let report = OrderStatusReport::new(
847 AccountId::from("SIM-001"),
848 InstrumentId::from("AUDUSD.SIM"),
849 None,
850 VenueOrderId::from("1"),
851 OrderSide::Buy,
852 OrderType::Limit,
853 TimeInForce::Gtc,
854 OrderStatus::Accepted,
855 Quantity::from("100"), Quantity::from("0"),
857 UnixNanos::from(1_000_000_000),
858 UnixNanos::from(2_000_000_000),
859 UnixNanos::from(3_000_000_000),
860 None,
861 )
862 .with_price(Price::from("1.00000")); assert!(!report.is_order_updated(&order));
865 }
866
867 #[rstest]
868 fn test_is_order_updated_returns_false_when_order_has_no_price() {
869 let order = OrderTestBuilder::new(OrderType::Market)
871 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
872 .quantity(Quantity::from(100))
873 .build();
874
875 let report = OrderStatusReport::new(
876 AccountId::from("SIM-001"),
877 InstrumentId::from("AUDUSD.SIM"),
878 None,
879 VenueOrderId::from("1"),
880 OrderSide::Buy,
881 OrderType::Market,
882 TimeInForce::Ioc,
883 OrderStatus::Accepted,
884 Quantity::from("100"), Quantity::from("0"),
886 UnixNanos::from(1_000_000_000),
887 UnixNanos::from(2_000_000_000),
888 UnixNanos::from(3_000_000_000),
889 None,
890 )
891 .with_price(Price::from("1.00000")); assert!(!report.is_order_updated(&order));
894 }
895
896 #[rstest]
897 fn test_is_order_updated_stop_limit_order_with_both_prices() {
898 let order = OrderTestBuilder::new(OrderType::StopLimit)
899 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
900 .quantity(Quantity::from(100))
901 .price(Price::from("1.00000"))
902 .trigger_price(Price::from("0.99000"))
903 .build();
904
905 let report_same = OrderStatusReport::new(
907 AccountId::from("SIM-001"),
908 InstrumentId::from("AUDUSD.SIM"),
909 None,
910 VenueOrderId::from("1"),
911 OrderSide::Buy,
912 OrderType::StopLimit,
913 TimeInForce::Gtc,
914 OrderStatus::Accepted,
915 Quantity::from("100"),
916 Quantity::from("0"),
917 UnixNanos::from(1_000_000_000),
918 UnixNanos::from(2_000_000_000),
919 UnixNanos::from(3_000_000_000),
920 None,
921 )
922 .with_price(Price::from("1.00000"))
923 .with_trigger_price(Price::from("0.99000"));
924
925 assert!(!report_same.is_order_updated(&order));
926
927 let report_diff_price = OrderStatusReport::new(
929 AccountId::from("SIM-001"),
930 InstrumentId::from("AUDUSD.SIM"),
931 None,
932 VenueOrderId::from("1"),
933 OrderSide::Buy,
934 OrderType::StopLimit,
935 TimeInForce::Gtc,
936 OrderStatus::Accepted,
937 Quantity::from("100"),
938 Quantity::from("0"),
939 UnixNanos::from(1_000_000_000),
940 UnixNanos::from(2_000_000_000),
941 UnixNanos::from(3_000_000_000),
942 None,
943 )
944 .with_price(Price::from("1.00100")) .with_trigger_price(Price::from("0.99000"));
946
947 assert!(report_diff_price.is_order_updated(&order));
948
949 let report_diff_trigger = OrderStatusReport::new(
951 AccountId::from("SIM-001"),
952 InstrumentId::from("AUDUSD.SIM"),
953 None,
954 VenueOrderId::from("1"),
955 OrderSide::Buy,
956 OrderType::StopLimit,
957 TimeInForce::Gtc,
958 OrderStatus::Accepted,
959 Quantity::from("100"),
960 Quantity::from("0"),
961 UnixNanos::from(1_000_000_000),
962 UnixNanos::from(2_000_000_000),
963 UnixNanos::from(3_000_000_000),
964 None,
965 )
966 .with_price(Price::from("1.00000"))
967 .with_trigger_price(Price::from("0.99100")); assert!(report_diff_trigger.is_order_updated(&order));
970 }
971}