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