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 UnixNanos::from(2_000_000_000),
212 UnixNanos::from(3_000_000_000),
213 None, Some(PositionId::from("P-001")), None, )
217 }
218
219 #[rstest]
220 fn test_execution_mass_status_new() {
221 let mass_status = test_execution_mass_status();
222
223 assert_eq!(mass_status.client_id, ClientId::from("IB"));
224 assert_eq!(mass_status.account_id, AccountId::from("IB-DU123456"));
225 assert_eq!(mass_status.venue, Venue::from("NASDAQ"));
226 assert_eq!(mass_status.ts_init, UnixNanos::from(1_000_000_000));
227 assert!(mass_status.order_reports().is_empty());
228 assert!(mass_status.fill_reports().is_empty());
229 assert!(mass_status.position_reports().is_empty());
230 }
231
232 #[rstest]
233 fn test_execution_mass_status_with_generated_report_id() {
234 let mass_status = ExecutionMassStatus::new(
235 ClientId::from("IB"),
236 AccountId::from("IB-DU123456"),
237 Venue::from("NASDAQ"),
238 UnixNanos::from(1_000_000_000),
239 None, );
241
242 assert_ne!(
244 mass_status.report_id.to_string(),
245 "00000000-0000-0000-0000-000000000000"
246 );
247 }
248
249 #[rstest]
250 fn test_add_order_reports() {
251 let mut mass_status = test_execution_mass_status();
252 let order_report1 = create_test_order_report();
253 let order_report2 = OrderStatusReport::new(
254 AccountId::from("IB-DU123456"),
255 InstrumentId::from("MSFT.NASDAQ"),
256 None,
257 VenueOrderId::from("2"),
258 OrderSide::Sell,
259 OrderType::Market,
260 TimeInForce::Ioc,
261 OrderStatus::Filled,
262 Quantity::from("200"),
263 Quantity::from("200"),
264 UnixNanos::from(1_000_000_000),
265 UnixNanos::from(2_000_000_000),
266 UnixNanos::from(3_000_000_000),
267 None,
268 );
269
270 mass_status.add_order_reports(vec![order_report1.clone(), order_report2.clone()]);
271
272 let order_reports = mass_status.order_reports();
273 assert_eq!(order_reports.len(), 2);
274 assert_eq!(
275 order_reports.get(&VenueOrderId::from("1")),
276 Some(&order_report1)
277 );
278 assert_eq!(
279 order_reports.get(&VenueOrderId::from("2")),
280 Some(&order_report2)
281 );
282 }
283
284 #[rstest]
285 fn test_add_fill_reports() {
286 let mut mass_status = test_execution_mass_status();
287 let fill_report1 = create_test_fill_report();
288 let fill_report2 = FillReport::new(
289 AccountId::from("IB-DU123456"),
290 InstrumentId::from("AAPL.NASDAQ"),
291 VenueOrderId::from("1"), TradeId::from("T-002"),
293 OrderSide::Buy,
294 Quantity::from("50"),
295 Price::from("151.00"),
296 Money::new(1.5, Currency::USD()),
297 LiquiditySide::Maker,
298 None,
299 None,
300 UnixNanos::from(1_600_000_000),
301 UnixNanos::from(2_600_000_000),
302 None,
303 );
304
305 mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
306
307 let fill_reports = mass_status.fill_reports();
308 assert_eq!(fill_reports.len(), 1); let fills_for_order = fill_reports.get(&VenueOrderId::from("1")).unwrap();
311 assert_eq!(fills_for_order.len(), 2);
312 assert_eq!(fills_for_order[0], fill_report1);
313 assert_eq!(fills_for_order[1], fill_report2);
314 }
315
316 #[rstest]
317 fn test_add_position_reports() {
318 let mut mass_status = test_execution_mass_status();
319 let position_report1 = create_test_position_report();
320 let position_report2 = PositionStatusReport::new(
321 AccountId::from("IB-DU123456"),
322 InstrumentId::from("AAPL.NASDAQ"), PositionSideSpecified::Short,
324 Quantity::from("25"),
325 UnixNanos::from(2_100_000_000),
326 UnixNanos::from(3_100_000_000),
327 None,
328 None,
329 None,
330 );
331 let position_report3 = PositionStatusReport::new(
332 AccountId::from("IB-DU123456"),
333 InstrumentId::from("MSFT.NASDAQ"), PositionSideSpecified::Long,
335 Quantity::from("100"),
336 UnixNanos::from(2_200_000_000),
337 UnixNanos::from(3_200_000_000),
338 None,
339 None,
340 None,
341 );
342
343 mass_status.add_position_reports(vec![
344 position_report1.clone(),
345 position_report2.clone(),
346 position_report3.clone(),
347 ]);
348
349 let position_reports = mass_status.position_reports();
350 assert_eq!(position_reports.len(), 2); let aapl_positions = position_reports
354 .get(&InstrumentId::from("AAPL.NASDAQ"))
355 .unwrap();
356 assert_eq!(aapl_positions.len(), 2);
357 assert_eq!(aapl_positions[0], position_report1);
358 assert_eq!(aapl_positions[1], position_report2);
359
360 let msft_positions = position_reports
362 .get(&InstrumentId::from("MSFT.NASDAQ"))
363 .unwrap();
364 assert_eq!(msft_positions.len(), 1);
365 assert_eq!(msft_positions[0], position_report3);
366 }
367
368 #[rstest]
369 fn test_add_multiple_fills_for_different_orders() {
370 let mut mass_status = test_execution_mass_status();
371 let fill_report1 = create_test_fill_report(); let fill_report2 = FillReport::new(
373 AccountId::from("IB-DU123456"),
374 InstrumentId::from("MSFT.NASDAQ"),
375 VenueOrderId::from("2"), TradeId::from("T-003"),
377 OrderSide::Sell,
378 Quantity::from("75"),
379 Price::from("300.00"),
380 Money::new(2.0, Currency::USD()),
381 LiquiditySide::Taker,
382 None,
383 None,
384 UnixNanos::from(1_700_000_000),
385 UnixNanos::from(2_700_000_000),
386 None,
387 );
388
389 mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
390
391 let fill_reports = mass_status.fill_reports();
392 assert_eq!(fill_reports.len(), 2); let fills_order_1 = fill_reports.get(&VenueOrderId::from("1")).unwrap();
395 assert_eq!(fills_order_1.len(), 1);
396 assert_eq!(fills_order_1[0], fill_report1);
397
398 let fills_order_2 = fill_reports.get(&VenueOrderId::from("2")).unwrap();
399 assert_eq!(fills_order_2.len(), 1);
400 assert_eq!(fills_order_2[0], fill_report2);
401 }
402
403 #[rstest]
404 fn test_comprehensive_mass_status() {
405 let mut mass_status = test_execution_mass_status();
406
407 let order_report = create_test_order_report();
409 let fill_report = create_test_fill_report();
410 let position_report = create_test_position_report();
411
412 mass_status.add_order_reports(vec![order_report.clone()]);
413 mass_status.add_fill_reports(vec![fill_report.clone()]);
414 mass_status.add_position_reports(vec![position_report.clone()]);
415
416 assert_eq!(mass_status.order_reports().len(), 1);
418 assert_eq!(mass_status.fill_reports().len(), 1);
419 assert_eq!(mass_status.position_reports().len(), 1);
420
421 assert_eq!(
423 mass_status.order_reports().get(&VenueOrderId::from("1")),
424 Some(&order_report)
425 );
426 assert_eq!(
427 mass_status
428 .fill_reports()
429 .get(&VenueOrderId::from("1"))
430 .unwrap()[0],
431 fill_report
432 );
433 assert_eq!(
434 mass_status
435 .position_reports()
436 .get(&InstrumentId::from("AAPL.NASDAQ"))
437 .unwrap()[0],
438 position_report
439 );
440 }
441
442 #[rstest]
443 fn test_display() {
444 let mass_status = test_execution_mass_status();
445 let display_str = format!("{mass_status}");
446
447 assert!(display_str.contains("ExecutionMassStatus"));
448 assert!(display_str.contains("IB"));
449 assert!(display_str.contains("IB-DU123456"));
450 assert!(display_str.contains("NASDAQ"));
451 }
452
453 #[rstest]
454 fn test_clone_and_equality() {
455 let mass_status1 = test_execution_mass_status();
456 let mass_status2 = mass_status1.clone();
457
458 assert_eq!(mass_status1, mass_status2);
459 }
460
461 #[rstest]
462 fn test_serialization_roundtrip() {
463 let original = test_execution_mass_status();
464
465 let json = serde_json::to_string(&original).unwrap();
467 let deserialized: ExecutionMassStatus = serde_json::from_str(&json).unwrap();
468 assert_eq!(original, deserialized);
469 }
470
471 #[rstest]
472 fn test_empty_mass_status_accessors() {
473 let mass_status = test_execution_mass_status();
474
475 assert!(mass_status.order_reports().is_empty());
477 assert!(mass_status.fill_reports().is_empty());
478 assert!(mass_status.position_reports().is_empty());
479 }
480
481 #[rstest]
482 fn test_add_empty_reports() {
483 let mut mass_status = test_execution_mass_status();
484
485 mass_status.add_order_reports(vec![]);
487 mass_status.add_fill_reports(vec![]);
488 mass_status.add_position_reports(vec![]);
489
490 assert!(mass_status.order_reports().is_empty());
492 assert!(mass_status.fill_reports().is_empty());
493 assert!(mass_status.position_reports().is_empty());
494 }
495
496 #[rstest]
497 fn test_overwrite_order_reports() {
498 let mut mass_status = test_execution_mass_status();
499 let venue_order_id = VenueOrderId::from("1");
500
501 let order_report1 = create_test_order_report();
503 mass_status.add_order_reports(vec![order_report1.clone()]);
504
505 let order_report2 = OrderStatusReport::new(
507 AccountId::from("IB-DU123456"),
508 InstrumentId::from("AAPL.NASDAQ"),
509 None,
510 venue_order_id,
511 OrderSide::Sell, OrderType::Market,
513 TimeInForce::Ioc,
514 OrderStatus::Filled,
515 Quantity::from("200"),
516 Quantity::from("200"),
517 UnixNanos::from(1_000_000_000),
518 UnixNanos::from(2_000_000_000),
519 UnixNanos::from(3_000_000_000),
520 None,
521 );
522 mass_status.add_order_reports(vec![order_report2.clone()]);
523
524 let order_reports = mass_status.order_reports();
526 assert_eq!(order_reports.len(), 1);
527 assert_eq!(order_reports.get(&venue_order_id), Some(&order_report2));
528 assert_ne!(order_reports.get(&venue_order_id), Some(&order_report1));
529 }
530}