1use anyhow::Context;
19use async_trait::async_trait;
20use nautilus_core::{DurationNanos, Params, UnixNanos, time::get_atomic_clock_realtime};
21use nautilus_model::{
22 accounts::AccountAny,
23 enums::{LiquiditySide, OmsType},
24 identifiers::{
25 AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, Venue, VenueOrderId,
26 },
27 instruments::InstrumentAny,
28 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
29 types::{AccountBalance, MarginBalance, Money, Price, Quantity},
30};
31use rust_decimal::Decimal;
32
33use super::log_not_implemented;
34use crate::messages::execution::{
35 BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
36 GenerateFillReportsBuilder, GenerateOrderStatusReport, GenerateOrderStatusReports,
37 GenerateOrderStatusReportsBuilder, GeneratePositionStatusReports,
38 GeneratePositionStatusReportsBuilder, ModifyOrder, QueryAccount, QueryOrder, SubmitOrder,
39 SubmitOrderList,
40};
41
42pub const DEFAULT_POSITION_RECONCILIATION_TOLERANCE: Decimal =
44 Decimal::from_parts(1, 0, 0, false, 8);
45
46#[async_trait(?Send)]
53pub trait ExecutionClient {
54 fn is_connected(&self) -> bool;
55 fn client_id(&self) -> ClientId;
56 fn account_id(&self) -> AccountId;
57 fn venue(&self) -> Venue;
58 fn oms_type(&self) -> OmsType;
59 fn get_account(&self) -> Option<AccountAny>;
60
61 fn position_reconciliation_tolerance(&self) -> Decimal {
63 DEFAULT_POSITION_RECONCILIATION_TOLERANCE
64 }
65
66 fn handles_order_venue(&self, venue: Venue) -> bool {
72 self.venue() == venue
73 }
74
75 fn provides_bulk_position_coverage(&self, _instrument_id: InstrumentId) -> bool {
78 true
79 }
80
81 fn generate_account_state(
91 &self,
92 balances: Vec<AccountBalance>,
93 margins: Vec<MarginBalance>,
94 reported: bool,
95 ts_event: UnixNanos,
96 info: Option<Params>,
97 ) -> anyhow::Result<()>;
98
99 fn start(&mut self) -> anyhow::Result<()>;
105
106 fn stop(&mut self) -> anyhow::Result<()>;
117
118 fn reset(&mut self) -> anyhow::Result<()> {
127 Ok(())
128 }
129
130 fn dispose(&mut self) -> anyhow::Result<()> {
139 Ok(())
140 }
141
142 async fn connect(&mut self) -> anyhow::Result<()> {
148 Ok(())
149 }
150
151 async fn disconnect(&mut self) -> anyhow::Result<()> {
157 Ok(())
158 }
159
160 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
166 log_not_implemented(&cmd);
167 Ok(())
168 }
169
170 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
176 log_not_implemented(&cmd);
177 Ok(())
178 }
179
180 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
186 log_not_implemented(&cmd);
187 Ok(())
188 }
189
190 fn batch_modify_orders(&self, cmd: BatchModifyOrders) -> anyhow::Result<()> {
199 for modify in cmd.modifies {
200 self.modify_order(modify)?;
201 }
202 Ok(())
203 }
204
205 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
211 log_not_implemented(&cmd);
212 Ok(())
213 }
214
215 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
221 log_not_implemented(&cmd);
222 Ok(())
223 }
224
225 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
231 log_not_implemented(&cmd);
232 Ok(())
233 }
234
235 fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
241 log_not_implemented(&cmd);
242 Ok(())
243 }
244
245 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
251 log_not_implemented(&cmd);
252 Ok(())
253 }
254
255 async fn generate_order_status_report(
261 &self,
262 cmd: &GenerateOrderStatusReport,
263 ) -> anyhow::Result<Option<OrderStatusReport>> {
264 log_not_implemented(cmd);
265 Ok(None)
266 }
267
268 async fn generate_order_status_reports(
274 &self,
275 cmd: &GenerateOrderStatusReports,
276 ) -> anyhow::Result<Vec<OrderStatusReport>> {
277 log_not_implemented(cmd);
278 Ok(Vec::new())
279 }
280
281 async fn generate_fill_reports(
287 &self,
288 cmd: GenerateFillReports,
289 ) -> anyhow::Result<Vec<FillReport>> {
290 log_not_implemented(&cmd);
291 Ok(Vec::new())
292 }
293
294 async fn generate_position_status_reports(
300 &self,
301 cmd: &GeneratePositionStatusReports,
302 ) -> anyhow::Result<Vec<PositionStatusReport>> {
303 log_not_implemented(cmd);
304 Ok(Vec::new())
305 }
306
307 async fn generate_mass_status(
317 &self,
318 lookback_mins: Option<u64>,
319 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
320 generate_mass_status(
321 self,
322 lookback_mins,
323 get_atomic_clock_realtime().get_time_ns(),
324 )
325 .await
326 }
327
328 fn register_external_order(
333 &self,
334 _client_order_id: ClientOrderId,
335 _venue_order_id: VenueOrderId,
336 _instrument_id: InstrumentId,
337 _strategy_id: StrategyId,
338 _ts_init: UnixNanos,
339 ) {
340 }
342
343 fn on_instrument(&mut self, _instrument: InstrumentAny) {
348 }
350
351 #[expect(unused_variables)]
367 fn calculate_commission(
368 &self,
369 instrument: &InstrumentAny,
370 last_qty: Quantity,
371 last_px: Price,
372 liquidity_side: LiquiditySide,
373 ) -> anyhow::Result<Option<Money>> {
374 Ok(None)
375 }
376}
377
378pub async fn generate_mass_status<C: ExecutionClient + ?Sized>(
384 client: &C,
385 lookback_mins: Option<u64>,
386 ts_init: UnixNanos,
387) -> anyhow::Result<Option<ExecutionMassStatus>> {
388 let start = lookback_mins
389 .map(DurationNanos::try_from_mins)
390 .transpose()?
391 .map(|lookback| ts_init.saturating_sub(lookback));
392
393 let order_cmd = GenerateOrderStatusReportsBuilder::default()
394 .ts_init(ts_init)
395 .open_only(false)
396 .start(start)
397 .build()
398 .context("failed to build order status reports command")?;
399 let fill_cmd = GenerateFillReportsBuilder::default()
400 .ts_init(ts_init)
401 .start(start)
402 .build()
403 .context("failed to build fill reports command")?;
404 let position_cmd = GeneratePositionStatusReportsBuilder::default()
405 .ts_init(ts_init)
406 .start(start)
407 .build()
408 .context("failed to build position status reports command")?;
409
410 let (order_reports, fill_reports, position_reports) = futures::try_join!(
411 async {
412 client
413 .generate_order_status_reports(&order_cmd)
414 .await
415 .context("failed to generate order status reports")
416 },
417 async {
418 client
419 .generate_fill_reports(fill_cmd)
420 .await
421 .context("failed to generate fill reports")
422 },
423 async {
424 client
425 .generate_position_status_reports(&position_cmd)
426 .await
427 .context("failed to generate position status reports")
428 },
429 )?;
430
431 let mut mass_status = ExecutionMassStatus::new(
432 client.client_id(),
433 client.account_id(),
434 client.venue(),
435 ts_init,
436 None,
437 );
438 mass_status.add_order_reports(order_reports);
439 mass_status.add_fill_reports(fill_reports);
440 mass_status.add_position_reports(position_reports);
441
442 Ok(Some(mass_status))
443}
444
445#[cfg(test)]
446mod tests {
447 use std::{cell::RefCell, rc::Rc};
448
449 use nautilus_core::UUID4;
450 use nautilus_model::{
451 enums::{
452 LiquiditySide, OmsType, OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce,
453 },
454 identifiers::{PositionId, TradeId, TraderId, Venue},
455 types::Currency,
456 };
457 use rstest::rstest;
458
459 use super::*;
460
461 struct RecordingExecutionClient {
462 modified_order_ids: Rc<RefCell<Vec<ClientOrderId>>>,
463 }
464
465 impl RecordingExecutionClient {
466 fn new(modified_order_ids: Rc<RefCell<Vec<ClientOrderId>>>) -> Self {
467 Self { modified_order_ids }
468 }
469 }
470
471 #[async_trait(?Send)]
472 impl ExecutionClient for RecordingExecutionClient {
473 fn is_connected(&self) -> bool {
474 true
475 }
476
477 fn client_id(&self) -> ClientId {
478 ClientId::from("TEST")
479 }
480
481 fn account_id(&self) -> AccountId {
482 AccountId::from("TEST-001")
483 }
484
485 fn venue(&self) -> Venue {
486 Venue::from("SIM")
487 }
488
489 fn oms_type(&self) -> OmsType {
490 OmsType::Netting
491 }
492
493 fn get_account(&self) -> Option<AccountAny> {
494 None
495 }
496
497 fn generate_account_state(
498 &self,
499 _balances: Vec<AccountBalance>,
500 _margins: Vec<MarginBalance>,
501 _reported: bool,
502 _ts_event: UnixNanos,
503 _info: Option<Params>,
504 ) -> anyhow::Result<()> {
505 Ok(())
506 }
507
508 fn start(&mut self) -> anyhow::Result<()> {
509 Ok(())
510 }
511
512 fn stop(&mut self) -> anyhow::Result<()> {
513 Ok(())
514 }
515
516 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
517 self.modified_order_ids
518 .borrow_mut()
519 .push(cmd.client_order_id);
520
521 Ok(())
522 }
523 }
524
525 struct MassStatusExecutionClient {
526 order_commands: RefCell<Vec<GenerateOrderStatusReports>>,
527 fill_requests: RefCell<Vec<GenerateFillReports>>,
528 position_queries: RefCell<Vec<GeneratePositionStatusReports>>,
529 fail_fill: bool,
530 }
531
532 impl MassStatusExecutionClient {
533 fn new(fail_fill: bool) -> Self {
534 Self {
535 order_commands: RefCell::new(Vec::new()),
536 fill_requests: RefCell::new(Vec::new()),
537 position_queries: RefCell::new(Vec::new()),
538 fail_fill,
539 }
540 }
541 }
542
543 #[async_trait(?Send)]
544 impl ExecutionClient for MassStatusExecutionClient {
545 fn is_connected(&self) -> bool {
546 true
547 }
548
549 fn client_id(&self) -> ClientId {
550 ClientId::from("MASS-STATUS")
551 }
552
553 fn account_id(&self) -> AccountId {
554 AccountId::from("MASS-STATUS-001")
555 }
556
557 fn venue(&self) -> Venue {
558 Venue::from("SIM")
559 }
560
561 fn oms_type(&self) -> OmsType {
562 OmsType::Netting
563 }
564
565 fn get_account(&self) -> Option<AccountAny> {
566 None
567 }
568
569 fn generate_account_state(
570 &self,
571 _balances: Vec<AccountBalance>,
572 _margins: Vec<MarginBalance>,
573 _reported: bool,
574 _ts_event: UnixNanos,
575 _info: Option<Params>,
576 ) -> anyhow::Result<()> {
577 Ok(())
578 }
579
580 fn start(&mut self) -> anyhow::Result<()> {
581 Ok(())
582 }
583
584 fn stop(&mut self) -> anyhow::Result<()> {
585 Ok(())
586 }
587
588 async fn generate_order_status_reports(
589 &self,
590 cmd: &GenerateOrderStatusReports,
591 ) -> anyhow::Result<Vec<OrderStatusReport>> {
592 self.order_commands.borrow_mut().push(cmd.clone());
593 Ok(vec![test_order_report()])
594 }
595
596 async fn generate_fill_reports(
597 &self,
598 cmd: GenerateFillReports,
599 ) -> anyhow::Result<Vec<FillReport>> {
600 self.fill_requests.borrow_mut().push(cmd);
601
602 if self.fail_fill {
603 anyhow::bail!("sentinel fill report failure");
604 }
605 Ok(vec![test_fill_report()])
606 }
607
608 async fn generate_position_status_reports(
609 &self,
610 cmd: &GeneratePositionStatusReports,
611 ) -> anyhow::Result<Vec<PositionStatusReport>> {
612 self.position_queries.borrow_mut().push(cmd.clone());
613 Ok(vec![test_position_report()])
614 }
615 }
616
617 fn test_order_report() -> OrderStatusReport {
618 OrderStatusReport::new(
619 AccountId::from("MASS-STATUS-001"),
620 InstrumentId::from("AUD/USD.SIM"),
621 None,
622 VenueOrderId::from("ORDER-001"),
623 OrderSide::Buy.into(),
624 OrderType::Limit,
625 TimeInForce::Gtc,
626 OrderStatus::Accepted,
627 Quantity::from("10"),
628 Quantity::from("0"),
629 UnixNanos::from(1_000_000_000),
630 UnixNanos::from(2_000_000_000),
631 UnixNanos::from(3_000_000_000),
632 None,
633 )
634 }
635
636 fn test_fill_report() -> FillReport {
637 FillReport::new(
638 AccountId::from("MASS-STATUS-001"),
639 InstrumentId::from("AUD/USD.SIM"),
640 VenueOrderId::from("ORDER-001"),
641 TradeId::from("TRADE-001"),
642 OrderSide::Buy,
643 Quantity::from("5"),
644 Price::from("1.00010"),
645 Money::new(1.0, Currency::USD()),
646 LiquiditySide::Taker,
647 None,
648 None,
649 UnixNanos::from(4_000_000_000),
650 UnixNanos::from(5_000_000_000),
651 None,
652 )
653 }
654
655 fn test_position_report() -> PositionStatusReport {
656 PositionStatusReport::new(
657 AccountId::from("MASS-STATUS-001"),
658 InstrumentId::from("AUD/USD.SIM"),
659 PositionSide::Long,
660 Quantity::from("5"),
661 UnixNanos::from(6_000_000_000),
662 UnixNanos::from(7_000_000_000),
663 None,
664 Some(PositionId::from("POSITION-001")),
665 None,
666 )
667 }
668
669 #[rstest]
670 fn batch_modify_orders_default_fans_out_to_modify_order() {
671 let modified_order_ids = Rc::new(RefCell::new(Vec::new()));
672 let client = RecordingExecutionClient::new(modified_order_ids.clone());
673 let instrument_id = InstrumentId::from("AUD/USD.SIM");
674 let order1 = ClientOrderId::from("O-DEFAULT-BATCH-001");
675 let order2 = ClientOrderId::from("O-DEFAULT-BATCH-002");
676 let command = BatchModifyOrders::new(
677 TraderId::from("TRADER-001"),
678 Some(ClientId::from("TEST")),
679 StrategyId::from("S-001"),
680 instrument_id,
681 vec![
682 ModifyOrder::new(
683 TraderId::from("TRADER-001"),
684 Some(ClientId::from("TEST")),
685 StrategyId::from("S-001"),
686 instrument_id,
687 order1,
688 None,
689 Some(Quantity::from("10")),
690 Some(Price::from("1.00010")),
691 None,
692 UUID4::new(),
693 UnixNanos::default(),
694 None,
695 None,
696 ),
697 ModifyOrder::new(
698 TraderId::from("TRADER-001"),
699 Some(ClientId::from("TEST")),
700 StrategyId::from("S-001"),
701 instrument_id,
702 order2,
703 None,
704 Some(Quantity::from("20")),
705 Some(Price::from("1.00020")),
706 None,
707 UUID4::new(),
708 UnixNanos::default(),
709 None,
710 None,
711 ),
712 ],
713 UUID4::new(),
714 UnixNanos::default(),
715 None,
716 None,
717 );
718
719 client.batch_modify_orders(command).unwrap();
720
721 assert_eq!(modified_order_ids.borrow().as_slice(), &[order1, order2]);
722 }
723
724 #[rstest]
725 fn generate_mass_status_default_composes_granular_reports() {
726 let client = MassStatusExecutionClient::new(false);
727
728 let mass_status = futures::executor::block_on(client.generate_mass_status(Some(5)))
729 .unwrap()
730 .unwrap();
731
732 assert_eq!(mass_status.client_id, ClientId::from("MASS-STATUS"));
733 assert_eq!(mass_status.account_id, AccountId::from("MASS-STATUS-001"));
734 assert_eq!(mass_status.venue, Venue::from("SIM"));
735
736 let order_reports = mass_status.order_reports();
737 let fill_reports = mass_status.fill_reports();
738 let position_reports = mass_status.position_reports();
739 let order_report = order_reports.get(&VenueOrderId::from("ORDER-001")).unwrap();
740 let fill_report = &fill_reports.get(&VenueOrderId::from("ORDER-001")).unwrap()[0];
741 let position_report = &position_reports
742 .get(&InstrumentId::from("AUD/USD.SIM"))
743 .unwrap()[0];
744 assert_eq!(order_reports.len(), 1);
745 assert_eq!(fill_reports.len(), 1);
746 assert_eq!(position_reports.len(), 1);
747 assert_eq!(
748 order_report.instrument_id,
749 InstrumentId::from("AUD/USD.SIM")
750 );
751 assert_eq!(fill_report.trade_id, TradeId::from("TRADE-001"));
752 assert_eq!(
753 position_report.venue_position_id,
754 Some(PositionId::from("POSITION-001")),
755 );
756
757 let order_commands = client.order_commands.borrow();
758 let fill_requests = client.fill_requests.borrow();
759 let position_queries = client.position_queries.borrow();
760 assert_eq!(order_commands.len(), 1);
761 assert_eq!(fill_requests.len(), 1);
762 assert_eq!(position_queries.len(), 1);
763
764 let order_cmd = &order_commands[0];
765 let fill_cmd = &fill_requests[0];
766 let position_cmd = &position_queries[0];
767 assert_eq!(order_cmd.ts_init, mass_status.ts_init);
768 assert_eq!(fill_cmd.ts_init, mass_status.ts_init);
769 assert_eq!(position_cmd.ts_init, mass_status.ts_init);
770 assert_ne!(test_order_report().ts_init, mass_status.ts_init);
771 assert_ne!(test_fill_report().ts_init, mass_status.ts_init);
772 assert_ne!(test_position_report().ts_init, mass_status.ts_init);
773
774 let expected_start = mass_status
775 .ts_init
776 .saturating_sub(DurationNanos::from_mins(5));
777 assert_eq!(order_cmd.start, Some(expected_start));
778 assert_eq!(fill_cmd.start, Some(expected_start));
779 assert_eq!(position_cmd.start, Some(expected_start));
780 assert!(!order_cmd.open_only);
781 assert!(order_cmd.instrument_id.is_none());
782 assert!(order_cmd.end.is_none());
783 assert!(order_cmd.params.is_none());
784 assert!(fill_cmd.instrument_id.is_none());
785 assert!(fill_cmd.venue_order_id.is_none());
786 assert!(fill_cmd.end.is_none());
787 assert!(fill_cmd.params.is_none());
788 assert!(position_cmd.instrument_id.is_none());
789 assert!(position_cmd.end.is_none());
790 assert!(position_cmd.params.is_none());
791 }
792
793 #[rstest]
794 fn generate_mass_status_uses_supplied_clock_for_all_sources() {
795 let client = MassStatusExecutionClient::new(false);
796 let timestamp = UnixNanos::from(900_000_000_123);
797
798 let report = futures::executor::block_on(generate_mass_status(&client, Some(3), timestamp))
799 .unwrap()
800 .unwrap();
801
802 let expected_start = Some(UnixNanos::from(720_000_000_123));
803 assert_eq!(report.ts_init, timestamp);
804 assert_eq!(client.order_commands.borrow()[0].ts_init, timestamp);
805 assert_eq!(client.order_commands.borrow()[0].start, expected_start);
806 assert_eq!(client.fill_requests.borrow()[0].ts_init, timestamp);
807 assert_eq!(client.fill_requests.borrow()[0].start, expected_start);
808 assert_eq!(client.position_queries.borrow()[0].ts_init, timestamp);
809 assert_eq!(client.position_queries.borrow()[0].start, expected_start);
810 }
811
812 #[rstest]
813 fn generate_mass_status_default_propagates_granular_error() {
814 let client = MassStatusExecutionClient::new(true);
815
816 let error = futures::executor::block_on(client.generate_mass_status(Some(5))).unwrap_err();
817
818 let error_chain = format!("{error:#}");
819 assert!(error_chain.contains("failed to generate fill reports"));
820 assert!(error_chain.contains("sentinel fill report failure"));
821 }
822}