nautilus_model/reports/
order.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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    types::{Price, Quantity},
29};
30
31/// Represents an order status at a point in time.
32#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
33#[serde(tag = "type")]
34#[cfg_attr(
35    feature = "python",
36    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
37)]
38pub struct OrderStatusReport {
39    /// The account ID associated with the position.
40    pub account_id: AccountId,
41    /// The instrument ID associated with the event.
42    pub instrument_id: InstrumentId,
43    /// The client order ID.
44    pub client_order_id: Option<ClientOrderId>,
45    /// The venue assigned order ID.
46    pub venue_order_id: VenueOrderId,
47    /// The order side.
48    pub order_side: OrderSide,
49    /// The order type.
50    pub order_type: OrderType,
51    /// The order time in force.
52    pub time_in_force: TimeInForce,
53    /// The order status.
54    pub order_status: OrderStatus,
55    /// The order quantity.
56    pub quantity: Quantity,
57    /// The order total filled quantity.
58    pub filled_qty: Quantity,
59    /// The unique identifier for the event.
60    pub report_id: UUID4,
61    /// UNIX timestamp (nanoseconds) when the order was accepted.
62    pub ts_accepted: UnixNanos,
63    /// UNIX timestamp (nanoseconds) when the last event occurred.
64    pub ts_last: UnixNanos,
65    /// UNIX timestamp (nanoseconds) when the event was initialized.
66    pub ts_init: UnixNanos,
67    /// The order list ID associated with the order.
68    pub order_list_id: Option<OrderListId>,
69    /// The position ID associated with the order (assigned by the venue).
70    pub venue_position_id: Option<PositionId>,
71    /// The reported linked client order IDs related to contingency orders.
72    pub linked_order_ids: Option<Vec<ClientOrderId>>,
73    /// The parent order ID for contingent child orders, if available.
74    pub parent_order_id: Option<ClientOrderId>,
75    /// The orders contingency type.
76    pub contingency_type: ContingencyType,
77    /// The order expiration (UNIX epoch nanoseconds), zero for no expiration.
78    pub expire_time: Option<UnixNanos>,
79    /// The order price (LIMIT).
80    pub price: Option<Price>,
81    /// The order trigger price (STOP).
82    pub trigger_price: Option<Price>,
83    /// The trigger type for the order.
84    pub trigger_type: Option<TriggerType>,
85    /// The trailing offset for the orders limit price.
86    pub limit_offset: Option<Decimal>,
87    /// The trailing offset for the orders trigger price (STOP).
88    pub trailing_offset: Option<Decimal>,
89    /// The trailing offset type.
90    pub trailing_offset_type: TrailingOffsetType,
91    /// The order average fill price.
92    pub avg_px: Option<Decimal>,
93    /// The quantity of the `LIMIT` order to display on the public book (iceberg).
94    pub display_qty: Option<Quantity>,
95    /// If the order will only provide liquidity (make a market).
96    pub post_only: bool,
97    /// If the order carries the 'reduce-only' execution instruction.
98    pub reduce_only: bool,
99    /// The reason for order cancellation.
100    pub cancel_reason: Option<String>,
101    /// UNIX timestamp (nanoseconds) when the order was triggered.
102    pub ts_triggered: Option<UnixNanos>,
103}
104
105impl OrderStatusReport {
106    /// Creates a new [`OrderStatusReport`] instance with required fields.
107    #[allow(clippy::too_many_arguments)]
108    #[must_use]
109    pub fn new(
110        account_id: AccountId,
111        instrument_id: InstrumentId,
112        client_order_id: Option<ClientOrderId>,
113        venue_order_id: VenueOrderId,
114        order_side: OrderSide,
115        order_type: OrderType,
116        time_in_force: TimeInForce,
117        order_status: OrderStatus,
118        quantity: Quantity,
119        filled_qty: Quantity,
120        ts_accepted: UnixNanos,
121        ts_last: UnixNanos,
122        ts_init: UnixNanos,
123        report_id: Option<UUID4>,
124    ) -> Self {
125        Self {
126            account_id,
127            instrument_id,
128            client_order_id,
129            venue_order_id,
130            order_side,
131            order_type,
132            time_in_force,
133            order_status,
134            quantity,
135            filled_qty,
136            report_id: report_id.unwrap_or_default(),
137            ts_accepted,
138            ts_last,
139            ts_init,
140            order_list_id: None,
141            venue_position_id: None,
142            linked_order_ids: None,
143            parent_order_id: None,
144            contingency_type: ContingencyType::default(),
145            expire_time: None,
146            price: None,
147            trigger_price: None,
148            trigger_type: None,
149            limit_offset: None,
150            trailing_offset: None,
151            trailing_offset_type: TrailingOffsetType::default(),
152            avg_px: None,
153            display_qty: None,
154            post_only: false,
155            reduce_only: false,
156            cancel_reason: None,
157            ts_triggered: None,
158        }
159    }
160
161    /// Sets the client order ID.
162    #[must_use]
163    pub const fn with_client_order_id(mut self, client_order_id: ClientOrderId) -> Self {
164        self.client_order_id = Some(client_order_id);
165        self
166    }
167
168    /// Sets the order list ID.
169    #[must_use]
170    pub const fn with_order_list_id(mut self, order_list_id: OrderListId) -> Self {
171        self.order_list_id = Some(order_list_id);
172        self
173    }
174
175    /// Sets the linked client order IDs.
176    #[must_use]
177    pub fn with_linked_order_ids(
178        mut self,
179        linked_order_ids: impl IntoIterator<Item = ClientOrderId>,
180    ) -> Self {
181        self.linked_order_ids = Some(linked_order_ids.into_iter().collect());
182        self
183    }
184
185    /// Sets the parent order ID.
186    #[must_use]
187    pub const fn with_parent_order_id(mut self, parent_order_id: ClientOrderId) -> Self {
188        self.parent_order_id = Some(parent_order_id);
189        self
190    }
191
192    /// Sets the venue position ID.
193    #[must_use]
194    pub const fn with_venue_position_id(mut self, venue_position_id: PositionId) -> Self {
195        self.venue_position_id = Some(venue_position_id);
196        self
197    }
198
199    /// Sets the price.
200    #[must_use]
201    pub const fn with_price(mut self, price: Price) -> Self {
202        self.price = Some(price);
203        self
204    }
205
206    /// Sets the average price.
207    ///
208    /// # Errors
209    ///
210    /// Returns an error if `avg_px` cannot be converted to a valid `Decimal`.
211    pub fn with_avg_px(mut self, avg_px: f64) -> anyhow::Result<Self> {
212        if !avg_px.is_finite() {
213            anyhow::bail!(
214                "avg_px must be finite, got: {} (is_nan: {}, is_infinite: {})",
215                avg_px,
216                avg_px.is_nan(),
217                avg_px.is_infinite()
218            );
219        }
220
221        self.avg_px = Some(Decimal::from_f64_retain(avg_px).ok_or_else(|| {
222            anyhow::anyhow!(
223                "Failed to convert avg_px to Decimal: {} (possible overflow/underflow)",
224                avg_px
225            )
226        })?);
227        Ok(self)
228    }
229
230    /// Sets the trigger price.
231    #[must_use]
232    pub const fn with_trigger_price(mut self, trigger_price: Price) -> Self {
233        self.trigger_price = Some(trigger_price);
234        self
235    }
236
237    /// Sets the trigger type.
238    #[must_use]
239    pub const fn with_trigger_type(mut self, trigger_type: TriggerType) -> Self {
240        self.trigger_type = Some(trigger_type);
241        self
242    }
243
244    /// Sets the limit offset.
245    #[must_use]
246    pub const fn with_limit_offset(mut self, limit_offset: Decimal) -> Self {
247        self.limit_offset = Some(limit_offset);
248        self
249    }
250
251    /// Sets the trailing offset.
252    #[must_use]
253    pub const fn with_trailing_offset(mut self, trailing_offset: Decimal) -> Self {
254        self.trailing_offset = Some(trailing_offset);
255        self
256    }
257
258    /// Sets the trailing offset type.
259    #[must_use]
260    pub const fn with_trailing_offset_type(
261        mut self,
262        trailing_offset_type: TrailingOffsetType,
263    ) -> Self {
264        self.trailing_offset_type = trailing_offset_type;
265        self
266    }
267
268    /// Sets the display quantity.
269    #[must_use]
270    pub const fn with_display_qty(mut self, display_qty: Quantity) -> Self {
271        self.display_qty = Some(display_qty);
272        self
273    }
274
275    /// Sets the expire time.
276    #[must_use]
277    pub const fn with_expire_time(mut self, expire_time: UnixNanos) -> Self {
278        self.expire_time = Some(expire_time);
279        self
280    }
281
282    /// Sets `post_only` flag.
283    #[must_use]
284    pub const fn with_post_only(mut self, post_only: bool) -> Self {
285        self.post_only = post_only;
286        self
287    }
288
289    /// Sets `reduce_only` flag.
290    #[must_use]
291    pub const fn with_reduce_only(mut self, reduce_only: bool) -> Self {
292        self.reduce_only = reduce_only;
293        self
294    }
295
296    /// Sets cancel reason.
297    #[must_use]
298    pub fn with_cancel_reason(mut self, cancel_reason: String) -> Self {
299        self.cancel_reason = Some(cancel_reason);
300        self
301    }
302
303    /// Sets the triggered timestamp.
304    #[must_use]
305    pub const fn with_ts_triggered(mut self, ts_triggered: UnixNanos) -> Self {
306        self.ts_triggered = Some(ts_triggered);
307        self
308    }
309
310    /// Sets the contingency type.
311    #[must_use]
312    pub const fn with_contingency_type(mut self, contingency_type: ContingencyType) -> Self {
313        self.contingency_type = contingency_type;
314        self
315    }
316}
317
318impl Display for OrderStatusReport {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        write!(
321            f,
322            "OrderStatusReport(\
323                account_id={}, \
324                instrument_id={}, \
325                venue_order_id={}, \
326                order_side={}, \
327                order_type={}, \
328                time_in_force={}, \
329                order_status={}, \
330                quantity={}, \
331                filled_qty={}, \
332                report_id={}, \
333                ts_accepted={}, \
334                ts_last={}, \
335                ts_init={}, \
336                client_order_id={:?}, \
337                order_list_id={:?}, \
338                venue_position_id={:?}, \
339                linked_order_ids={:?}, \
340                parent_order_id={:?}, \
341                contingency_type={}, \
342                expire_time={:?}, \
343                price={:?}, \
344                trigger_price={:?}, \
345                trigger_type={:?}, \
346                limit_offset={:?}, \
347                trailing_offset={:?}, \
348                trailing_offset_type={}, \
349                avg_px={:?}, \
350                display_qty={:?}, \
351                post_only={}, \
352                reduce_only={}, \
353                cancel_reason={:?}, \
354                ts_triggered={:?}\
355            )",
356            self.account_id,
357            self.instrument_id,
358            self.venue_order_id,
359            self.order_side,
360            self.order_type,
361            self.time_in_force,
362            self.order_status,
363            self.quantity,
364            self.filled_qty,
365            self.report_id,
366            self.ts_accepted,
367            self.ts_last,
368            self.ts_init,
369            self.client_order_id,
370            self.order_list_id,
371            self.venue_position_id,
372            self.linked_order_ids,
373            self.parent_order_id,
374            self.contingency_type,
375            self.expire_time,
376            self.price,
377            self.trigger_price,
378            self.trigger_type,
379            self.limit_offset,
380            self.trailing_offset,
381            self.trailing_offset_type,
382            self.avg_px,
383            self.display_qty,
384            self.post_only,
385            self.reduce_only,
386            self.cancel_reason,
387            self.ts_triggered,
388        )
389    }
390}
391
392////////////////////////////////////////////////////////////////////////////////
393// Tests
394////////////////////////////////////////////////////////////////////////////////
395#[cfg(test)]
396mod tests {
397    use nautilus_core::UnixNanos;
398    use rstest::*;
399    use rust_decimal::Decimal;
400
401    use super::*;
402    use crate::{
403        enums::{
404            ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType,
405            TriggerType,
406        },
407        identifiers::{
408            AccountId, ClientOrderId, InstrumentId, OrderListId, PositionId, VenueOrderId,
409        },
410        types::{Price, Quantity},
411    };
412
413    fn test_order_status_report() -> OrderStatusReport {
414        OrderStatusReport::new(
415            AccountId::from("SIM-001"),
416            InstrumentId::from("AUDUSD.SIM"),
417            Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
418            VenueOrderId::from("1"),
419            OrderSide::Buy,
420            OrderType::Limit,
421            TimeInForce::Gtc,
422            OrderStatus::Accepted,
423            Quantity::from("100"),
424            Quantity::from("0"),
425            UnixNanos::from(1_000_000_000),
426            UnixNanos::from(2_000_000_000),
427            UnixNanos::from(3_000_000_000),
428            None,
429        )
430    }
431
432    #[rstest]
433    fn test_order_status_report_new() {
434        let report = test_order_status_report();
435
436        assert_eq!(report.account_id, AccountId::from("SIM-001"));
437        assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
438        assert_eq!(
439            report.client_order_id,
440            Some(ClientOrderId::from("O-19700101-000000-001-001-1"))
441        );
442        assert_eq!(report.venue_order_id, VenueOrderId::from("1"));
443        assert_eq!(report.order_side, OrderSide::Buy);
444        assert_eq!(report.order_type, OrderType::Limit);
445        assert_eq!(report.time_in_force, TimeInForce::Gtc);
446        assert_eq!(report.order_status, OrderStatus::Accepted);
447        assert_eq!(report.quantity, Quantity::from("100"));
448        assert_eq!(report.filled_qty, Quantity::from("0"));
449        assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
450        assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
451        assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
452
453        // Test default values
454        assert_eq!(report.order_list_id, None);
455        assert_eq!(report.venue_position_id, None);
456        assert_eq!(report.linked_order_ids, None);
457        assert_eq!(report.parent_order_id, None);
458        assert_eq!(report.contingency_type, ContingencyType::default());
459        assert_eq!(report.expire_time, None);
460        assert_eq!(report.price, None);
461        assert_eq!(report.trigger_price, None);
462        assert_eq!(report.trigger_type, None);
463        assert_eq!(report.limit_offset, None);
464        assert_eq!(report.trailing_offset, None);
465        assert_eq!(report.trailing_offset_type, TrailingOffsetType::default());
466        assert_eq!(report.avg_px, None);
467        assert_eq!(report.display_qty, None);
468        assert!(!report.post_only);
469        assert!(!report.reduce_only);
470        assert_eq!(report.cancel_reason, None);
471        assert_eq!(report.ts_triggered, None);
472    }
473
474    #[rstest]
475    fn test_order_status_report_with_generated_report_id() {
476        let report = OrderStatusReport::new(
477            AccountId::from("SIM-001"),
478            InstrumentId::from("AUDUSD.SIM"),
479            None,
480            VenueOrderId::from("1"),
481            OrderSide::Buy,
482            OrderType::Market,
483            TimeInForce::Ioc,
484            OrderStatus::Filled,
485            Quantity::from("100"),
486            Quantity::from("100"),
487            UnixNanos::from(1_000_000_000),
488            UnixNanos::from(2_000_000_000),
489            UnixNanos::from(3_000_000_000),
490            None, // No report ID provided, should generate one
491        );
492
493        // Should have a generated UUID
494        assert_ne!(
495            report.report_id.to_string(),
496            "00000000-0000-0000-0000-000000000000"
497        );
498    }
499
500    #[rstest]
501    fn test_order_status_report_builder_methods() -> anyhow::Result<()> {
502        let report = test_order_status_report()
503            .with_client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
504            .with_order_list_id(OrderListId::from("OL-001"))
505            .with_venue_position_id(PositionId::from("P-001"))
506            .with_parent_order_id(ClientOrderId::from("O-PARENT"))
507            .with_price(Price::from("1.00000"))
508            .with_avg_px(1.00001)?
509            .with_trigger_price(Price::from("0.99000"))
510            .with_trigger_type(TriggerType::Default)
511            .with_limit_offset(Decimal::from_f64_retain(0.0001).unwrap())
512            .with_trailing_offset(Decimal::from_f64_retain(0.0002).unwrap())
513            .with_trailing_offset_type(TrailingOffsetType::BasisPoints)
514            .with_display_qty(Quantity::from("50"))
515            .with_expire_time(UnixNanos::from(4_000_000_000))
516            .with_post_only(true)
517            .with_reduce_only(true)
518            .with_cancel_reason("User requested".to_string())
519            .with_ts_triggered(UnixNanos::from(1_500_000_000))
520            .with_contingency_type(ContingencyType::Oco);
521
522        assert_eq!(
523            report.client_order_id,
524            Some(ClientOrderId::from("O-19700101-000000-001-001-2"))
525        );
526        assert_eq!(report.order_list_id, Some(OrderListId::from("OL-001")));
527        assert_eq!(report.venue_position_id, Some(PositionId::from("P-001")));
528        assert_eq!(
529            report.parent_order_id,
530            Some(ClientOrderId::from("O-PARENT"))
531        );
532        assert_eq!(report.price, Some(Price::from("1.00000")));
533        assert_eq!(
534            report.avg_px,
535            Some(Decimal::from_f64_retain(1.00001).unwrap())
536        );
537        assert_eq!(report.trigger_price, Some(Price::from("0.99000")));
538        assert_eq!(report.trigger_type, Some(TriggerType::Default));
539        assert_eq!(
540            report.limit_offset,
541            Some(Decimal::from_f64_retain(0.0001).unwrap())
542        );
543        assert_eq!(
544            report.trailing_offset,
545            Some(Decimal::from_f64_retain(0.0002).unwrap())
546        );
547        assert_eq!(report.trailing_offset_type, TrailingOffsetType::BasisPoints);
548        assert_eq!(report.display_qty, Some(Quantity::from("50")));
549        assert_eq!(report.expire_time, Some(UnixNanos::from(4_000_000_000)));
550        assert!(report.post_only);
551        assert!(report.reduce_only);
552        assert_eq!(report.cancel_reason, Some("User requested".to_string()));
553        assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
554        assert_eq!(report.contingency_type, ContingencyType::Oco);
555        Ok(())
556    }
557
558    #[rstest]
559    fn test_display() {
560        let report = test_order_status_report();
561        let display_str = format!("{report}");
562
563        assert!(display_str.contains("OrderStatusReport"));
564        assert!(display_str.contains("SIM-001"));
565        assert!(display_str.contains("AUDUSD.SIM"));
566        assert!(display_str.contains("BUY"));
567        assert!(display_str.contains("LIMIT"));
568        assert!(display_str.contains("GTC"));
569        assert!(display_str.contains("ACCEPTED"));
570        assert!(display_str.contains("100"));
571    }
572
573    #[rstest]
574    fn test_clone_and_equality() {
575        let report1 = test_order_status_report();
576        let report2 = report1.clone();
577
578        assert_eq!(report1, report2);
579    }
580
581    #[rstest]
582    fn test_serialization_roundtrip() {
583        let original = test_order_status_report();
584
585        // Test JSON serialization
586        let json = serde_json::to_string(&original).unwrap();
587        let deserialized: OrderStatusReport = serde_json::from_str(&json).unwrap();
588        assert_eq!(original, deserialized);
589    }
590
591    #[rstest]
592    fn test_order_status_report_different_order_types() {
593        let market_report = OrderStatusReport::new(
594            AccountId::from("SIM-001"),
595            InstrumentId::from("AUDUSD.SIM"),
596            None,
597            VenueOrderId::from("1"),
598            OrderSide::Buy,
599            OrderType::Market,
600            TimeInForce::Ioc,
601            OrderStatus::Filled,
602            Quantity::from("100"),
603            Quantity::from("100"),
604            UnixNanos::from(1_000_000_000),
605            UnixNanos::from(2_000_000_000),
606            UnixNanos::from(3_000_000_000),
607            None,
608        );
609
610        let stop_report = OrderStatusReport::new(
611            AccountId::from("SIM-001"),
612            InstrumentId::from("AUDUSD.SIM"),
613            None,
614            VenueOrderId::from("2"),
615            OrderSide::Sell,
616            OrderType::StopMarket,
617            TimeInForce::Gtc,
618            OrderStatus::Accepted,
619            Quantity::from("50"),
620            Quantity::from("0"),
621            UnixNanos::from(1_000_000_000),
622            UnixNanos::from(2_000_000_000),
623            UnixNanos::from(3_000_000_000),
624            None,
625        );
626
627        assert_eq!(market_report.order_type, OrderType::Market);
628        assert_eq!(stop_report.order_type, OrderType::StopMarket);
629        assert_ne!(market_report, stop_report);
630    }
631
632    #[rstest]
633    fn test_order_status_report_different_statuses() {
634        let accepted_report = test_order_status_report();
635
636        let filled_report = OrderStatusReport::new(
637            AccountId::from("SIM-001"),
638            InstrumentId::from("AUDUSD.SIM"),
639            Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
640            VenueOrderId::from("1"),
641            OrderSide::Buy,
642            OrderType::Limit,
643            TimeInForce::Gtc,
644            OrderStatus::Filled,
645            Quantity::from("100"),
646            Quantity::from("100"), // Fully filled
647            UnixNanos::from(1_000_000_000),
648            UnixNanos::from(2_000_000_000),
649            UnixNanos::from(3_000_000_000),
650            None,
651        );
652
653        assert_eq!(accepted_report.order_status, OrderStatus::Accepted);
654        assert_eq!(filled_report.order_status, OrderStatus::Filled);
655        assert_ne!(accepted_report, filled_report);
656    }
657
658    #[rstest]
659    fn test_order_status_report_with_optional_fields() -> anyhow::Result<()> {
660        let mut report = test_order_status_report();
661
662        // Initially no optional fields set
663        assert_eq!(report.price, None);
664        assert_eq!(report.avg_px, None);
665        assert!(!report.post_only);
666        assert!(!report.reduce_only);
667
668        // Test builder pattern with various optional fields
669        report = report
670            .with_price(Price::from("1.00000"))
671            .with_avg_px(1.00001)?
672            .with_post_only(true)
673            .with_reduce_only(true);
674
675        assert_eq!(report.price, Some(Price::from("1.00000")));
676        assert_eq!(
677            report.avg_px,
678            Some(Decimal::from_f64_retain(1.00001).unwrap())
679        );
680        assert!(report.post_only);
681        assert!(report.reduce_only);
682        Ok(())
683    }
684
685    #[rstest]
686    fn test_order_status_report_partial_fill() {
687        let partial_fill_report = OrderStatusReport::new(
688            AccountId::from("SIM-001"),
689            InstrumentId::from("AUDUSD.SIM"),
690            Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
691            VenueOrderId::from("1"),
692            OrderSide::Buy,
693            OrderType::Limit,
694            TimeInForce::Gtc,
695            OrderStatus::PartiallyFilled,
696            Quantity::from("100"),
697            Quantity::from("30"), // Partially filled
698            UnixNanos::from(1_000_000_000),
699            UnixNanos::from(2_000_000_000),
700            UnixNanos::from(3_000_000_000),
701            None,
702        );
703
704        assert_eq!(partial_fill_report.quantity, Quantity::from("100"));
705        assert_eq!(partial_fill_report.filled_qty, Quantity::from("30"));
706        assert_eq!(
707            partial_fill_report.order_status,
708            OrderStatus::PartiallyFilled
709        );
710    }
711
712    #[rstest]
713    fn test_order_status_report_with_all_timestamp_fields() {
714        let report = OrderStatusReport::new(
715            AccountId::from("SIM-001"),
716            InstrumentId::from("AUDUSD.SIM"),
717            None,
718            VenueOrderId::from("1"),
719            OrderSide::Buy,
720            OrderType::StopLimit,
721            TimeInForce::Gtc,
722            OrderStatus::Triggered,
723            Quantity::from("100"),
724            Quantity::from("0"),
725            UnixNanos::from(1_000_000_000), // ts_accepted
726            UnixNanos::from(2_000_000_000), // ts_last
727            UnixNanos::from(3_000_000_000), // ts_init
728            None,
729        )
730        .with_ts_triggered(UnixNanos::from(1_500_000_000));
731
732        assert_eq!(report.ts_accepted, UnixNanos::from(1_000_000_000));
733        assert_eq!(report.ts_last, UnixNanos::from(2_000_000_000));
734        assert_eq!(report.ts_init, UnixNanos::from(3_000_000_000));
735        assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
736    }
737}