1use 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30#[serde(tag = "type")]
31#[cfg_attr(
32 feature = "python",
33 pyo3::pyclass(module = "nautilus_trader.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 pub client_id: ClientId,
42 pub account_id: AccountId,
44 pub venue: Venue,
46 pub report_id: UUID4,
48 pub ts_init: UnixNanos,
50 #[serde(default)]
52 lookback_start: Option<UnixNanos>,
53 #[serde(default = "default_true")]
55 reports_complete: bool,
56 order_reports: IndexMap<VenueOrderId, OrderStatusReport>,
58 fill_reports: IndexMap<VenueOrderId, Vec<FillReport>>,
60 position_reports: IndexMap<InstrumentId, Vec<PositionStatusReport>>,
62}
63
64impl ExecutionMassStatus {
65 #[must_use]
67 pub fn new(
68 client_id: ClientId,
69 account_id: AccountId,
70 venue: Venue,
71 ts_init: UnixNanos,
72 report_id: Option<UUID4>,
73 ) -> Self {
74 Self {
75 client_id,
76 account_id,
77 venue,
78 report_id: report_id.unwrap_or_default(),
79 ts_init,
80 lookback_start: None,
81 reports_complete: true,
82 order_reports: IndexMap::new(),
83 fill_reports: IndexMap::new(),
84 position_reports: IndexMap::new(),
85 }
86 }
87
88 #[must_use]
90 pub fn order_reports(&self) -> IndexMap<VenueOrderId, OrderStatusReport> {
91 self.order_reports.clone()
92 }
93
94 #[must_use]
96 pub fn fill_reports(&self) -> IndexMap<VenueOrderId, Vec<FillReport>> {
97 self.fill_reports.clone()
98 }
99
100 #[must_use]
102 pub fn position_reports(&self) -> IndexMap<InstrumentId, Vec<PositionStatusReport>> {
103 self.position_reports.clone()
104 }
105
106 #[must_use]
108 pub const fn lookback_start(&self) -> Option<UnixNanos> {
109 self.lookback_start
110 }
111
112 #[must_use]
114 pub const fn reports_complete(&self) -> bool {
115 self.reports_complete
116 }
117
118 pub const fn set_report_window(
120 &mut self,
121 lookback_start: Option<UnixNanos>,
122 reports_complete: bool,
123 ) {
124 self.lookback_start = lookback_start;
125 self.reports_complete = reports_complete;
126 }
127
128 pub fn add_order_reports(&mut self, reports: Vec<OrderStatusReport>) {
130 for report in reports {
131 self.order_reports.insert(report.venue_order_id, report);
132 }
133 }
134
135 pub fn add_fill_reports(&mut self, reports: Vec<FillReport>) {
137 for report in reports {
138 self.fill_reports
139 .entry(report.venue_order_id)
140 .or_default()
141 .push(report);
142 }
143 }
144
145 pub fn add_position_reports(&mut self, reports: Vec<PositionStatusReport>) {
147 for report in reports {
148 self.position_reports
149 .entry(report.instrument_id)
150 .or_default()
151 .push(report);
152 }
153 }
154}
155
156const fn default_true() -> bool {
157 true
158}
159
160impl Display for ExecutionMassStatus {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 write!(
163 f,
164 "ExecutionMassStatus(client_id={}, account_id={}, venue={}, order_reports={:?}, fill_reports={:?}, position_reports={:?}, report_id={}, ts_init={})",
165 self.client_id,
166 self.account_id,
167 self.venue,
168 self.order_reports,
169 self.fill_reports,
170 self.position_reports,
171 self.report_id,
172 self.ts_init,
173 )
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use nautilus_core::UnixNanos;
180 use rstest::*;
181
182 use super::*;
183 use crate::{
184 enums::{LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce},
185 identifiers::{
186 AccountId, ClientId, InstrumentId, PositionId, TradeId, Venue, VenueOrderId,
187 },
188 reports::{fill::FillReport, order::OrderStatusReport, position::PositionStatusReport},
189 types::{Currency, Money, Price, Quantity},
190 };
191
192 fn test_execution_mass_status() -> ExecutionMassStatus {
193 ExecutionMassStatus::new(
194 ClientId::from("IB"),
195 AccountId::from("IB-DU123456"),
196 Venue::from("NASDAQ"),
197 UnixNanos::from(1_000_000_000),
198 None,
199 )
200 }
201
202 fn create_test_order_report() -> OrderStatusReport {
203 OrderStatusReport::new(
204 AccountId::from("IB-DU123456"),
205 InstrumentId::from("AAPL.NASDAQ"),
206 None,
207 VenueOrderId::from("1"),
208 OrderSide::Buy.into(),
209 OrderType::Limit,
210 TimeInForce::Gtc,
211 OrderStatus::Accepted,
212 Quantity::from("100"),
213 Quantity::from("0"),
214 UnixNanos::from(1_000_000_000),
215 UnixNanos::from(2_000_000_000),
216 UnixNanos::from(3_000_000_000),
217 None,
218 )
219 }
220
221 fn create_test_fill_report() -> FillReport {
222 FillReport::new(
223 AccountId::from("IB-DU123456"),
224 InstrumentId::from("AAPL.NASDAQ"),
225 VenueOrderId::from("1"),
226 TradeId::from("T-001"),
227 OrderSide::Buy,
228 Quantity::from("50"),
229 Price::from("150.00"),
230 Money::new(1.0, Currency::USD()),
231 LiquiditySide::Taker,
232 None,
233 None,
234 UnixNanos::from(1_500_000_000),
235 UnixNanos::from(2_500_000_000),
236 None,
237 )
238 }
239
240 fn create_test_position_report() -> PositionStatusReport {
241 PositionStatusReport::new(
242 AccountId::from("IB-DU123456"),
243 InstrumentId::from("AAPL.NASDAQ"),
244 PositionSide::Long,
245 Quantity::from("50"),
246 UnixNanos::from(2_000_000_000),
247 UnixNanos::from(3_000_000_000),
248 None, Some(PositionId::from("P-001")), None, )
252 }
253
254 #[rstest]
255 fn test_execution_mass_status_new() {
256 let mass_status = test_execution_mass_status();
257
258 assert_eq!(mass_status.client_id, ClientId::from("IB"));
259 assert_eq!(mass_status.account_id, AccountId::from("IB-DU123456"));
260 assert_eq!(mass_status.venue, Venue::from("NASDAQ"));
261 assert_eq!(mass_status.ts_init, UnixNanos::from(1_000_000_000));
262 assert_eq!(mass_status.lookback_start(), None);
263 assert!(mass_status.reports_complete());
264 assert!(mass_status.order_reports().is_empty());
265 assert!(mass_status.fill_reports().is_empty());
266 assert!(mass_status.position_reports().is_empty());
267 }
268
269 #[rstest]
270 fn test_set_report_window() {
271 let mut mass_status = test_execution_mass_status();
272 let lookback_start = UnixNanos::from(500_000_000);
273
274 mass_status.set_report_window(Some(lookback_start), false);
275
276 assert_eq!(mass_status.lookback_start(), Some(lookback_start));
277 assert!(!mass_status.reports_complete());
278 }
279
280 #[rstest]
281 fn test_execution_mass_status_with_generated_report_id() {
282 let mass_status = ExecutionMassStatus::new(
283 ClientId::from("IB"),
284 AccountId::from("IB-DU123456"),
285 Venue::from("NASDAQ"),
286 UnixNanos::from(1_000_000_000),
287 None, );
289
290 assert_ne!(
292 mass_status.report_id.to_string(),
293 "00000000-0000-0000-0000-000000000000"
294 );
295 }
296
297 #[rstest]
298 fn test_add_order_reports() {
299 let mut mass_status = test_execution_mass_status();
300 let order_report1 = create_test_order_report();
301 let order_report2 = OrderStatusReport::new(
302 AccountId::from("IB-DU123456"),
303 InstrumentId::from("MSFT.NASDAQ"),
304 None,
305 VenueOrderId::from("2"),
306 OrderSide::Sell.into(),
307 OrderType::Market,
308 TimeInForce::Ioc,
309 OrderStatus::Filled,
310 Quantity::from("200"),
311 Quantity::from("200"),
312 UnixNanos::from(1_000_000_000),
313 UnixNanos::from(2_000_000_000),
314 UnixNanos::from(3_000_000_000),
315 None,
316 );
317
318 mass_status.add_order_reports(vec![order_report1.clone(), order_report2.clone()]);
319
320 let order_reports = mass_status.order_reports();
321 assert_eq!(order_reports.len(), 2);
322 assert_eq!(
323 order_reports.get(&VenueOrderId::from("1")),
324 Some(&order_report1)
325 );
326 assert_eq!(
327 order_reports.get(&VenueOrderId::from("2")),
328 Some(&order_report2)
329 );
330 }
331
332 #[rstest]
333 fn test_add_fill_reports() {
334 let mut mass_status = test_execution_mass_status();
335 let fill_report1 = create_test_fill_report();
336 let fill_report2 = FillReport::new(
337 AccountId::from("IB-DU123456"),
338 InstrumentId::from("AAPL.NASDAQ"),
339 VenueOrderId::from("1"), TradeId::from("T-002"),
341 OrderSide::Buy,
342 Quantity::from("50"),
343 Price::from("151.00"),
344 Money::new(1.5, Currency::USD()),
345 LiquiditySide::Maker,
346 None,
347 None,
348 UnixNanos::from(1_600_000_000),
349 UnixNanos::from(2_600_000_000),
350 None,
351 );
352
353 mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
354
355 let fill_reports = mass_status.fill_reports();
356 assert_eq!(fill_reports.len(), 1); let fills_for_order = fill_reports.get(&VenueOrderId::from("1")).unwrap();
359 assert_eq!(fills_for_order.len(), 2);
360 assert_eq!(fills_for_order[0], fill_report1);
361 assert_eq!(fills_for_order[1], fill_report2);
362 }
363
364 #[rstest]
365 fn test_add_position_reports() {
366 let mut mass_status = test_execution_mass_status();
367 let position_report1 = create_test_position_report();
368 let position_report2 = PositionStatusReport::new(
369 AccountId::from("IB-DU123456"),
370 InstrumentId::from("AAPL.NASDAQ"), PositionSide::Short,
372 Quantity::from("25"),
373 UnixNanos::from(2_100_000_000),
374 UnixNanos::from(3_100_000_000),
375 None,
376 None,
377 None,
378 );
379 let position_report3 = PositionStatusReport::new(
380 AccountId::from("IB-DU123456"),
381 InstrumentId::from("MSFT.NASDAQ"), PositionSide::Long,
383 Quantity::from("100"),
384 UnixNanos::from(2_200_000_000),
385 UnixNanos::from(3_200_000_000),
386 None,
387 None,
388 None,
389 );
390
391 mass_status.add_position_reports(vec![
392 position_report1.clone(),
393 position_report2.clone(),
394 position_report3.clone(),
395 ]);
396
397 let position_reports = mass_status.position_reports();
398 assert_eq!(position_reports.len(), 2); let aapl_positions = position_reports
402 .get(&InstrumentId::from("AAPL.NASDAQ"))
403 .unwrap();
404 assert_eq!(aapl_positions.len(), 2);
405 assert_eq!(aapl_positions[0], position_report1);
406 assert_eq!(aapl_positions[1], position_report2);
407
408 let msft_positions = position_reports
410 .get(&InstrumentId::from("MSFT.NASDAQ"))
411 .unwrap();
412 assert_eq!(msft_positions.len(), 1);
413 assert_eq!(msft_positions[0], position_report3);
414 }
415
416 #[rstest]
417 fn test_add_multiple_fills_for_different_orders() {
418 let mut mass_status = test_execution_mass_status();
419 let fill_report1 = create_test_fill_report(); let fill_report2 = FillReport::new(
421 AccountId::from("IB-DU123456"),
422 InstrumentId::from("MSFT.NASDAQ"),
423 VenueOrderId::from("2"), TradeId::from("T-003"),
425 OrderSide::Sell,
426 Quantity::from("75"),
427 Price::from("300.00"),
428 Money::new(2.0, Currency::USD()),
429 LiquiditySide::Taker,
430 None,
431 None,
432 UnixNanos::from(1_700_000_000),
433 UnixNanos::from(2_700_000_000),
434 None,
435 );
436
437 mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
438
439 let fill_reports = mass_status.fill_reports();
440 assert_eq!(fill_reports.len(), 2); let fills_order_1 = fill_reports.get(&VenueOrderId::from("1")).unwrap();
443 assert_eq!(fills_order_1.len(), 1);
444 assert_eq!(fills_order_1[0], fill_report1);
445
446 let fills_order_2 = fill_reports.get(&VenueOrderId::from("2")).unwrap();
447 assert_eq!(fills_order_2.len(), 1);
448 assert_eq!(fills_order_2[0], fill_report2);
449 }
450
451 #[rstest]
452 fn test_comprehensive_mass_status() {
453 let mut mass_status = test_execution_mass_status();
454
455 let order_report = create_test_order_report();
457 let fill_report = create_test_fill_report();
458 let position_report = create_test_position_report();
459
460 mass_status.add_order_reports(vec![order_report.clone()]);
461 mass_status.add_fill_reports(vec![fill_report.clone()]);
462 mass_status.add_position_reports(vec![position_report.clone()]);
463
464 assert_eq!(mass_status.order_reports().len(), 1);
466 assert_eq!(mass_status.fill_reports().len(), 1);
467 assert_eq!(mass_status.position_reports().len(), 1);
468
469 assert_eq!(
471 mass_status.order_reports().get(&VenueOrderId::from("1")),
472 Some(&order_report)
473 );
474 assert_eq!(
475 mass_status
476 .fill_reports()
477 .get(&VenueOrderId::from("1"))
478 .unwrap()[0],
479 fill_report
480 );
481 assert_eq!(
482 mass_status
483 .position_reports()
484 .get(&InstrumentId::from("AAPL.NASDAQ"))
485 .unwrap()[0],
486 position_report
487 );
488 }
489
490 #[rstest]
491 fn test_display() {
492 let mass_status = test_execution_mass_status();
493 let display_str = format!("{mass_status}");
494
495 assert!(display_str.contains("ExecutionMassStatus"));
496 assert!(display_str.contains("IB"));
497 assert!(display_str.contains("IB-DU123456"));
498 assert!(display_str.contains("NASDAQ"));
499 }
500
501 #[rstest]
502 fn test_clone_and_equality() {
503 let mass_status1 = test_execution_mass_status();
504 let mass_status2 = mass_status1.clone();
505
506 assert_eq!(mass_status1, mass_status2);
507 }
508
509 #[rstest]
510 fn test_serialization_roundtrip() {
511 let mut original = test_execution_mass_status();
512 original.set_report_window(Some(UnixNanos::from(500_000_000)), false);
513
514 let json = serde_json::to_string(&original).unwrap();
516 let deserialized: ExecutionMassStatus = serde_json::from_str(&json).unwrap();
517 assert_eq!(original, deserialized);
518 }
519
520 #[rstest]
521 fn test_deserialization_defaults_unbounded_report_contract() {
522 let original = test_execution_mass_status();
523 let mut value = serde_json::to_value(original).unwrap();
524 let object = value.as_object_mut().unwrap();
525 object.remove("lookback_start");
526 object.remove("reports_complete");
527
528 let deserialized: ExecutionMassStatus = serde_json::from_value(value).unwrap();
529
530 assert_eq!(deserialized.lookback_start(), None);
531 assert!(deserialized.reports_complete());
532 }
533
534 #[rstest]
535 fn test_empty_mass_status_accessors() {
536 let mass_status = test_execution_mass_status();
537
538 assert!(mass_status.order_reports().is_empty());
540 assert!(mass_status.fill_reports().is_empty());
541 assert!(mass_status.position_reports().is_empty());
542 }
543
544 #[rstest]
545 fn test_add_empty_reports() {
546 let mut mass_status = test_execution_mass_status();
547
548 mass_status.add_order_reports(vec![]);
550 mass_status.add_fill_reports(vec![]);
551 mass_status.add_position_reports(vec![]);
552
553 assert!(mass_status.order_reports().is_empty());
555 assert!(mass_status.fill_reports().is_empty());
556 assert!(mass_status.position_reports().is_empty());
557 }
558
559 #[rstest]
560 fn test_overwrite_order_reports() {
561 let mut mass_status = test_execution_mass_status();
562 let venue_order_id = VenueOrderId::from("1");
563
564 let order_report1 = create_test_order_report();
566 mass_status.add_order_reports(vec![order_report1.clone()]);
567
568 let order_report2 = OrderStatusReport::new(
570 AccountId::from("IB-DU123456"),
571 InstrumentId::from("AAPL.NASDAQ"),
572 None,
573 venue_order_id,
574 OrderSide::Sell.into(), OrderType::Market,
576 TimeInForce::Ioc,
577 OrderStatus::Filled,
578 Quantity::from("200"),
579 Quantity::from("200"),
580 UnixNanos::from(1_000_000_000),
581 UnixNanos::from(2_000_000_000),
582 UnixNanos::from(3_000_000_000),
583 None,
584 );
585 mass_status.add_order_reports(vec![order_report2.clone()]);
586
587 let order_reports = mass_status.order_reports();
589 assert_eq!(order_reports.len(), 1);
590 assert_eq!(order_reports.get(&venue_order_id), Some(&order_report2));
591 assert_ne!(order_reports.get(&venue_order_id), Some(&order_report1));
592 }
593}