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