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", 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 activation_price: Option<Price>,
88 pub trigger_price: Option<Price>,
90 pub trigger_type: Option<TriggerType>,
92 pub limit_offset: Option<Decimal>,
94 pub trailing_offset: Option<Decimal>,
96 pub trailing_offset_type: TrailingOffsetType,
98 pub avg_px: Option<Decimal>,
100 pub display_qty: Option<Quantity>,
102 pub post_only: bool,
104 pub reduce_only: bool,
106 pub cancel_reason: Option<String>,
108 pub ts_triggered: Option<UnixNanos>,
110}
111
112impl OrderStatusReport {
113 #[expect(clippy::too_many_arguments)]
115 #[must_use]
116 pub fn new(
117 account_id: AccountId,
118 instrument_id: InstrumentId,
119 client_order_id: Option<ClientOrderId>,
120 venue_order_id: VenueOrderId,
121 order_side: OrderSide,
122 order_type: OrderType,
123 time_in_force: TimeInForce,
124 order_status: OrderStatus,
125 quantity: Quantity,
126 filled_qty: Quantity,
127 ts_accepted: UnixNanos,
128 ts_last: UnixNanos,
129 ts_init: UnixNanos,
130 report_id: Option<UUID4>,
131 ) -> Self {
132 Self {
133 account_id,
134 instrument_id,
135 client_order_id,
136 venue_order_id,
137 order_side,
138 order_type,
139 time_in_force,
140 order_status,
141 quantity,
142 filled_qty,
143 report_id: report_id.unwrap_or_default(),
144 ts_accepted,
145 ts_last,
146 ts_init,
147 order_list_id: None,
148 venue_position_id: None,
149 linked_order_ids: None,
150 parent_order_id: None,
151 contingency_type: ContingencyType::default(),
152 expire_time: None,
153 price: None,
154 activation_price: None,
155 trigger_price: None,
156 trigger_type: None,
157 limit_offset: None,
158 trailing_offset: None,
159 trailing_offset_type: TrailingOffsetType::default(),
160 avg_px: None,
161 display_qty: None,
162 post_only: false,
163 reduce_only: false,
164 cancel_reason: None,
165 ts_triggered: None,
166 }
167 }
168
169 #[must_use]
171 pub const fn with_client_order_id(mut self, client_order_id: ClientOrderId) -> Self {
172 self.client_order_id = Some(client_order_id);
173 self
174 }
175
176 #[must_use]
178 pub const fn with_order_list_id(mut self, order_list_id: OrderListId) -> Self {
179 self.order_list_id = Some(order_list_id);
180 self
181 }
182
183 #[must_use]
185 pub fn with_linked_order_ids(
186 mut self,
187 linked_order_ids: impl IntoIterator<Item = ClientOrderId>,
188 ) -> Self {
189 self.linked_order_ids = Some(linked_order_ids.into_iter().collect());
190 self
191 }
192
193 #[must_use]
195 pub const fn with_parent_order_id(mut self, parent_order_id: ClientOrderId) -> Self {
196 self.parent_order_id = Some(parent_order_id);
197 self
198 }
199
200 #[must_use]
202 pub const fn with_venue_position_id(mut self, venue_position_id: PositionId) -> Self {
203 self.venue_position_id = Some(venue_position_id);
204 self
205 }
206
207 #[must_use]
209 pub const fn with_price(mut self, price: Price) -> Self {
210 self.price = Some(price);
211 self
212 }
213
214 #[must_use]
216 pub const fn with_avg_px(mut self, avg_px: Decimal) -> Self {
217 self.avg_px = Some(avg_px);
218 self
219 }
220
221 #[must_use]
223 pub const fn with_activation_price(mut self, activation_price: Price) -> Self {
224 self.activation_price = Some(activation_price);
225 self
226 }
227
228 #[must_use]
230 pub const fn with_trigger_price(mut self, trigger_price: Price) -> Self {
231 self.trigger_price = Some(trigger_price);
232 self
233 }
234
235 #[must_use]
237 pub const fn with_trigger_type(mut self, trigger_type: TriggerType) -> Self {
238 self.trigger_type = Some(trigger_type);
239 self
240 }
241
242 #[must_use]
244 pub const fn with_limit_offset(mut self, limit_offset: Decimal) -> Self {
245 self.limit_offset = Some(limit_offset);
246 self
247 }
248
249 #[must_use]
251 pub const fn with_trailing_offset(mut self, trailing_offset: Decimal) -> Self {
252 self.trailing_offset = Some(trailing_offset);
253 self
254 }
255
256 #[must_use]
258 pub const fn with_trailing_offset_type(
259 mut self,
260 trailing_offset_type: TrailingOffsetType,
261 ) -> Self {
262 self.trailing_offset_type = trailing_offset_type;
263 self
264 }
265
266 #[must_use]
268 pub const fn with_display_qty(mut self, display_qty: Quantity) -> Self {
269 self.display_qty = Some(display_qty);
270 self
271 }
272
273 #[must_use]
275 pub const fn with_expire_time(mut self, expire_time: UnixNanos) -> Self {
276 self.expire_time = Some(expire_time);
277 self
278 }
279
280 #[must_use]
282 pub const fn with_post_only(mut self, post_only: bool) -> Self {
283 self.post_only = post_only;
284 self
285 }
286
287 #[must_use]
289 pub const fn with_reduce_only(mut self, reduce_only: bool) -> Self {
290 self.reduce_only = reduce_only;
291 self
292 }
293
294 #[must_use]
296 pub fn with_cancel_reason(mut self, cancel_reason: String) -> Self {
297 self.cancel_reason = Some(cancel_reason);
298 self
299 }
300
301 #[must_use]
303 pub const fn with_ts_triggered(mut self, ts_triggered: UnixNanos) -> Self {
304 self.ts_triggered = Some(ts_triggered);
305 self
306 }
307
308 #[must_use]
310 pub const fn with_contingency_type(mut self, contingency_type: ContingencyType) -> Self {
311 self.contingency_type = contingency_type;
312 self
313 }
314
315 #[must_use]
322 pub fn is_order_updated(&self, order: &impl Order) -> bool {
323 if order.has_price()
324 && let Some(report_price) = self.price
325 && let Some(order_price) = order.price()
326 && order_price != report_price
327 {
328 return true;
329 }
330
331 if let Some(order_trigger_price) = order.trigger_price()
332 && let Some(report_trigger_price) = self.trigger_price
333 && order_trigger_price != report_trigger_price
334 {
335 return true;
336 }
337
338 order.quantity() != self.quantity
339 }
340}
341
342impl Display for OrderStatusReport {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 write!(
345 f,
346 "OrderStatusReport(\
347 account_id={}, \
348 instrument_id={}, \
349 venue_order_id={}, \
350 order_side={}, \
351 order_type={}, \
352 time_in_force={}, \
353 order_status={}, \
354 quantity={}, \
355 filled_qty={}, \
356 report_id={}, \
357 ts_accepted={}, \
358 ts_last={}, \
359 ts_init={}, \
360 client_order_id={:?}, \
361 order_list_id={:?}, \
362 venue_position_id={:?}, \
363 linked_order_ids={:?}, \
364 parent_order_id={:?}, \
365 contingency_type={}, \
366 expire_time={:?}, \
367 price={:?}, \
368 activation_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.activation_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_macros::dec;
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 fn test_order_status_report_builder_methods() {
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(dec!(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 }
571
572 #[rstest]
573 fn test_display() {
574 let report = test_order_status_report();
575 let display_str = format!("{report}");
576
577 assert!(display_str.contains("OrderStatusReport"));
578 assert!(display_str.contains("SIM-001"));
579 assert!(display_str.contains("AUDUSD.SIM"));
580 assert!(display_str.contains("BUY"));
581 assert!(display_str.contains("LIMIT"));
582 assert!(display_str.contains("GTC"));
583 assert!(display_str.contains("ACCEPTED"));
584 assert!(display_str.contains("100"));
585 }
586
587 #[rstest]
588 fn test_clone_and_equality() {
589 let report1 = test_order_status_report();
590 let report2 = report1.clone();
591
592 assert_eq!(report1, report2);
593 }
594
595 #[rstest]
596 fn test_serialization_roundtrip() {
597 let original = test_order_status_report();
598
599 let json = serde_json::to_string(&original).unwrap();
601 let deserialized: OrderStatusReport = serde_json::from_str(&json).unwrap();
602 assert_eq!(original, deserialized);
603 }
604
605 #[rstest]
606 fn test_order_status_report_different_order_types() {
607 let market_report = OrderStatusReport::new(
608 AccountId::from("SIM-001"),
609 InstrumentId::from("AUDUSD.SIM"),
610 None,
611 VenueOrderId::from("1"),
612 OrderSide::Buy,
613 OrderType::Market,
614 TimeInForce::Ioc,
615 OrderStatus::Filled,
616 Quantity::from("100"),
617 Quantity::from("100"),
618 UnixNanos::from(1_000_000_000),
619 UnixNanos::from(2_000_000_000),
620 UnixNanos::from(3_000_000_000),
621 None,
622 );
623
624 let stop_report = OrderStatusReport::new(
625 AccountId::from("SIM-001"),
626 InstrumentId::from("AUDUSD.SIM"),
627 None,
628 VenueOrderId::from("2"),
629 OrderSide::Sell,
630 OrderType::StopMarket,
631 TimeInForce::Gtc,
632 OrderStatus::Accepted,
633 Quantity::from("50"),
634 Quantity::from("0"),
635 UnixNanos::from(1_000_000_000),
636 UnixNanos::from(2_000_000_000),
637 UnixNanos::from(3_000_000_000),
638 None,
639 );
640
641 assert_eq!(market_report.order_type, OrderType::Market);
642 assert_eq!(stop_report.order_type, OrderType::StopMarket);
643 assert_ne!(market_report, stop_report);
644 }
645
646 #[rstest]
647 fn test_order_status_report_different_statuses() {
648 let accepted_report = test_order_status_report();
649
650 let filled_report = OrderStatusReport::new(
651 AccountId::from("SIM-001"),
652 InstrumentId::from("AUDUSD.SIM"),
653 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
654 VenueOrderId::from("1"),
655 OrderSide::Buy,
656 OrderType::Limit,
657 TimeInForce::Gtc,
658 OrderStatus::Filled,
659 Quantity::from("100"),
660 Quantity::from("100"), UnixNanos::from(1_000_000_000),
662 UnixNanos::from(2_000_000_000),
663 UnixNanos::from(3_000_000_000),
664 None,
665 );
666
667 assert_eq!(accepted_report.order_status, OrderStatus::Accepted);
668 assert_eq!(filled_report.order_status, OrderStatus::Filled);
669 assert_ne!(accepted_report, filled_report);
670 }
671
672 #[rstest]
673 fn test_order_status_report_with_optional_fields() {
674 let mut report = test_order_status_report();
675
676 assert_eq!(report.price, None);
678 assert_eq!(report.avg_px, None);
679 assert!(!report.post_only);
680 assert!(!report.reduce_only);
681
682 report = report
684 .with_price(Price::from("1.00000"))
685 .with_avg_px(dec!(1.00001))
686 .with_post_only(true)
687 .with_reduce_only(true);
688
689 assert_eq!(report.price, Some(Price::from("1.00000")));
690 assert_eq!(report.avg_px, Some(dec!(1.00001)));
691 assert!(report.post_only);
692 assert!(report.reduce_only);
693 }
694
695 #[rstest]
696 fn test_order_status_report_partial_fill() {
697 let partial_fill_report = OrderStatusReport::new(
698 AccountId::from("SIM-001"),
699 InstrumentId::from("AUDUSD.SIM"),
700 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
701 VenueOrderId::from("1"),
702 OrderSide::Buy,
703 OrderType::Limit,
704 TimeInForce::Gtc,
705 OrderStatus::PartiallyFilled,
706 Quantity::from("100"),
707 Quantity::from("30"), UnixNanos::from(1_000_000_000),
709 UnixNanos::from(2_000_000_000),
710 UnixNanos::from(3_000_000_000),
711 None,
712 );
713
714 assert_eq!(partial_fill_report.quantity, Quantity::from("100"));
715 assert_eq!(partial_fill_report.filled_qty, Quantity::from("30"));
716 assert_eq!(
717 partial_fill_report.order_status,
718 OrderStatus::PartiallyFilled
719 );
720 }
721
722 #[rstest]
723 fn test_order_status_report_with_all_timestamp_fields() {
724 let report = OrderStatusReport::new(
725 AccountId::from("SIM-001"),
726 InstrumentId::from("AUDUSD.SIM"),
727 None,
728 VenueOrderId::from("1"),
729 OrderSide::Buy,
730 OrderType::StopLimit,
731 TimeInForce::Gtc,
732 OrderStatus::Triggered,
733 Quantity::from("100"),
734 Quantity::from("0"),
735 UnixNanos::from(1_000_000_000), UnixNanos::from(2_000_000_000), UnixNanos::from(3_000_000_000), None,
739 )
740 .with_ts_triggered(UnixNanos::from(1_500_000_000));
741
742 assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
743 assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
744 assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
745 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
746 }
747
748 #[rstest]
749 fn test_is_order_updated_returns_true_when_price_differs() {
750 let order = OrderTestBuilder::new(OrderType::Limit)
751 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
752 .quantity(Quantity::from(100))
753 .price(Price::from("1.00000"))
754 .build();
755
756 let report = OrderStatusReport::new(
757 AccountId::from("SIM-001"),
758 InstrumentId::from("AUDUSD.SIM"),
759 None,
760 VenueOrderId::from("1"),
761 OrderSide::Buy,
762 OrderType::Limit,
763 TimeInForce::Gtc,
764 OrderStatus::Accepted,
765 Quantity::from("100"),
766 Quantity::from("0"),
767 UnixNanos::from(1_000_000_000),
768 UnixNanos::from(2_000_000_000),
769 UnixNanos::from(3_000_000_000),
770 None,
771 )
772 .with_price(Price::from("1.00100")); assert!(report.is_order_updated(&order));
775 }
776
777 #[rstest]
778 fn test_is_order_updated_returns_true_when_trigger_price_differs() {
779 let order = OrderTestBuilder::new(OrderType::StopMarket)
780 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
781 .quantity(Quantity::from(100))
782 .trigger_price(Price::from("0.99000"))
783 .build();
784
785 let report = OrderStatusReport::new(
786 AccountId::from("SIM-001"),
787 InstrumentId::from("AUDUSD.SIM"),
788 None,
789 VenueOrderId::from("1"),
790 OrderSide::Buy,
791 OrderType::StopMarket,
792 TimeInForce::Gtc,
793 OrderStatus::Accepted,
794 Quantity::from("100"),
795 Quantity::from("0"),
796 UnixNanos::from(1_000_000_000),
797 UnixNanos::from(2_000_000_000),
798 UnixNanos::from(3_000_000_000),
799 None,
800 )
801 .with_trigger_price(Price::from("0.99100")); assert!(report.is_order_updated(&order));
804 }
805
806 #[rstest]
807 fn test_is_order_updated_returns_true_when_quantity_differs() {
808 let order = OrderTestBuilder::new(OrderType::Limit)
809 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
810 .quantity(Quantity::from(100))
811 .price(Price::from("1.00000"))
812 .build();
813
814 let report = OrderStatusReport::new(
815 AccountId::from("SIM-001"),
816 InstrumentId::from("AUDUSD.SIM"),
817 None,
818 VenueOrderId::from("1"),
819 OrderSide::Buy,
820 OrderType::Limit,
821 TimeInForce::Gtc,
822 OrderStatus::Accepted,
823 Quantity::from("200"), Quantity::from("0"),
825 UnixNanos::from(1_000_000_000),
826 UnixNanos::from(2_000_000_000),
827 UnixNanos::from(3_000_000_000),
828 None,
829 )
830 .with_price(Price::from("1.00000"));
831
832 assert!(report.is_order_updated(&order));
833 }
834
835 #[rstest]
836 fn test_is_order_updated_returns_false_when_all_match() {
837 let order = OrderTestBuilder::new(OrderType::Limit)
838 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
839 .quantity(Quantity::from(100))
840 .price(Price::from("1.00000"))
841 .build();
842
843 let report = OrderStatusReport::new(
844 AccountId::from("SIM-001"),
845 InstrumentId::from("AUDUSD.SIM"),
846 None,
847 VenueOrderId::from("1"),
848 OrderSide::Buy,
849 OrderType::Limit,
850 TimeInForce::Gtc,
851 OrderStatus::Accepted,
852 Quantity::from("100"), Quantity::from("0"),
854 UnixNanos::from(1_000_000_000),
855 UnixNanos::from(2_000_000_000),
856 UnixNanos::from(3_000_000_000),
857 None,
858 )
859 .with_price(Price::from("1.00000")); assert!(!report.is_order_updated(&order));
862 }
863
864 #[rstest]
865 fn test_is_order_updated_returns_false_when_order_has_no_price() {
866 let order = OrderTestBuilder::new(OrderType::Market)
868 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
869 .quantity(Quantity::from(100))
870 .build();
871
872 let report = OrderStatusReport::new(
873 AccountId::from("SIM-001"),
874 InstrumentId::from("AUDUSD.SIM"),
875 None,
876 VenueOrderId::from("1"),
877 OrderSide::Buy,
878 OrderType::Market,
879 TimeInForce::Ioc,
880 OrderStatus::Accepted,
881 Quantity::from("100"), Quantity::from("0"),
883 UnixNanos::from(1_000_000_000),
884 UnixNanos::from(2_000_000_000),
885 UnixNanos::from(3_000_000_000),
886 None,
887 )
888 .with_price(Price::from("1.00000")); assert!(!report.is_order_updated(&order));
891 }
892
893 #[rstest]
894 fn test_is_order_updated_stop_limit_order_with_both_prices() {
895 let order = OrderTestBuilder::new(OrderType::StopLimit)
896 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
897 .quantity(Quantity::from(100))
898 .price(Price::from("1.00000"))
899 .trigger_price(Price::from("0.99000"))
900 .build();
901
902 let report_same = OrderStatusReport::new(
904 AccountId::from("SIM-001"),
905 InstrumentId::from("AUDUSD.SIM"),
906 None,
907 VenueOrderId::from("1"),
908 OrderSide::Buy,
909 OrderType::StopLimit,
910 TimeInForce::Gtc,
911 OrderStatus::Accepted,
912 Quantity::from("100"),
913 Quantity::from("0"),
914 UnixNanos::from(1_000_000_000),
915 UnixNanos::from(2_000_000_000),
916 UnixNanos::from(3_000_000_000),
917 None,
918 )
919 .with_price(Price::from("1.00000"))
920 .with_trigger_price(Price::from("0.99000"));
921
922 assert!(!report_same.is_order_updated(&order));
923
924 let report_diff_price = OrderStatusReport::new(
926 AccountId::from("SIM-001"),
927 InstrumentId::from("AUDUSD.SIM"),
928 None,
929 VenueOrderId::from("1"),
930 OrderSide::Buy,
931 OrderType::StopLimit,
932 TimeInForce::Gtc,
933 OrderStatus::Accepted,
934 Quantity::from("100"),
935 Quantity::from("0"),
936 UnixNanos::from(1_000_000_000),
937 UnixNanos::from(2_000_000_000),
938 UnixNanos::from(3_000_000_000),
939 None,
940 )
941 .with_price(Price::from("1.00100")) .with_trigger_price(Price::from("0.99000"));
943
944 assert!(report_diff_price.is_order_updated(&order));
945
946 let report_diff_trigger = OrderStatusReport::new(
948 AccountId::from("SIM-001"),
949 InstrumentId::from("AUDUSD.SIM"),
950 None,
951 VenueOrderId::from("1"),
952 OrderSide::Buy,
953 OrderType::StopLimit,
954 TimeInForce::Gtc,
955 OrderStatus::Accepted,
956 Quantity::from("100"),
957 Quantity::from("0"),
958 UnixNanos::from(1_000_000_000),
959 UnixNanos::from(2_000_000_000),
960 UnixNanos::from(3_000_000_000),
961 None,
962 )
963 .with_price(Price::from("1.00000"))
964 .with_trigger_price(Price::from("0.99100")); assert!(report_diff_trigger.is_order_updated(&order));
967 }
968}