nautilus_model/reports/
mass_status.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 indexmap::IndexMap;
19use nautilus_core::{UUID4, UnixNanos};
20use serde::{Deserialize, Serialize};
21
22use crate::{
23    identifiers::{AccountId, ClientId, InstrumentId, Venue, VenueOrderId},
24    reports::{fill::FillReport, order::OrderStatusReport, position::PositionStatusReport},
25};
26
27/// Represents an execution mass status report for an execution client - including
28/// status of all orders, trades for those orders and open positions.
29#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30#[serde(tag = "type")]
31#[cfg_attr(
32    feature = "python",
33    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
34)]
35pub struct ExecutionMassStatus {
36    /// The client ID for the report.
37    pub client_id: ClientId,
38    /// The account ID for the report.
39    pub account_id: AccountId,
40    /// The venue for the report.
41    pub venue: Venue,
42    /// The report ID.
43    pub report_id: UUID4,
44    /// UNIX timestamp (nanoseconds) when the object was initialized.
45    pub ts_init: UnixNanos,
46    /// The order status reports.
47    order_reports: IndexMap<VenueOrderId, OrderStatusReport>,
48    /// The fill reports.
49    fill_reports: IndexMap<VenueOrderId, Vec<FillReport>>,
50    /// The position status reports.
51    position_reports: IndexMap<InstrumentId, Vec<PositionStatusReport>>,
52}
53
54impl ExecutionMassStatus {
55    /// Creates a new execution mass status report.
56    #[must_use]
57    pub fn new(
58        client_id: ClientId,
59        account_id: AccountId,
60        venue: Venue,
61        ts_init: UnixNanos,
62        report_id: Option<UUID4>,
63    ) -> Self {
64        Self {
65            client_id,
66            account_id,
67            venue,
68            report_id: report_id.unwrap_or_default(),
69            ts_init,
70            order_reports: IndexMap::new(),
71            fill_reports: IndexMap::new(),
72            position_reports: IndexMap::new(),
73        }
74    }
75
76    /// Get a copy of the order reports map.
77    #[must_use]
78    pub fn order_reports(&self) -> IndexMap<VenueOrderId, OrderStatusReport> {
79        self.order_reports.clone()
80    }
81
82    /// Get a copy of the fill reports map.
83    #[must_use]
84    pub fn fill_reports(&self) -> IndexMap<VenueOrderId, Vec<FillReport>> {
85        self.fill_reports.clone()
86    }
87
88    /// Get a copy of the position reports map.
89    #[must_use]
90    pub fn position_reports(&self) -> IndexMap<InstrumentId, Vec<PositionStatusReport>> {
91        self.position_reports.clone()
92    }
93
94    /// Add order reports to the mass status.
95    pub fn add_order_reports(&mut self, reports: Vec<OrderStatusReport>) {
96        for report in reports {
97            self.order_reports.insert(report.venue_order_id, report);
98        }
99    }
100
101    /// Add fill reports to the mass status.
102    pub fn add_fill_reports(&mut self, reports: Vec<FillReport>) {
103        for report in reports {
104            self.fill_reports
105                .entry(report.venue_order_id)
106                .or_default()
107                .push(report);
108        }
109    }
110
111    /// Add position reports to the mass status.
112    pub fn add_position_reports(&mut self, reports: Vec<PositionStatusReport>) {
113        for report in reports {
114            self.position_reports
115                .entry(report.instrument_id)
116                .or_default()
117                .push(report);
118        }
119    }
120}
121
122impl Display for ExecutionMassStatus {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        write!(
125            f,
126            "ExecutionMassStatus(client_id={}, account_id={}, venue={}, order_reports={:?}, fill_reports={:?}, position_reports={:?}, report_id={}, ts_init={})",
127            self.client_id,
128            self.account_id,
129            self.venue,
130            self.order_reports,
131            self.fill_reports,
132            self.position_reports,
133            self.report_id,
134            self.ts_init,
135        )
136    }
137}
138
139////////////////////////////////////////////////////////////////////////////////
140// Tests
141////////////////////////////////////////////////////////////////////////////////
142#[cfg(test)]
143mod tests {
144    use nautilus_core::UnixNanos;
145    use rstest::*;
146
147    use super::*;
148    use crate::{
149        enums::{
150            LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSideSpecified, TimeInForce,
151        },
152        identifiers::{
153            AccountId, ClientId, InstrumentId, PositionId, TradeId, Venue, VenueOrderId,
154        },
155        reports::{fill::FillReport, order::OrderStatusReport, position::PositionStatusReport},
156        types::{Currency, Money, Price, Quantity},
157    };
158
159    fn test_execution_mass_status() -> ExecutionMassStatus {
160        ExecutionMassStatus::new(
161            ClientId::from("IB"),
162            AccountId::from("IB-DU123456"),
163            Venue::from("NASDAQ"),
164            UnixNanos::from(1_000_000_000),
165            None,
166        )
167    }
168
169    fn create_test_order_report() -> OrderStatusReport {
170        OrderStatusReport::new(
171            AccountId::from("IB-DU123456"),
172            InstrumentId::from("AAPL.NASDAQ"),
173            None,
174            VenueOrderId::from("1"),
175            OrderSide::Buy,
176            OrderType::Limit,
177            TimeInForce::Gtc,
178            OrderStatus::Accepted,
179            Quantity::from("100"),
180            Quantity::from("0"),
181            UnixNanos::from(1_000_000_000),
182            UnixNanos::from(2_000_000_000),
183            UnixNanos::from(3_000_000_000),
184            None,
185        )
186    }
187
188    fn create_test_fill_report() -> FillReport {
189        FillReport::new(
190            AccountId::from("IB-DU123456"),
191            InstrumentId::from("AAPL.NASDAQ"),
192            VenueOrderId::from("1"),
193            TradeId::from("T-001"),
194            OrderSide::Buy,
195            Quantity::from("50"),
196            Price::from("150.00"),
197            Money::new(1.0, Currency::USD()),
198            LiquiditySide::Taker,
199            None,
200            None,
201            UnixNanos::from(1_500_000_000),
202            UnixNanos::from(2_500_000_000),
203            None,
204        )
205    }
206
207    fn create_test_position_report() -> PositionStatusReport {
208        PositionStatusReport::new(
209            AccountId::from("IB-DU123456"),
210            InstrumentId::from("AAPL.NASDAQ"),
211            PositionSideSpecified::Long,
212            Quantity::from("50"),
213            UnixNanos::from(2_000_000_000),
214            UnixNanos::from(3_000_000_000),
215            None,                            // report_id
216            Some(PositionId::from("P-001")), // venue_position_id
217            None,                            // avg_px_open
218        )
219    }
220
221    #[rstest]
222    fn test_execution_mass_status_new() {
223        let mass_status = test_execution_mass_status();
224
225        assert_eq!(mass_status.client_id, ClientId::from("IB"));
226        assert_eq!(mass_status.account_id, AccountId::from("IB-DU123456"));
227        assert_eq!(mass_status.venue, Venue::from("NASDAQ"));
228        assert_eq!(mass_status.ts_init, UnixNanos::from(1_000_000_000));
229        assert!(mass_status.order_reports().is_empty());
230        assert!(mass_status.fill_reports().is_empty());
231        assert!(mass_status.position_reports().is_empty());
232    }
233
234    #[rstest]
235    fn test_execution_mass_status_with_generated_report_id() {
236        let mass_status = ExecutionMassStatus::new(
237            ClientId::from("IB"),
238            AccountId::from("IB-DU123456"),
239            Venue::from("NASDAQ"),
240            UnixNanos::from(1_000_000_000),
241            None, // No report ID provided, should generate one
242        );
243
244        // Should have a generated UUID
245        assert_ne!(
246            mass_status.report_id.to_string(),
247            "00000000-0000-0000-0000-000000000000"
248        );
249    }
250
251    #[rstest]
252    fn test_add_order_reports() {
253        let mut mass_status = test_execution_mass_status();
254        let order_report1 = create_test_order_report();
255        let order_report2 = OrderStatusReport::new(
256            AccountId::from("IB-DU123456"),
257            InstrumentId::from("MSFT.NASDAQ"),
258            None,
259            VenueOrderId::from("2"),
260            OrderSide::Sell,
261            OrderType::Market,
262            TimeInForce::Ioc,
263            OrderStatus::Filled,
264            Quantity::from("200"),
265            Quantity::from("200"),
266            UnixNanos::from(1_000_000_000),
267            UnixNanos::from(2_000_000_000),
268            UnixNanos::from(3_000_000_000),
269            None,
270        );
271
272        mass_status.add_order_reports(vec![order_report1.clone(), order_report2.clone()]);
273
274        let order_reports = mass_status.order_reports();
275        assert_eq!(order_reports.len(), 2);
276        assert_eq!(
277            order_reports.get(&VenueOrderId::from("1")),
278            Some(&order_report1)
279        );
280        assert_eq!(
281            order_reports.get(&VenueOrderId::from("2")),
282            Some(&order_report2)
283        );
284    }
285
286    #[rstest]
287    fn test_add_fill_reports() {
288        let mut mass_status = test_execution_mass_status();
289        let fill_report1 = create_test_fill_report();
290        let fill_report2 = FillReport::new(
291            AccountId::from("IB-DU123456"),
292            InstrumentId::from("AAPL.NASDAQ"),
293            VenueOrderId::from("1"), // Same venue order ID
294            TradeId::from("T-002"),
295            OrderSide::Buy,
296            Quantity::from("50"),
297            Price::from("151.00"),
298            Money::new(1.5, Currency::USD()),
299            LiquiditySide::Maker,
300            None,
301            None,
302            UnixNanos::from(1_600_000_000),
303            UnixNanos::from(2_600_000_000),
304            None,
305        );
306
307        mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
308
309        let fill_reports = mass_status.fill_reports();
310        assert_eq!(fill_reports.len(), 1); // One entry because same venue order ID
311
312        let fills_for_order = fill_reports.get(&VenueOrderId::from("1")).unwrap();
313        assert_eq!(fills_for_order.len(), 2);
314        assert_eq!(fills_for_order[0], fill_report1);
315        assert_eq!(fills_for_order[1], fill_report2);
316    }
317
318    #[rstest]
319    fn test_add_position_reports() {
320        let mut mass_status = test_execution_mass_status();
321        let position_report1 = create_test_position_report();
322        let position_report2 = PositionStatusReport::new(
323            AccountId::from("IB-DU123456"),
324            InstrumentId::from("AAPL.NASDAQ"), // Same instrument ID
325            PositionSideSpecified::Short,
326            Quantity::from("25"),
327            UnixNanos::from(2_100_000_000),
328            UnixNanos::from(3_100_000_000),
329            None,
330            None,
331            None,
332        );
333        let position_report3 = PositionStatusReport::new(
334            AccountId::from("IB-DU123456"),
335            InstrumentId::from("MSFT.NASDAQ"), // Different instrument
336            PositionSideSpecified::Long,
337            Quantity::from("100"),
338            UnixNanos::from(2_200_000_000),
339            UnixNanos::from(3_200_000_000),
340            None,
341            None,
342            None,
343        );
344
345        mass_status.add_position_reports(vec![
346            position_report1.clone(),
347            position_report2.clone(),
348            position_report3.clone(),
349        ]);
350
351        let position_reports = mass_status.position_reports();
352        assert_eq!(position_reports.len(), 2); // Two instruments
353
354        // Check AAPL positions
355        let aapl_positions = position_reports
356            .get(&InstrumentId::from("AAPL.NASDAQ"))
357            .unwrap();
358        assert_eq!(aapl_positions.len(), 2);
359        assert_eq!(aapl_positions[0], position_report1);
360        assert_eq!(aapl_positions[1], position_report2);
361
362        // Check MSFT positions
363        let msft_positions = position_reports
364            .get(&InstrumentId::from("MSFT.NASDAQ"))
365            .unwrap();
366        assert_eq!(msft_positions.len(), 1);
367        assert_eq!(msft_positions[0], position_report3);
368    }
369
370    #[rstest]
371    fn test_add_multiple_fills_for_different_orders() {
372        let mut mass_status = test_execution_mass_status();
373        let fill_report1 = create_test_fill_report(); // venue_order_id = "1"
374        let fill_report2 = FillReport::new(
375            AccountId::from("IB-DU123456"),
376            InstrumentId::from("MSFT.NASDAQ"),
377            VenueOrderId::from("2"), // Different venue order ID
378            TradeId::from("T-003"),
379            OrderSide::Sell,
380            Quantity::from("75"),
381            Price::from("300.00"),
382            Money::new(2.0, Currency::USD()),
383            LiquiditySide::Taker,
384            None,
385            None,
386            UnixNanos::from(1_700_000_000),
387            UnixNanos::from(2_700_000_000),
388            None,
389        );
390
391        mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
392
393        let fill_reports = mass_status.fill_reports();
394        assert_eq!(fill_reports.len(), 2); // Two different venue order IDs
395
396        let fills_order_1 = fill_reports.get(&VenueOrderId::from("1")).unwrap();
397        assert_eq!(fills_order_1.len(), 1);
398        assert_eq!(fills_order_1[0], fill_report1);
399
400        let fills_order_2 = fill_reports.get(&VenueOrderId::from("2")).unwrap();
401        assert_eq!(fills_order_2.len(), 1);
402        assert_eq!(fills_order_2[0], fill_report2);
403    }
404
405    #[rstest]
406    fn test_comprehensive_mass_status() {
407        let mut mass_status = test_execution_mass_status();
408
409        // Add various reports
410        let order_report = create_test_order_report();
411        let fill_report = create_test_fill_report();
412        let position_report = create_test_position_report();
413
414        mass_status.add_order_reports(vec![order_report.clone()]);
415        mass_status.add_fill_reports(vec![fill_report.clone()]);
416        mass_status.add_position_reports(vec![position_report.clone()]);
417
418        // Verify all reports are present
419        assert_eq!(mass_status.order_reports().len(), 1);
420        assert_eq!(mass_status.fill_reports().len(), 1);
421        assert_eq!(mass_status.position_reports().len(), 1);
422
423        // Verify specific content
424        assert_eq!(
425            mass_status.order_reports().get(&VenueOrderId::from("1")),
426            Some(&order_report)
427        );
428        assert_eq!(
429            mass_status
430                .fill_reports()
431                .get(&VenueOrderId::from("1"))
432                .unwrap()[0],
433            fill_report
434        );
435        assert_eq!(
436            mass_status
437                .position_reports()
438                .get(&InstrumentId::from("AAPL.NASDAQ"))
439                .unwrap()[0],
440            position_report
441        );
442    }
443
444    #[rstest]
445    fn test_display() {
446        let mass_status = test_execution_mass_status();
447        let display_str = format!("{mass_status}");
448
449        assert!(display_str.contains("ExecutionMassStatus"));
450        assert!(display_str.contains("IB"));
451        assert!(display_str.contains("IB-DU123456"));
452        assert!(display_str.contains("NASDAQ"));
453    }
454
455    #[rstest]
456    fn test_clone_and_equality() {
457        let mass_status1 = test_execution_mass_status();
458        let mass_status2 = mass_status1.clone();
459
460        assert_eq!(mass_status1, mass_status2);
461    }
462
463    #[rstest]
464    fn test_serialization_roundtrip() {
465        let original = test_execution_mass_status();
466
467        // Test JSON serialization
468        let json = serde_json::to_string(&original).unwrap();
469        let deserialized: ExecutionMassStatus = serde_json::from_str(&json).unwrap();
470        assert_eq!(original, deserialized);
471    }
472
473    #[rstest]
474    fn test_empty_mass_status_accessors() {
475        let mass_status = test_execution_mass_status();
476
477        // All collections should be empty initially
478        assert!(mass_status.order_reports().is_empty());
479        assert!(mass_status.fill_reports().is_empty());
480        assert!(mass_status.position_reports().is_empty());
481    }
482
483    #[rstest]
484    fn test_add_empty_reports() {
485        let mut mass_status = test_execution_mass_status();
486
487        // Adding empty vectors should work without issues
488        mass_status.add_order_reports(vec![]);
489        mass_status.add_fill_reports(vec![]);
490        mass_status.add_position_reports(vec![]);
491
492        // Should still be empty
493        assert!(mass_status.order_reports().is_empty());
494        assert!(mass_status.fill_reports().is_empty());
495        assert!(mass_status.position_reports().is_empty());
496    }
497
498    #[rstest]
499    fn test_overwrite_order_reports() {
500        let mut mass_status = test_execution_mass_status();
501        let venue_order_id = VenueOrderId::from("1");
502
503        // Add first order report
504        let order_report1 = create_test_order_report();
505        mass_status.add_order_reports(vec![order_report1.clone()]);
506
507        // Add second order report with same venue order ID (should overwrite)
508        let order_report2 = OrderStatusReport::new(
509            AccountId::from("IB-DU123456"),
510            InstrumentId::from("AAPL.NASDAQ"),
511            None,
512            venue_order_id,
513            OrderSide::Sell, // Different side
514            OrderType::Market,
515            TimeInForce::Ioc,
516            OrderStatus::Filled,
517            Quantity::from("200"),
518            Quantity::from("200"),
519            UnixNanos::from(1_000_000_000),
520            UnixNanos::from(2_000_000_000),
521            UnixNanos::from(3_000_000_000),
522            None,
523        );
524        mass_status.add_order_reports(vec![order_report2.clone()]);
525
526        // Should have only one report (the latest one)
527        let order_reports = mass_status.order_reports();
528        assert_eq!(order_reports.len(), 1);
529        assert_eq!(order_reports.get(&venue_order_id), Some(&order_report2));
530        assert_ne!(order_reports.get(&venue_order_id), Some(&order_report1));
531    }
532}