1use indexmap::IndexMap;
17use nautilus_core::{UUID4, UnixNanos};
18use serde::{Deserialize, Serialize};
19
20use crate::{
21 identifiers::{AccountId, ClientId, InstrumentId, Venue, VenueOrderId},
22 reports::{fill::FillReport, order::OrderStatusReport, position::PositionStatusReport},
23};
24
25#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28#[serde(tag = "type")]
29#[cfg_attr(
30 feature = "python",
31 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
32)]
33pub struct ExecutionMassStatus {
34 pub client_id: ClientId,
36 pub account_id: AccountId,
38 pub venue: Venue,
40 pub report_id: UUID4,
42 pub ts_init: UnixNanos,
44 order_reports: IndexMap<VenueOrderId, OrderStatusReport>,
46 fill_reports: IndexMap<VenueOrderId, Vec<FillReport>>,
48 position_reports: IndexMap<InstrumentId, Vec<PositionStatusReport>>,
50}
51
52impl ExecutionMassStatus {
53 #[must_use]
55 pub fn new(
56 client_id: ClientId,
57 account_id: AccountId,
58 venue: Venue,
59 ts_init: UnixNanos,
60 report_id: Option<UUID4>,
61 ) -> Self {
62 Self {
63 client_id,
64 account_id,
65 venue,
66 report_id: report_id.unwrap_or_default(),
67 ts_init,
68 order_reports: IndexMap::new(),
69 fill_reports: IndexMap::new(),
70 position_reports: IndexMap::new(),
71 }
72 }
73
74 #[must_use]
76 pub fn order_reports(&self) -> IndexMap<VenueOrderId, OrderStatusReport> {
77 self.order_reports.clone()
78 }
79
80 #[must_use]
82 pub fn fill_reports(&self) -> IndexMap<VenueOrderId, Vec<FillReport>> {
83 self.fill_reports.clone()
84 }
85
86 #[must_use]
88 pub fn position_reports(&self) -> IndexMap<InstrumentId, Vec<PositionStatusReport>> {
89 self.position_reports.clone()
90 }
91
92 pub fn add_order_reports(&mut self, reports: Vec<OrderStatusReport>) {
94 for report in reports {
95 self.order_reports.insert(report.venue_order_id, report);
96 }
97 }
98
99 pub fn add_fill_reports(&mut self, reports: Vec<FillReport>) {
101 for report in reports {
102 self.fill_reports
103 .entry(report.venue_order_id)
104 .or_default()
105 .push(report);
106 }
107 }
108
109 pub fn add_position_reports(&mut self, reports: Vec<PositionStatusReport>) {
111 for report in reports {
112 self.position_reports
113 .entry(report.instrument_id)
114 .or_default()
115 .push(report);
116 }
117 }
118}
119
120impl std::fmt::Display for ExecutionMassStatus {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 write!(
123 f,
124 "ExecutionMassStatus(client_id={}, account_id={}, venue={}, order_reports={:?}, fill_reports={:?}, position_reports={:?}, report_id={}, ts_init={})",
125 self.client_id,
126 self.account_id,
127 self.venue,
128 self.order_reports,
129 self.fill_reports,
130 self.position_reports,
131 self.report_id,
132 self.ts_init,
133 )
134 }
135}
136
137#[cfg(test)]
141mod tests {
142 use nautilus_core::UnixNanos;
143 use rstest::*;
144
145 use super::*;
146 use crate::{
147 enums::{
148 LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSideSpecified, TimeInForce,
149 },
150 identifiers::{
151 AccountId, ClientId, InstrumentId, PositionId, TradeId, Venue, VenueOrderId,
152 },
153 reports::{fill::FillReport, order::OrderStatusReport, position::PositionStatusReport},
154 types::{Currency, Money, Price, Quantity},
155 };
156
157 fn test_execution_mass_status() -> ExecutionMassStatus {
158 ExecutionMassStatus::new(
159 ClientId::from("IB"),
160 AccountId::from("IB-DU123456"),
161 Venue::from("NASDAQ"),
162 UnixNanos::from(1_000_000_000),
163 None,
164 )
165 }
166
167 fn create_test_order_report() -> OrderStatusReport {
168 OrderStatusReport::new(
169 AccountId::from("IB-DU123456"),
170 InstrumentId::from("AAPL.NASDAQ"),
171 None,
172 VenueOrderId::from("1"),
173 OrderSide::Buy,
174 OrderType::Limit,
175 TimeInForce::Gtc,
176 OrderStatus::Accepted,
177 Quantity::from("100"),
178 Quantity::from("0"),
179 UnixNanos::from(1_000_000_000),
180 UnixNanos::from(2_000_000_000),
181 UnixNanos::from(3_000_000_000),
182 None,
183 )
184 }
185
186 fn create_test_fill_report() -> FillReport {
187 FillReport::new(
188 AccountId::from("IB-DU123456"),
189 InstrumentId::from("AAPL.NASDAQ"),
190 VenueOrderId::from("1"),
191 TradeId::from("T-001"),
192 OrderSide::Buy,
193 Quantity::from("50"),
194 Price::from("150.00"),
195 Money::new(1.0, Currency::USD()),
196 LiquiditySide::Taker,
197 None,
198 None,
199 UnixNanos::from(1_500_000_000),
200 UnixNanos::from(2_500_000_000),
201 None,
202 )
203 }
204
205 fn create_test_position_report() -> PositionStatusReport {
206 PositionStatusReport::new(
207 AccountId::from("IB-DU123456"),
208 InstrumentId::from("AAPL.NASDAQ"),
209 PositionSideSpecified::Long,
210 Quantity::from("50"),
211 Some(PositionId::from("P-001")),
212 UnixNanos::from(2_000_000_000),
213 UnixNanos::from(3_000_000_000),
214 None,
215 )
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 None,
325 UnixNanos::from(2_100_000_000),
326 UnixNanos::from(3_100_000_000),
327 None,
328 );
329 let position_report3 = PositionStatusReport::new(
330 AccountId::from("IB-DU123456"),
331 InstrumentId::from("MSFT.NASDAQ"), PositionSideSpecified::Long,
333 Quantity::from("100"),
334 None,
335 UnixNanos::from(2_200_000_000),
336 UnixNanos::from(3_200_000_000),
337 None,
338 );
339
340 mass_status.add_position_reports(vec![
341 position_report1.clone(),
342 position_report2.clone(),
343 position_report3.clone(),
344 ]);
345
346 let position_reports = mass_status.position_reports();
347 assert_eq!(position_reports.len(), 2); let aapl_positions = position_reports
351 .get(&InstrumentId::from("AAPL.NASDAQ"))
352 .unwrap();
353 assert_eq!(aapl_positions.len(), 2);
354 assert_eq!(aapl_positions[0], position_report1);
355 assert_eq!(aapl_positions[1], position_report2);
356
357 let msft_positions = position_reports
359 .get(&InstrumentId::from("MSFT.NASDAQ"))
360 .unwrap();
361 assert_eq!(msft_positions.len(), 1);
362 assert_eq!(msft_positions[0], position_report3);
363 }
364
365 #[rstest]
366 fn test_add_multiple_fills_for_different_orders() {
367 let mut mass_status = test_execution_mass_status();
368 let fill_report1 = create_test_fill_report(); let fill_report2 = FillReport::new(
370 AccountId::from("IB-DU123456"),
371 InstrumentId::from("MSFT.NASDAQ"),
372 VenueOrderId::from("2"), TradeId::from("T-003"),
374 OrderSide::Sell,
375 Quantity::from("75"),
376 Price::from("300.00"),
377 Money::new(2.0, Currency::USD()),
378 LiquiditySide::Taker,
379 None,
380 None,
381 UnixNanos::from(1_700_000_000),
382 UnixNanos::from(2_700_000_000),
383 None,
384 );
385
386 mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
387
388 let fill_reports = mass_status.fill_reports();
389 assert_eq!(fill_reports.len(), 2); let fills_order_1 = fill_reports.get(&VenueOrderId::from("1")).unwrap();
392 assert_eq!(fills_order_1.len(), 1);
393 assert_eq!(fills_order_1[0], fill_report1);
394
395 let fills_order_2 = fill_reports.get(&VenueOrderId::from("2")).unwrap();
396 assert_eq!(fills_order_2.len(), 1);
397 assert_eq!(fills_order_2[0], fill_report2);
398 }
399
400 #[rstest]
401 fn test_comprehensive_mass_status() {
402 let mut mass_status = test_execution_mass_status();
403
404 let order_report = create_test_order_report();
406 let fill_report = create_test_fill_report();
407 let position_report = create_test_position_report();
408
409 mass_status.add_order_reports(vec![order_report.clone()]);
410 mass_status.add_fill_reports(vec![fill_report.clone()]);
411 mass_status.add_position_reports(vec![position_report.clone()]);
412
413 assert_eq!(mass_status.order_reports().len(), 1);
415 assert_eq!(mass_status.fill_reports().len(), 1);
416 assert_eq!(mass_status.position_reports().len(), 1);
417
418 assert_eq!(
420 mass_status.order_reports().get(&VenueOrderId::from("1")),
421 Some(&order_report)
422 );
423 assert_eq!(
424 mass_status
425 .fill_reports()
426 .get(&VenueOrderId::from("1"))
427 .unwrap()[0],
428 fill_report
429 );
430 assert_eq!(
431 mass_status
432 .position_reports()
433 .get(&InstrumentId::from("AAPL.NASDAQ"))
434 .unwrap()[0],
435 position_report
436 );
437 }
438
439 #[rstest]
440 fn test_display() {
441 let mass_status = test_execution_mass_status();
442 let display_str = format!("{mass_status}");
443
444 assert!(display_str.contains("ExecutionMassStatus"));
445 assert!(display_str.contains("IB"));
446 assert!(display_str.contains("IB-DU123456"));
447 assert!(display_str.contains("NASDAQ"));
448 }
449
450 #[rstest]
451 fn test_clone_and_equality() {
452 let mass_status1 = test_execution_mass_status();
453 let mass_status2 = mass_status1.clone();
454
455 assert_eq!(mass_status1, mass_status2);
456 }
457
458 #[rstest]
459 fn test_serialization_roundtrip() {
460 let original = test_execution_mass_status();
461
462 let json = serde_json::to_string(&original).unwrap();
464 let deserialized: ExecutionMassStatus = serde_json::from_str(&json).unwrap();
465 assert_eq!(original, deserialized);
466 }
467
468 #[rstest]
469 fn test_empty_mass_status_accessors() {
470 let mass_status = test_execution_mass_status();
471
472 assert!(mass_status.order_reports().is_empty());
474 assert!(mass_status.fill_reports().is_empty());
475 assert!(mass_status.position_reports().is_empty());
476 }
477
478 #[rstest]
479 fn test_add_empty_reports() {
480 let mut mass_status = test_execution_mass_status();
481
482 mass_status.add_order_reports(vec![]);
484 mass_status.add_fill_reports(vec![]);
485 mass_status.add_position_reports(vec![]);
486
487 assert!(mass_status.order_reports().is_empty());
489 assert!(mass_status.fill_reports().is_empty());
490 assert!(mass_status.position_reports().is_empty());
491 }
492
493 #[rstest]
494 fn test_overwrite_order_reports() {
495 let mut mass_status = test_execution_mass_status();
496 let venue_order_id = VenueOrderId::from("1");
497
498 let order_report1 = create_test_order_report();
500 mass_status.add_order_reports(vec![order_report1.clone()]);
501
502 let order_report2 = OrderStatusReport::new(
504 AccountId::from("IB-DU123456"),
505 InstrumentId::from("AAPL.NASDAQ"),
506 None,
507 venue_order_id,
508 OrderSide::Sell, OrderType::Market,
510 TimeInForce::Ioc,
511 OrderStatus::Filled,
512 Quantity::from("200"),
513 Quantity::from("200"),
514 UnixNanos::from(1_000_000_000),
515 UnixNanos::from(2_000_000_000),
516 UnixNanos::from(3_000_000_000),
517 None,
518 );
519 mass_status.add_order_reports(vec![order_report2.clone()]);
520
521 let order_reports = mass_status.order_reports();
523 assert_eq!(order_reports.len(), 1);
524 assert_eq!(order_reports.get(&venue_order_id), Some(&order_report2));
525 assert_ne!(order_reports.get(&venue_order_id), Some(&order_report1));
526 }
527}