Skip to main content

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