1use std::fmt::{Debug, 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 ReportMapDisplay(&self.order_reports),
169 ReportListMapDisplay(&self.fill_reports),
170 ReportListMapDisplay(&self.position_reports),
171 self.report_id,
172 self.ts_init,
173 )
174 }
175}
176
177struct ReportMapDisplay<'a, K, V>(&'a IndexMap<K, V>);
178
179impl<K: Debug, V: Display> Display for ReportMapDisplay<'_, K, V> {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 f.write_str("{")?;
182
183 for (index, (key, value)) in self.0.iter().enumerate() {
184 if index > 0 {
185 f.write_str(", ")?;
186 }
187 write!(f, "{key:?}: {value}")?;
188 }
189 f.write_str("}")
190 }
191}
192
193struct ReportListMapDisplay<'a, K, V>(&'a IndexMap<K, Vec<V>>);
194
195impl<K: Debug, V: Display> Display for ReportListMapDisplay<'_, K, V> {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 f.write_str("{")?;
198
199 for (map_index, (key, values)) in self.0.iter().enumerate() {
200 if map_index > 0 {
201 f.write_str(", ")?;
202 }
203 write!(f, "{key:?}: [")?;
204
205 for (value_index, value) in values.iter().enumerate() {
206 if value_index > 0 {
207 f.write_str(", ")?;
208 }
209 write!(f, "{value}")?;
210 }
211 f.write_str("]")?;
212 }
213 f.write_str("}")
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use nautilus_core::UnixNanos;
220 use rstest::*;
221
222 use super::*;
223 use crate::{
224 enums::{LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce},
225 identifiers::{
226 AccountId, ClientId, InstrumentId, PositionId, TradeId, Venue, VenueOrderId,
227 },
228 reports::{fill::FillReport, order::OrderStatusReport, position::PositionStatusReport},
229 types::{Currency, Money, Price, Quantity},
230 };
231
232 fn test_execution_mass_status() -> ExecutionMassStatus {
233 ExecutionMassStatus::new(
234 ClientId::from("IB"),
235 AccountId::from("IB-DU123456"),
236 Venue::from("NASDAQ"),
237 UnixNanos::from(1_000_000_000),
238 None,
239 )
240 }
241
242 fn create_test_order_report() -> OrderStatusReport {
243 OrderStatusReport::new(
244 AccountId::from("IB-DU123456"),
245 InstrumentId::from("AAPL.NASDAQ"),
246 None,
247 VenueOrderId::from("1"),
248 OrderSide::Buy.into(),
249 OrderType::Limit,
250 TimeInForce::Gtc,
251 OrderStatus::Accepted,
252 Quantity::from("100"),
253 Quantity::from("0"),
254 UnixNanos::from(1_000_000_000),
255 UnixNanos::from(2_000_000_000),
256 UnixNanos::from(3_000_000_000),
257 None,
258 )
259 }
260
261 fn create_test_fill_report() -> FillReport {
262 FillReport::new(
263 AccountId::from("IB-DU123456"),
264 InstrumentId::from("AAPL.NASDAQ"),
265 VenueOrderId::from("1"),
266 TradeId::from("T-001"),
267 OrderSide::Buy,
268 Quantity::from("50"),
269 Price::from("150.00"),
270 Money::new(1.0, Currency::USD()),
271 LiquiditySide::Taker,
272 None,
273 None,
274 UnixNanos::from(1_500_000_000),
275 UnixNanos::from(2_500_000_000),
276 None,
277 )
278 }
279
280 fn create_test_position_report() -> PositionStatusReport {
281 PositionStatusReport::new(
282 AccountId::from("IB-DU123456"),
283 InstrumentId::from("AAPL.NASDAQ"),
284 PositionSide::Long,
285 Quantity::from("50"),
286 UnixNanos::from(2_000_000_000),
287 UnixNanos::from(3_000_000_000),
288 None, Some(PositionId::from("P-001")), None, )
292 }
293
294 #[rstest]
295 fn test_execution_mass_status_new() {
296 let mass_status = test_execution_mass_status();
297
298 assert_eq!(mass_status.client_id, ClientId::from("IB"));
299 assert_eq!(mass_status.account_id, AccountId::from("IB-DU123456"));
300 assert_eq!(mass_status.venue, Venue::from("NASDAQ"));
301 assert_eq!(mass_status.ts_init, UnixNanos::from(1_000_000_000));
302 assert_eq!(mass_status.lookback_start(), None);
303 assert!(mass_status.reports_complete());
304 assert!(mass_status.order_reports().is_empty());
305 assert!(mass_status.fill_reports().is_empty());
306 assert!(mass_status.position_reports().is_empty());
307 }
308
309 #[rstest]
310 fn test_set_report_window() {
311 let mut mass_status = test_execution_mass_status();
312 let lookback_start = UnixNanos::from(500_000_000);
313
314 mass_status.set_report_window(Some(lookback_start), false);
315
316 assert_eq!(mass_status.lookback_start(), Some(lookback_start));
317 assert!(!mass_status.reports_complete());
318 }
319
320 #[rstest]
321 fn test_execution_mass_status_with_generated_report_id() {
322 let mass_status = ExecutionMassStatus::new(
323 ClientId::from("IB"),
324 AccountId::from("IB-DU123456"),
325 Venue::from("NASDAQ"),
326 UnixNanos::from(1_000_000_000),
327 None, );
329
330 assert_ne!(
332 mass_status.report_id.to_string(),
333 "00000000-0000-0000-0000-000000000000"
334 );
335 }
336
337 #[rstest]
338 fn test_add_order_reports() {
339 let mut mass_status = test_execution_mass_status();
340 let order_report1 = create_test_order_report();
341 let order_report2 = OrderStatusReport::new(
342 AccountId::from("IB-DU123456"),
343 InstrumentId::from("MSFT.NASDAQ"),
344 None,
345 VenueOrderId::from("2"),
346 OrderSide::Sell.into(),
347 OrderType::Market,
348 TimeInForce::Ioc,
349 OrderStatus::Filled,
350 Quantity::from("200"),
351 Quantity::from("200"),
352 UnixNanos::from(1_000_000_000),
353 UnixNanos::from(2_000_000_000),
354 UnixNanos::from(3_000_000_000),
355 None,
356 );
357
358 mass_status.add_order_reports(vec![order_report1.clone(), order_report2.clone()]);
359
360 let order_reports = mass_status.order_reports();
361 assert_eq!(order_reports.len(), 2);
362 assert_eq!(
363 order_reports.get(&VenueOrderId::from("1")),
364 Some(&order_report1)
365 );
366 assert_eq!(
367 order_reports.get(&VenueOrderId::from("2")),
368 Some(&order_report2)
369 );
370 }
371
372 #[rstest]
373 fn test_add_fill_reports() {
374 let mut mass_status = test_execution_mass_status();
375 let fill_report1 = create_test_fill_report();
376 let fill_report2 = FillReport::new(
377 AccountId::from("IB-DU123456"),
378 InstrumentId::from("AAPL.NASDAQ"),
379 VenueOrderId::from("1"), TradeId::from("T-002"),
381 OrderSide::Buy,
382 Quantity::from("50"),
383 Price::from("151.00"),
384 Money::new(1.5, Currency::USD()),
385 LiquiditySide::Maker,
386 None,
387 None,
388 UnixNanos::from(1_600_000_000),
389 UnixNanos::from(2_600_000_000),
390 None,
391 );
392
393 mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
394
395 let fill_reports = mass_status.fill_reports();
396 assert_eq!(fill_reports.len(), 1); let fills_for_order = fill_reports.get(&VenueOrderId::from("1")).unwrap();
399 assert_eq!(fills_for_order.len(), 2);
400 assert_eq!(fills_for_order[0], fill_report1);
401 assert_eq!(fills_for_order[1], fill_report2);
402 }
403
404 #[rstest]
405 fn test_add_position_reports() {
406 let mut mass_status = test_execution_mass_status();
407 let position_report1 = create_test_position_report();
408 let position_report2 = PositionStatusReport::new(
409 AccountId::from("IB-DU123456"),
410 InstrumentId::from("AAPL.NASDAQ"), PositionSide::Short,
412 Quantity::from("25"),
413 UnixNanos::from(2_100_000_000),
414 UnixNanos::from(3_100_000_000),
415 None,
416 None,
417 None,
418 );
419 let position_report3 = PositionStatusReport::new(
420 AccountId::from("IB-DU123456"),
421 InstrumentId::from("MSFT.NASDAQ"), PositionSide::Long,
423 Quantity::from("100"),
424 UnixNanos::from(2_200_000_000),
425 UnixNanos::from(3_200_000_000),
426 None,
427 None,
428 None,
429 );
430
431 mass_status.add_position_reports(vec![
432 position_report1.clone(),
433 position_report2.clone(),
434 position_report3.clone(),
435 ]);
436
437 let position_reports = mass_status.position_reports();
438 assert_eq!(position_reports.len(), 2); let aapl_positions = position_reports
442 .get(&InstrumentId::from("AAPL.NASDAQ"))
443 .unwrap();
444 assert_eq!(aapl_positions.len(), 2);
445 assert_eq!(aapl_positions[0], position_report1);
446 assert_eq!(aapl_positions[1], position_report2);
447
448 let msft_positions = position_reports
450 .get(&InstrumentId::from("MSFT.NASDAQ"))
451 .unwrap();
452 assert_eq!(msft_positions.len(), 1);
453 assert_eq!(msft_positions[0], position_report3);
454 }
455
456 #[rstest]
457 fn test_add_multiple_fills_for_different_orders() {
458 let mut mass_status = test_execution_mass_status();
459 let fill_report1 = create_test_fill_report(); let fill_report2 = FillReport::new(
461 AccountId::from("IB-DU123456"),
462 InstrumentId::from("MSFT.NASDAQ"),
463 VenueOrderId::from("2"), TradeId::from("T-003"),
465 OrderSide::Sell,
466 Quantity::from("75"),
467 Price::from("300.00"),
468 Money::new(2.0, Currency::USD()),
469 LiquiditySide::Taker,
470 None,
471 None,
472 UnixNanos::from(1_700_000_000),
473 UnixNanos::from(2_700_000_000),
474 None,
475 );
476
477 mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
478
479 let fill_reports = mass_status.fill_reports();
480 assert_eq!(fill_reports.len(), 2); let fills_order_1 = fill_reports.get(&VenueOrderId::from("1")).unwrap();
483 assert_eq!(fills_order_1.len(), 1);
484 assert_eq!(fills_order_1[0], fill_report1);
485
486 let fills_order_2 = fill_reports.get(&VenueOrderId::from("2")).unwrap();
487 assert_eq!(fills_order_2.len(), 1);
488 assert_eq!(fills_order_2[0], fill_report2);
489 }
490
491 #[rstest]
492 fn test_comprehensive_mass_status() {
493 let mut mass_status = test_execution_mass_status();
494
495 let order_report = create_test_order_report();
497 let fill_report = create_test_fill_report();
498 let position_report = create_test_position_report();
499
500 mass_status.add_order_reports(vec![order_report.clone()]);
501 mass_status.add_fill_reports(vec![fill_report.clone()]);
502 mass_status.add_position_reports(vec![position_report.clone()]);
503
504 assert_eq!(mass_status.order_reports().len(), 1);
506 assert_eq!(mass_status.fill_reports().len(), 1);
507 assert_eq!(mass_status.position_reports().len(), 1);
508
509 assert_eq!(
511 mass_status.order_reports().get(&VenueOrderId::from("1")),
512 Some(&order_report)
513 );
514 assert_eq!(
515 mass_status
516 .fill_reports()
517 .get(&VenueOrderId::from("1"))
518 .unwrap()[0],
519 fill_report
520 );
521 assert_eq!(
522 mass_status
523 .position_reports()
524 .get(&InstrumentId::from("AAPL.NASDAQ"))
525 .unwrap()[0],
526 position_report
527 );
528 }
529
530 #[rstest]
531 fn test_display() {
532 let mass_status = test_execution_mass_status();
533
534 assert_eq!(
535 mass_status.to_string(),
536 format!(
537 "ExecutionMassStatus(client_id=IB, account_id=IB-DU123456, venue=NASDAQ, order_reports={{}}, fill_reports={{}}, position_reports={{}}, report_id={}, ts_init=1000000000)",
538 mass_status.report_id,
539 )
540 );
541 }
542
543 #[rstest]
544 fn test_display_with_reports_uses_report_display() {
545 let mut mass_status = test_execution_mass_status();
546 let order_report = create_test_order_report();
547 let fill_report = create_test_fill_report();
548 let position_report = create_test_position_report();
549 let expected_order_report = order_report.to_string();
550 let expected_fill_report = fill_report.to_string();
551 let expected_position_report = position_report.to_string();
552
553 mass_status.add_order_reports(vec![order_report]);
554 mass_status.add_fill_reports(vec![fill_report]);
555 mass_status.add_position_reports(vec![position_report]);
556
557 assert_eq!(
558 mass_status.to_string(),
559 format!(
560 "ExecutionMassStatus(client_id=IB, account_id=IB-DU123456, venue=NASDAQ, order_reports={{\"1\": {expected_order_report}}}, fill_reports={{\"1\": [{expected_fill_report}]}}, position_reports={{\"AAPL.NASDAQ\": [{expected_position_report}]}}, report_id={}, ts_init=1000000000)",
561 mass_status.report_id,
562 )
563 );
564 }
565
566 #[rstest]
567 fn test_report_map_display_uses_value_display() {
568 let reports = IndexMap::from([("key", "value")]);
569
570 assert_eq!(ReportMapDisplay(&reports).to_string(), "{\"key\": value}");
571 }
572
573 #[rstest]
574 fn test_report_list_map_display_uses_value_display() {
575 let reports = IndexMap::from([("key", vec!["one", "two"])]);
576
577 assert_eq!(
578 ReportListMapDisplay(&reports).to_string(),
579 "{\"key\": [one, two]}"
580 );
581 }
582
583 #[rstest]
584 fn test_clone_and_equality() {
585 let mass_status1 = test_execution_mass_status();
586 let mass_status2 = mass_status1.clone();
587
588 assert_eq!(mass_status1, mass_status2);
589 }
590
591 #[rstest]
592 fn test_serialization_roundtrip() {
593 let mut original = test_execution_mass_status();
594 original.set_report_window(Some(UnixNanos::from(500_000_000)), false);
595
596 let json = serde_json::to_string(&original).unwrap();
598 let deserialized: ExecutionMassStatus = serde_json::from_str(&json).unwrap();
599 assert_eq!(original, deserialized);
600 }
601
602 #[rstest]
603 fn test_deserialization_defaults_unbounded_report_contract() {
604 let original = test_execution_mass_status();
605 let mut value = serde_json::to_value(original).unwrap();
606 let object = value.as_object_mut().unwrap();
607 object.remove("lookback_start");
608 object.remove("reports_complete");
609
610 let deserialized: ExecutionMassStatus = serde_json::from_value(value).unwrap();
611
612 assert_eq!(deserialized.lookback_start(), None);
613 assert!(deserialized.reports_complete());
614 }
615
616 #[rstest]
617 fn test_empty_mass_status_accessors() {
618 let mass_status = test_execution_mass_status();
619
620 assert!(mass_status.order_reports().is_empty());
622 assert!(mass_status.fill_reports().is_empty());
623 assert!(mass_status.position_reports().is_empty());
624 }
625
626 #[rstest]
627 fn test_add_empty_reports() {
628 let mut mass_status = test_execution_mass_status();
629
630 mass_status.add_order_reports(vec![]);
632 mass_status.add_fill_reports(vec![]);
633 mass_status.add_position_reports(vec![]);
634
635 assert!(mass_status.order_reports().is_empty());
637 assert!(mass_status.fill_reports().is_empty());
638 assert!(mass_status.position_reports().is_empty());
639 }
640
641 #[rstest]
642 fn test_overwrite_order_reports() {
643 let mut mass_status = test_execution_mass_status();
644 let venue_order_id = VenueOrderId::from("1");
645
646 let order_report1 = create_test_order_report();
648 mass_status.add_order_reports(vec![order_report1.clone()]);
649
650 let order_report2 = OrderStatusReport::new(
652 AccountId::from("IB-DU123456"),
653 InstrumentId::from("AAPL.NASDAQ"),
654 None,
655 venue_order_id,
656 OrderSide::Sell.into(), OrderType::Market,
658 TimeInForce::Ioc,
659 OrderStatus::Filled,
660 Quantity::from("200"),
661 Quantity::from("200"),
662 UnixNanos::from(1_000_000_000),
663 UnixNanos::from(2_000_000_000),
664 UnixNanos::from(3_000_000_000),
665 None,
666 );
667 mass_status.add_order_reports(vec![order_report2.clone()]);
668
669 let order_reports = mass_status.order_reports();
671 assert_eq!(order_reports.len(), 1);
672 assert_eq!(order_reports.get(&venue_order_id), Some(&order_report2));
673 assert_ne!(order_reports.get(&venue_order_id), Some(&order_report1));
674 }
675}