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)]
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, Some(PositionId::from("P-001")), None, )
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, );
243
244 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"), 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); 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"), 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"), 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); 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 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(); let fill_report2 = FillReport::new(
375 AccountId::from("IB-DU123456"),
376 InstrumentId::from("MSFT.NASDAQ"),
377 VenueOrderId::from("2"), 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); 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 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 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 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 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 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 mass_status.add_order_reports(vec![]);
489 mass_status.add_fill_reports(vec![]);
490 mass_status.add_position_reports(vec![]);
491
492 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 let order_report1 = create_test_order_report();
505 mass_status.add_order_reports(vec![order_report1.clone()]);
506
507 let order_report2 = OrderStatusReport::new(
509 AccountId::from("IB-DU123456"),
510 InstrumentId::from("AAPL.NASDAQ"),
511 None,
512 venue_order_id,
513 OrderSide::Sell, 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 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}