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.core.nautilus_pyo3.model")
34)]
35pub struct ExecutionMassStatus {
36 pub client_id: ClientId,
38 pub account_id: AccountId,
40 pub venue: Venue,
42 pub report_id: UUID4,
44 pub ts_init: UnixNanos,
46 order_reports: IndexMap<VenueOrderId, OrderStatusReport>,
48 fill_reports: IndexMap<VenueOrderId, Vec<FillReport>>,
50 position_reports: IndexMap<InstrumentId, Vec<PositionStatusReport>>,
52}
53
54impl ExecutionMassStatus {
55 #[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 #[must_use]
78 pub fn order_reports(&self) -> IndexMap<VenueOrderId, OrderStatusReport> {
79 self.order_reports.clone()
80 }
81
82 #[must_use]
84 pub fn fill_reports(&self) -> IndexMap<VenueOrderId, Vec<FillReport>> {
85 self.fill_reports.clone()
86 }
87
88 #[must_use]
90 pub fn position_reports(&self) -> IndexMap<InstrumentId, Vec<PositionStatusReport>> {
91 self.position_reports.clone()
92 }
93
94 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 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 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#[cfg(test)]
140mod tests {
141 use nautilus_core::UnixNanos;
142 use rstest::*;
143
144 use super::*;
145 use crate::{
146 enums::{
147 LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSideSpecified, TimeInForce,
148 },
149 identifiers::{
150 AccountId, ClientId, InstrumentId, PositionId, TradeId, Venue, VenueOrderId,
151 },
152 reports::{fill::FillReport, order::OrderStatusReport, position::PositionStatusReport},
153 types::{Currency, Money, Price, Quantity},
154 };
155
156 fn test_execution_mass_status() -> ExecutionMassStatus {
157 ExecutionMassStatus::new(
158 ClientId::from("IB"),
159 AccountId::from("IB-DU123456"),
160 Venue::from("NASDAQ"),
161 UnixNanos::from(1_000_000_000),
162 None,
163 )
164 }
165
166 fn create_test_order_report() -> OrderStatusReport {
167 OrderStatusReport::new(
168 AccountId::from("IB-DU123456"),
169 InstrumentId::from("AAPL.NASDAQ"),
170 None,
171 VenueOrderId::from("1"),
172 OrderSide::Buy,
173 OrderType::Limit,
174 TimeInForce::Gtc,
175 OrderStatus::Accepted,
176 Quantity::from("100"),
177 Quantity::from("0"),
178 UnixNanos::from(1_000_000_000),
179 UnixNanos::from(2_000_000_000),
180 UnixNanos::from(3_000_000_000),
181 None,
182 )
183 }
184
185 fn create_test_fill_report() -> FillReport {
186 FillReport::new(
187 AccountId::from("IB-DU123456"),
188 InstrumentId::from("AAPL.NASDAQ"),
189 VenueOrderId::from("1"),
190 TradeId::from("T-001"),
191 OrderSide::Buy,
192 Quantity::from("50"),
193 Price::from("150.00"),
194 Money::new(1.0, Currency::USD()),
195 LiquiditySide::Taker,
196 None,
197 None,
198 UnixNanos::from(1_500_000_000),
199 UnixNanos::from(2_500_000_000),
200 None,
201 )
202 }
203
204 fn create_test_position_report() -> PositionStatusReport {
205 PositionStatusReport::new(
206 AccountId::from("IB-DU123456"),
207 InstrumentId::from("AAPL.NASDAQ"),
208 PositionSideSpecified::Long,
209 Quantity::from("50"),
210 UnixNanos::from(2_000_000_000),
211 UnixNanos::from(3_000_000_000),
212 None, Some(PositionId::from("P-001")), None, )
216 }
217
218 #[rstest]
219 fn test_execution_mass_status_new() {
220 let mass_status = test_execution_mass_status();
221
222 assert_eq!(mass_status.client_id, ClientId::from("IB"));
223 assert_eq!(mass_status.account_id, AccountId::from("IB-DU123456"));
224 assert_eq!(mass_status.venue, Venue::from("NASDAQ"));
225 assert_eq!(mass_status.ts_init, UnixNanos::from(1_000_000_000));
226 assert!(mass_status.order_reports().is_empty());
227 assert!(mass_status.fill_reports().is_empty());
228 assert!(mass_status.position_reports().is_empty());
229 }
230
231 #[rstest]
232 fn test_execution_mass_status_with_generated_report_id() {
233 let mass_status = ExecutionMassStatus::new(
234 ClientId::from("IB"),
235 AccountId::from("IB-DU123456"),
236 Venue::from("NASDAQ"),
237 UnixNanos::from(1_000_000_000),
238 None, );
240
241 assert_ne!(
243 mass_status.report_id.to_string(),
244 "00000000-0000-0000-0000-000000000000"
245 );
246 }
247
248 #[rstest]
249 fn test_add_order_reports() {
250 let mut mass_status = test_execution_mass_status();
251 let order_report1 = create_test_order_report();
252 let order_report2 = OrderStatusReport::new(
253 AccountId::from("IB-DU123456"),
254 InstrumentId::from("MSFT.NASDAQ"),
255 None,
256 VenueOrderId::from("2"),
257 OrderSide::Sell,
258 OrderType::Market,
259 TimeInForce::Ioc,
260 OrderStatus::Filled,
261 Quantity::from("200"),
262 Quantity::from("200"),
263 UnixNanos::from(1_000_000_000),
264 UnixNanos::from(2_000_000_000),
265 UnixNanos::from(3_000_000_000),
266 None,
267 );
268
269 mass_status.add_order_reports(vec![order_report1.clone(), order_report2.clone()]);
270
271 let order_reports = mass_status.order_reports();
272 assert_eq!(order_reports.len(), 2);
273 assert_eq!(
274 order_reports.get(&VenueOrderId::from("1")),
275 Some(&order_report1)
276 );
277 assert_eq!(
278 order_reports.get(&VenueOrderId::from("2")),
279 Some(&order_report2)
280 );
281 }
282
283 #[rstest]
284 fn test_add_fill_reports() {
285 let mut mass_status = test_execution_mass_status();
286 let fill_report1 = create_test_fill_report();
287 let fill_report2 = FillReport::new(
288 AccountId::from("IB-DU123456"),
289 InstrumentId::from("AAPL.NASDAQ"),
290 VenueOrderId::from("1"), TradeId::from("T-002"),
292 OrderSide::Buy,
293 Quantity::from("50"),
294 Price::from("151.00"),
295 Money::new(1.5, Currency::USD()),
296 LiquiditySide::Maker,
297 None,
298 None,
299 UnixNanos::from(1_600_000_000),
300 UnixNanos::from(2_600_000_000),
301 None,
302 );
303
304 mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
305
306 let fill_reports = mass_status.fill_reports();
307 assert_eq!(fill_reports.len(), 1); let fills_for_order = fill_reports.get(&VenueOrderId::from("1")).unwrap();
310 assert_eq!(fills_for_order.len(), 2);
311 assert_eq!(fills_for_order[0], fill_report1);
312 assert_eq!(fills_for_order[1], fill_report2);
313 }
314
315 #[rstest]
316 fn test_add_position_reports() {
317 let mut mass_status = test_execution_mass_status();
318 let position_report1 = create_test_position_report();
319 let position_report2 = PositionStatusReport::new(
320 AccountId::from("IB-DU123456"),
321 InstrumentId::from("AAPL.NASDAQ"), PositionSideSpecified::Short,
323 Quantity::from("25"),
324 UnixNanos::from(2_100_000_000),
325 UnixNanos::from(3_100_000_000),
326 None,
327 None,
328 None,
329 );
330 let position_report3 = PositionStatusReport::new(
331 AccountId::from("IB-DU123456"),
332 InstrumentId::from("MSFT.NASDAQ"), PositionSideSpecified::Long,
334 Quantity::from("100"),
335 UnixNanos::from(2_200_000_000),
336 UnixNanos::from(3_200_000_000),
337 None,
338 None,
339 None,
340 );
341
342 mass_status.add_position_reports(vec![
343 position_report1.clone(),
344 position_report2.clone(),
345 position_report3.clone(),
346 ]);
347
348 let position_reports = mass_status.position_reports();
349 assert_eq!(position_reports.len(), 2); let aapl_positions = position_reports
353 .get(&InstrumentId::from("AAPL.NASDAQ"))
354 .unwrap();
355 assert_eq!(aapl_positions.len(), 2);
356 assert_eq!(aapl_positions[0], position_report1);
357 assert_eq!(aapl_positions[1], position_report2);
358
359 let msft_positions = position_reports
361 .get(&InstrumentId::from("MSFT.NASDAQ"))
362 .unwrap();
363 assert_eq!(msft_positions.len(), 1);
364 assert_eq!(msft_positions[0], position_report3);
365 }
366
367 #[rstest]
368 fn test_add_multiple_fills_for_different_orders() {
369 let mut mass_status = test_execution_mass_status();
370 let fill_report1 = create_test_fill_report(); let fill_report2 = FillReport::new(
372 AccountId::from("IB-DU123456"),
373 InstrumentId::from("MSFT.NASDAQ"),
374 VenueOrderId::from("2"), TradeId::from("T-003"),
376 OrderSide::Sell,
377 Quantity::from("75"),
378 Price::from("300.00"),
379 Money::new(2.0, Currency::USD()),
380 LiquiditySide::Taker,
381 None,
382 None,
383 UnixNanos::from(1_700_000_000),
384 UnixNanos::from(2_700_000_000),
385 None,
386 );
387
388 mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
389
390 let fill_reports = mass_status.fill_reports();
391 assert_eq!(fill_reports.len(), 2); let fills_order_1 = fill_reports.get(&VenueOrderId::from("1")).unwrap();
394 assert_eq!(fills_order_1.len(), 1);
395 assert_eq!(fills_order_1[0], fill_report1);
396
397 let fills_order_2 = fill_reports.get(&VenueOrderId::from("2")).unwrap();
398 assert_eq!(fills_order_2.len(), 1);
399 assert_eq!(fills_order_2[0], fill_report2);
400 }
401
402 #[rstest]
403 fn test_comprehensive_mass_status() {
404 let mut mass_status = test_execution_mass_status();
405
406 let order_report = create_test_order_report();
408 let fill_report = create_test_fill_report();
409 let position_report = create_test_position_report();
410
411 mass_status.add_order_reports(vec![order_report.clone()]);
412 mass_status.add_fill_reports(vec![fill_report.clone()]);
413 mass_status.add_position_reports(vec![position_report.clone()]);
414
415 assert_eq!(mass_status.order_reports().len(), 1);
417 assert_eq!(mass_status.fill_reports().len(), 1);
418 assert_eq!(mass_status.position_reports().len(), 1);
419
420 assert_eq!(
422 mass_status.order_reports().get(&VenueOrderId::from("1")),
423 Some(&order_report)
424 );
425 assert_eq!(
426 mass_status
427 .fill_reports()
428 .get(&VenueOrderId::from("1"))
429 .unwrap()[0],
430 fill_report
431 );
432 assert_eq!(
433 mass_status
434 .position_reports()
435 .get(&InstrumentId::from("AAPL.NASDAQ"))
436 .unwrap()[0],
437 position_report
438 );
439 }
440
441 #[rstest]
442 fn test_display() {
443 let mass_status = test_execution_mass_status();
444 let display_str = format!("{mass_status}");
445
446 assert!(display_str.contains("ExecutionMassStatus"));
447 assert!(display_str.contains("IB"));
448 assert!(display_str.contains("IB-DU123456"));
449 assert!(display_str.contains("NASDAQ"));
450 }
451
452 #[rstest]
453 fn test_clone_and_equality() {
454 let mass_status1 = test_execution_mass_status();
455 let mass_status2 = mass_status1.clone();
456
457 assert_eq!(mass_status1, mass_status2);
458 }
459
460 #[rstest]
461 fn test_serialization_roundtrip() {
462 let original = test_execution_mass_status();
463
464 let json = serde_json::to_string(&original).unwrap();
466 let deserialized: ExecutionMassStatus = serde_json::from_str(&json).unwrap();
467 assert_eq!(original, deserialized);
468 }
469
470 #[rstest]
471 fn test_empty_mass_status_accessors() {
472 let mass_status = test_execution_mass_status();
473
474 assert!(mass_status.order_reports().is_empty());
476 assert!(mass_status.fill_reports().is_empty());
477 assert!(mass_status.position_reports().is_empty());
478 }
479
480 #[rstest]
481 fn test_add_empty_reports() {
482 let mut mass_status = test_execution_mass_status();
483
484 mass_status.add_order_reports(vec![]);
486 mass_status.add_fill_reports(vec![]);
487 mass_status.add_position_reports(vec![]);
488
489 assert!(mass_status.order_reports().is_empty());
491 assert!(mass_status.fill_reports().is_empty());
492 assert!(mass_status.position_reports().is_empty());
493 }
494
495 #[rstest]
496 fn test_overwrite_order_reports() {
497 let mut mass_status = test_execution_mass_status();
498 let venue_order_id = VenueOrderId::from("1");
499
500 let order_report1 = create_test_order_report();
502 mass_status.add_order_reports(vec![order_report1.clone()]);
503
504 let order_report2 = OrderStatusReport::new(
506 AccountId::from("IB-DU123456"),
507 InstrumentId::from("AAPL.NASDAQ"),
508 None,
509 venue_order_id,
510 OrderSide::Sell, OrderType::Market,
512 TimeInForce::Ioc,
513 OrderStatus::Filled,
514 Quantity::from("200"),
515 Quantity::from("200"),
516 UnixNanos::from(1_000_000_000),
517 UnixNanos::from(2_000_000_000),
518 UnixNanos::from(3_000_000_000),
519 None,
520 );
521 mass_status.add_order_reports(vec![order_report2.clone()]);
522
523 let order_reports = mass_status.order_reports();
525 assert_eq!(order_reports.len(), 1);
526 assert_eq!(order_reports.get(&venue_order_id), Some(&order_report2));
527 assert_ne!(order_reports.get(&venue_order_id), Some(&order_report1));
528 }
529}