Skip to main content

nautilus_model/reports/
order.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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/// Represents an order status at a point in time.
33#[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    /// The account ID associated with the position.
45    pub account_id: AccountId,
46    /// The instrument ID associated with the event.
47    pub instrument_id: InstrumentId,
48    /// The client order ID.
49    pub client_order_id: Option<ClientOrderId>,
50    /// The venue assigned order ID.
51    pub venue_order_id: VenueOrderId,
52    /// The order side.
53    pub order_side: OrderSide,
54    /// The order type.
55    pub order_type: OrderType,
56    /// The order time in force.
57    pub time_in_force: TimeInForce,
58    /// The order status.
59    pub order_status: OrderStatus,
60    /// The order quantity.
61    pub quantity: Quantity,
62    /// The order total filled quantity.
63    pub filled_qty: Quantity,
64    /// The unique identifier for the event.
65    pub report_id: UUID4,
66    /// UNIX timestamp (nanoseconds) when the order was accepted.
67    pub ts_accepted: UnixNanos,
68    /// UNIX timestamp (nanoseconds) when the last event occurred.
69    pub ts_last: UnixNanos,
70    /// UNIX timestamp (nanoseconds) when the event was initialized.
71    pub ts_init: UnixNanos,
72    /// The order list ID associated with the order.
73    pub order_list_id: Option<OrderListId>,
74    /// The position ID associated with the order (assigned by the venue).
75    pub venue_position_id: Option<PositionId>,
76    /// The reported linked client order IDs related to contingency orders.
77    pub linked_order_ids: Option<Vec<ClientOrderId>>,
78    /// The parent order ID for contingent child orders, if available.
79    pub parent_order_id: Option<ClientOrderId>,
80    /// The orders contingency type.
81    pub contingency_type: ContingencyType,
82    /// The order expiration (UNIX epoch nanoseconds), zero for no expiration.
83    pub expire_time: Option<UnixNanos>,
84    /// The order price (LIMIT).
85    pub price: Option<Price>,
86    /// The order activation price (trailing stop).
87    pub activation_price: Option<Price>,
88    /// The order trigger price (STOP).
89    pub trigger_price: Option<Price>,
90    /// The trigger type for the order.
91    pub trigger_type: Option<TriggerType>,
92    /// The trailing offset for the orders limit price.
93    pub limit_offset: Option<Decimal>,
94    /// The trailing offset for the orders trigger price (STOP).
95    pub trailing_offset: Option<Decimal>,
96    /// The trailing offset type.
97    pub trailing_offset_type: TrailingOffsetType,
98    /// The order average fill price.
99    pub avg_px: Option<Decimal>,
100    /// The quantity of the `LIMIT` order to display on the public book (iceberg).
101    pub display_qty: Option<Quantity>,
102    /// If the order will only provide liquidity (make a market).
103    pub post_only: bool,
104    /// If the order carries the 'reduce-only' execution instruction.
105    pub reduce_only: bool,
106    /// The reason for order cancellation.
107    pub cancel_reason: Option<String>,
108    /// UNIX timestamp (nanoseconds) when the order was triggered.
109    pub ts_triggered: Option<UnixNanos>,
110}
111
112impl OrderStatusReport {
113    /// Creates a new [`OrderStatusReport`] instance with required fields.
114    #[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    /// Sets the client order ID.
170    #[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    /// Sets the order list ID.
177    #[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    /// Sets the linked client order IDs.
184    #[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    /// Sets the parent order ID.
194    #[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    /// Sets the venue position ID.
201    #[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    /// Sets the price.
208    #[must_use]
209    pub const fn with_price(mut self, price: Price) -> Self {
210        self.price = Some(price);
211        self
212    }
213
214    /// Sets the average price.
215    #[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    /// Sets the activation price.
222    #[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    /// Sets the trigger price.
229    #[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    /// Sets the trigger type.
236    #[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    /// Sets the limit offset.
243    #[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    /// Sets the trailing offset.
250    #[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    /// Sets the trailing offset type.
257    #[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    /// Sets the display quantity.
267    #[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    /// Sets the expire time.
274    #[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    /// Sets `post_only` flag.
281    #[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    /// Sets `reduce_only` flag.
288    #[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    /// Sets cancel reason.
295    #[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    /// Sets the triggered timestamp.
302    #[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    /// Sets the contingency type.
309    #[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    /// Returns whether the order has been updated based on this report.
316    ///
317    /// An order is considered updated if any of the following differ:
318    /// - Price (if both the order and report have a price).
319    /// - Trigger price (if both the order and report have a trigger price).
320    /// - Quantity.
321    #[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        // Test default values
478        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, // No report ID provided, should generate one
515        );
516
517        // Should have a generated UUID
518        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        // Test JSON serialization
600        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"), // Fully filled
661            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        // Initially no optional fields set
677        assert_eq!(report.price, None);
678        assert_eq!(report.avg_px, None);
679        assert!(!report.post_only);
680        assert!(!report.reduce_only);
681
682        // Test builder pattern with various optional fields
683        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"), // Partially filled
708            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), // ts_accepted
736            UnixNanos::from(2_000_000_000), // ts_last
737            UnixNanos::from(3_000_000_000), // ts_init
738            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")); // Different price
773
774        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")); // Different trigger price
802
803        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"), // Different quantity
824            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"), // Same quantity
853            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")); // Same price
860
861        assert!(!report.is_order_updated(&order));
862    }
863
864    #[rstest]
865    fn test_is_order_updated_returns_false_when_order_has_no_price() {
866        // Market orders have no price, so only quantity comparison matters
867        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"), // Same quantity
882            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")); // Report has price, but order doesn't
889
890        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        // Same everything
903        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        // Different limit price
925        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")) // Different
942        .with_trigger_price(Price::from("0.99000"));
943
944        assert!(report_diff_price.is_order_updated(&order));
945
946        // Different trigger price
947        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")); // Different
965
966        assert!(report_diff_trigger.is_order_updated(&order));
967    }
968}