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", 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 order_reports: IndexMap<VenueOrderId, OrderStatusReport>,
52 fill_reports: IndexMap<VenueOrderId, Vec<FillReport>>,
54 position_reports: IndexMap<InstrumentId, Vec<PositionStatusReport>>,
56}
57
58impl ExecutionMassStatus {
59 #[must_use]
61 pub fn new(
62 client_id: ClientId,
63 account_id: AccountId,
64 venue: Venue,
65 ts_init: UnixNanos,
66 report_id: Option<UUID4>,
67 ) -> Self {
68 Self {
69 client_id,
70 account_id,
71 venue,
72 report_id: report_id.unwrap_or_default(),
73 ts_init,
74 order_reports: IndexMap::new(),
75 fill_reports: IndexMap::new(),
76 position_reports: IndexMap::new(),
77 }
78 }
79
80 #[must_use]
82 pub fn order_reports(&self) -> IndexMap<VenueOrderId, OrderStatusReport> {
83 self.order_reports.clone()
84 }
85
86 #[must_use]
88 pub fn fill_reports(&self) -> IndexMap<VenueOrderId, Vec<FillReport>> {
89 self.fill_reports.clone()
90 }
91
92 #[must_use]
94 pub fn position_reports(&self) -> IndexMap<InstrumentId, Vec<PositionStatusReport>> {
95 self.position_reports.clone()
96 }
97
98 pub fn add_order_reports(&mut self, reports: Vec<OrderStatusReport>) {
100 for report in reports {
101 self.order_reports.insert(report.venue_order_id, report);
102 }
103 }
104
105 pub fn add_fill_reports(&mut self, reports: Vec<FillReport>) {
107 for report in reports {
108 self.fill_reports
109 .entry(report.venue_order_id)
110 .or_default()
111 .push(report);
112 }
113 }
114
115 pub fn add_position_reports(&mut self, reports: Vec<PositionStatusReport>) {
117 for report in reports {
118 self.position_reports
119 .entry(report.instrument_id)
120 .or_default()
121 .push(report);
122 }
123 }
124}
125
126impl Display for ExecutionMassStatus {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 write!(
129 f,
130 "ExecutionMassStatus(client_id={}, account_id={}, venue={}, order_reports={:?}, fill_reports={:?}, position_reports={:?}, report_id={}, ts_init={})",
131 self.client_id,
132 self.account_id,
133 self.venue,
134 self.order_reports,
135 self.fill_reports,
136 self.position_reports,
137 self.report_id,
138 self.ts_init,
139 )
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use nautilus_core::UnixNanos;
146 use rstest::*;
147
148 use super::*;
149 use crate::{
150 enums::{
151 LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSideSpecified, TimeInForce,
152 },
153 identifiers::{
154 AccountId, ClientId, InstrumentId, PositionId, TradeId, Venue, VenueOrderId,
155 },
156 reports::{fill::FillReport, order::OrderStatusReport, position::PositionStatusReport},
157 types::{Currency, Money, Price, Quantity},
158 };
159
160 fn test_execution_mass_status() -> ExecutionMassStatus {
161 ExecutionMassStatus::new(
162 ClientId::from("IB"),
163 AccountId::from("IB-DU123456"),
164 Venue::from("NASDAQ"),
165 UnixNanos::from(1_000_000_000),
166 None,
167 )
168 }
169
170 fn create_test_order_report() -> OrderStatusReport {
171 OrderStatusReport::new(
172 AccountId::from("IB-DU123456"),
173 InstrumentId::from("AAPL.NASDAQ"),
174 None,
175 VenueOrderId::from("1"),
176 OrderSide::Buy,
177 OrderType::Limit,
178 TimeInForce::Gtc,
179 OrderStatus::Accepted,
180 Quantity::from("100"),
181 Quantity::from("0"),
182 UnixNanos::from(1_000_000_000),
183 UnixNanos::from(2_000_000_000),
184 UnixNanos::from(3_000_000_000),
185 None,
186 )
187 }
188
189 fn create_test_fill_report() -> FillReport {
190 FillReport::new(
191 AccountId::from("IB-DU123456"),
192 InstrumentId::from("AAPL.NASDAQ"),
193 VenueOrderId::from("1"),
194 TradeId::from("T-001"),
195 OrderSide::Buy,
196 Quantity::from("50"),
197 Price::from("150.00"),
198 Money::new(1.0, Currency::USD()),
199 LiquiditySide::Taker,
200 None,
201 None,
202 UnixNanos::from(1_500_000_000),
203 UnixNanos::from(2_500_000_000),
204 None,
205 )
206 }
207
208 fn create_test_position_report() -> PositionStatusReport {
209 PositionStatusReport::new(
210 AccountId::from("IB-DU123456"),
211 InstrumentId::from("AAPL.NASDAQ"),
212 PositionSideSpecified::Long,
213 Quantity::from("50"),
214 UnixNanos::from(2_000_000_000),
215 UnixNanos::from(3_000_000_000),
216 None, Some(PositionId::from("P-001")), None, )
220 }
221
222 #[rstest]
223 fn test_execution_mass_status_new() {
224 let mass_status = test_execution_mass_status();
225
226 assert_eq!(mass_status.client_id, ClientId::from("IB"));
227 assert_eq!(mass_status.account_id, AccountId::from("IB-DU123456"));
228 assert_eq!(mass_status.venue, Venue::from("NASDAQ"));
229 assert_eq!(mass_status.ts_init, UnixNanos::from(1_000_000_000));
230 assert!(mass_status.order_reports().is_empty());
231 assert!(mass_status.fill_reports().is_empty());
232 assert!(mass_status.position_reports().is_empty());
233 }
234
235 #[rstest]
236 fn test_execution_mass_status_with_generated_report_id() {
237 let mass_status = ExecutionMassStatus::new(
238 ClientId::from("IB"),
239 AccountId::from("IB-DU123456"),
240 Venue::from("NASDAQ"),
241 UnixNanos::from(1_000_000_000),
242 None, );
244
245 assert_ne!(
247 mass_status.report_id.to_string(),
248 "00000000-0000-0000-0000-000000000000"
249 );
250 }
251
252 #[rstest]
253 fn test_add_order_reports() {
254 let mut mass_status = test_execution_mass_status();
255 let order_report1 = create_test_order_report();
256 let order_report2 = OrderStatusReport::new(
257 AccountId::from("IB-DU123456"),
258 InstrumentId::from("MSFT.NASDAQ"),
259 None,
260 VenueOrderId::from("2"),
261 OrderSide::Sell,
262 OrderType::Market,
263 TimeInForce::Ioc,
264 OrderStatus::Filled,
265 Quantity::from("200"),
266 Quantity::from("200"),
267 UnixNanos::from(1_000_000_000),
268 UnixNanos::from(2_000_000_000),
269 UnixNanos::from(3_000_000_000),
270 None,
271 );
272
273 mass_status.add_order_reports(vec![order_report1.clone(), order_report2.clone()]);
274
275 let order_reports = mass_status.order_reports();
276 assert_eq!(order_reports.len(), 2);
277 assert_eq!(
278 order_reports.get(&VenueOrderId::from("1")),
279 Some(&order_report1)
280 );
281 assert_eq!(
282 order_reports.get(&VenueOrderId::from("2")),
283 Some(&order_report2)
284 );
285 }
286
287 #[rstest]
288 fn test_add_fill_reports() {
289 let mut mass_status = test_execution_mass_status();
290 let fill_report1 = create_test_fill_report();
291 let fill_report2 = FillReport::new(
292 AccountId::from("IB-DU123456"),
293 InstrumentId::from("AAPL.NASDAQ"),
294 VenueOrderId::from("1"), TradeId::from("T-002"),
296 OrderSide::Buy,
297 Quantity::from("50"),
298 Price::from("151.00"),
299 Money::new(1.5, Currency::USD()),
300 LiquiditySide::Maker,
301 None,
302 None,
303 UnixNanos::from(1_600_000_000),
304 UnixNanos::from(2_600_000_000),
305 None,
306 );
307
308 mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
309
310 let fill_reports = mass_status.fill_reports();
311 assert_eq!(fill_reports.len(), 1); let fills_for_order = fill_reports.get(&VenueOrderId::from("1")).unwrap();
314 assert_eq!(fills_for_order.len(), 2);
315 assert_eq!(fills_for_order[0], fill_report1);
316 assert_eq!(fills_for_order[1], fill_report2);
317 }
318
319 #[rstest]
320 fn test_add_position_reports() {
321 let mut mass_status = test_execution_mass_status();
322 let position_report1 = create_test_position_report();
323 let position_report2 = PositionStatusReport::new(
324 AccountId::from("IB-DU123456"),
325 InstrumentId::from("AAPL.NASDAQ"), PositionSideSpecified::Short,
327 Quantity::from("25"),
328 UnixNanos::from(2_100_000_000),
329 UnixNanos::from(3_100_000_000),
330 None,
331 None,
332 None,
333 );
334 let position_report3 = PositionStatusReport::new(
335 AccountId::from("IB-DU123456"),
336 InstrumentId::from("MSFT.NASDAQ"), PositionSideSpecified::Long,
338 Quantity::from("100"),
339 UnixNanos::from(2_200_000_000),
340 UnixNanos::from(3_200_000_000),
341 None,
342 None,
343 None,
344 );
345
346 mass_status.add_position_reports(vec![
347 position_report1.clone(),
348 position_report2.clone(),
349 position_report3.clone(),
350 ]);
351
352 let position_reports = mass_status.position_reports();
353 assert_eq!(position_reports.len(), 2); let aapl_positions = position_reports
357 .get(&InstrumentId::from("AAPL.NASDAQ"))
358 .unwrap();
359 assert_eq!(aapl_positions.len(), 2);
360 assert_eq!(aapl_positions[0], position_report1);
361 assert_eq!(aapl_positions[1], position_report2);
362
363 let msft_positions = position_reports
365 .get(&InstrumentId::from("MSFT.NASDAQ"))
366 .unwrap();
367 assert_eq!(msft_positions.len(), 1);
368 assert_eq!(msft_positions[0], position_report3);
369 }
370
371 #[rstest]
372 fn test_add_multiple_fills_for_different_orders() {
373 let mut mass_status = test_execution_mass_status();
374 let fill_report1 = create_test_fill_report(); let fill_report2 = FillReport::new(
376 AccountId::from("IB-DU123456"),
377 InstrumentId::from("MSFT.NASDAQ"),
378 VenueOrderId::from("2"), TradeId::from("T-003"),
380 OrderSide::Sell,
381 Quantity::from("75"),
382 Price::from("300.00"),
383 Money::new(2.0, Currency::USD()),
384 LiquiditySide::Taker,
385 None,
386 None,
387 UnixNanos::from(1_700_000_000),
388 UnixNanos::from(2_700_000_000),
389 None,
390 );
391
392 mass_status.add_fill_reports(vec![fill_report1.clone(), fill_report2.clone()]);
393
394 let fill_reports = mass_status.fill_reports();
395 assert_eq!(fill_reports.len(), 2); let fills_order_1 = fill_reports.get(&VenueOrderId::from("1")).unwrap();
398 assert_eq!(fills_order_1.len(), 1);
399 assert_eq!(fills_order_1[0], fill_report1);
400
401 let fills_order_2 = fill_reports.get(&VenueOrderId::from("2")).unwrap();
402 assert_eq!(fills_order_2.len(), 1);
403 assert_eq!(fills_order_2[0], fill_report2);
404 }
405
406 #[rstest]
407 fn test_comprehensive_mass_status() {
408 let mut mass_status = test_execution_mass_status();
409
410 let order_report = create_test_order_report();
412 let fill_report = create_test_fill_report();
413 let position_report = create_test_position_report();
414
415 mass_status.add_order_reports(vec![order_report.clone()]);
416 mass_status.add_fill_reports(vec![fill_report.clone()]);
417 mass_status.add_position_reports(vec![position_report.clone()]);
418
419 assert_eq!(mass_status.order_reports().len(), 1);
421 assert_eq!(mass_status.fill_reports().len(), 1);
422 assert_eq!(mass_status.position_reports().len(), 1);
423
424 assert_eq!(
426 mass_status.order_reports().get(&VenueOrderId::from("1")),
427 Some(&order_report)
428 );
429 assert_eq!(
430 mass_status
431 .fill_reports()
432 .get(&VenueOrderId::from("1"))
433 .unwrap()[0],
434 fill_report
435 );
436 assert_eq!(
437 mass_status
438 .position_reports()
439 .get(&InstrumentId::from("AAPL.NASDAQ"))
440 .unwrap()[0],
441 position_report
442 );
443 }
444
445 #[rstest]
446 fn test_display() {
447 let mass_status = test_execution_mass_status();
448 let display_str = format!("{mass_status}");
449
450 assert!(display_str.contains("ExecutionMassStatus"));
451 assert!(display_str.contains("IB"));
452 assert!(display_str.contains("IB-DU123456"));
453 assert!(display_str.contains("NASDAQ"));
454 }
455
456 #[rstest]
457 fn test_clone_and_equality() {
458 let mass_status1 = test_execution_mass_status();
459 let mass_status2 = mass_status1.clone();
460
461 assert_eq!(mass_status1, mass_status2);
462 }
463
464 #[rstest]
465 fn test_serialization_roundtrip() {
466 let original = test_execution_mass_status();
467
468 let json = serde_json::to_string(&original).unwrap();
470 let deserialized: ExecutionMassStatus = serde_json::from_str(&json).unwrap();
471 assert_eq!(original, deserialized);
472 }
473
474 #[rstest]
475 fn test_empty_mass_status_accessors() {
476 let mass_status = test_execution_mass_status();
477
478 assert!(mass_status.order_reports().is_empty());
480 assert!(mass_status.fill_reports().is_empty());
481 assert!(mass_status.position_reports().is_empty());
482 }
483
484 #[rstest]
485 fn test_add_empty_reports() {
486 let mut mass_status = test_execution_mass_status();
487
488 mass_status.add_order_reports(vec![]);
490 mass_status.add_fill_reports(vec![]);
491 mass_status.add_position_reports(vec![]);
492
493 assert!(mass_status.order_reports().is_empty());
495 assert!(mass_status.fill_reports().is_empty());
496 assert!(mass_status.position_reports().is_empty());
497 }
498
499 #[rstest]
500 fn test_overwrite_order_reports() {
501 let mut mass_status = test_execution_mass_status();
502 let venue_order_id = VenueOrderId::from("1");
503
504 let order_report1 = create_test_order_report();
506 mass_status.add_order_reports(vec![order_report1.clone()]);
507
508 let order_report2 = OrderStatusReport::new(
510 AccountId::from("IB-DU123456"),
511 InstrumentId::from("AAPL.NASDAQ"),
512 None,
513 venue_order_id,
514 OrderSide::Sell, OrderType::Market,
516 TimeInForce::Ioc,
517 OrderStatus::Filled,
518 Quantity::from("200"),
519 Quantity::from("200"),
520 UnixNanos::from(1_000_000_000),
521 UnixNanos::from(2_000_000_000),
522 UnixNanos::from(3_000_000_000),
523 None,
524 );
525 mass_status.add_order_reports(vec![order_report2.clone()]);
526
527 let order_reports = mass_status.order_reports();
529 assert_eq!(order_reports.len(), 1);
530 assert_eq!(order_reports.get(&venue_order_id), Some(&order_report2));
531 assert_ne!(order_reports.get(&venue_order_id), Some(&order_report1));
532 }
533}