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