1use std::{any::Any, fmt::Debug, time::Duration};
85
86use anyhow::Context;
87use nautilus_common::{
88 actor::{self, Actor, DataActor, DataActorNative},
89 cache::database::{CacheDatabaseAdapter, CacheDatabaseFactory},
90 clients::ExecutionClient,
91 component::Component,
92 enums::{Environment, LogColor},
93 live::{dispatch::DispatchMessage, dst},
94 log_info,
95 messages::{
96 DataEvent, ExecutionEvent, ExecutionReport, SystemCommand, SystemEvent,
97 data::DataCommand,
98 execution::TradingCommand,
99 system::{QueueStateChanged, ReconnectSocket, SocketStateChange, SocketStateChanged},
100 },
101 msgbus::{self, BusMessage, MessagingSwitchboard},
102 runner::{SystemChannel, TimeEventMessage, TradingCommandMessage},
103};
104use nautilus_core::{
105 UUID4,
106 datetime::{mins_to_secs, secs_to_nanos_unchecked},
107};
108#[cfg(test)]
109use nautilus_model::reports::OrderStatusReport;
110use nautilus_model::{
111 events::OrderEventAny,
112 identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId},
113 orders::Order,
114};
115use nautilus_network::mode::ReconnectRequestOutcome;
116#[cfg(feature = "python")]
117use nautilus_system::trader::Trader;
118use nautilus_system::{config::NautilusKernelConfig, kernel::NautilusKernel};
119use nautilus_trading::{
120 ExecutionAlgorithm, ExecutionAlgorithmNative,
121 strategy::{Strategy, StrategyNative},
122};
123use tabled::{builder::Builder, settings::Style};
124
125use crate::{
126 dispatch::drain_callbacks,
127 execution::{
128 client::LiveExecutionClient,
129 manager::{
130 ExecutionManager, ExecutionManagerConfig, TargetedOrderQuery, TargetedOrderReportResult,
131 },
132 },
133 runner::{AsyncRunner, AsyncRunnerChannels, PendingRunnerEvent},
134 socket::{SocketReconnectLookup, SocketReconnectRegistry},
135};
136
137pub mod builder;
138pub mod config;
139
140#[cfg(feature = "plugin")]
141pub mod plugin;
142
143mod metrics;
144mod queue;
145mod reconciliation;
146mod state;
147
148use builder::ExternalMessageBusIngress;
149pub use builder::LiveNodeBuilder;
150use config::{LiveNodeConfig, PluginConfig, validate_live_environment};
151pub use metrics::{RunnerChannelMetricsSnapshot, RunnerMetricsDelta, RunnerMetricsSnapshot};
152use metrics::{RunnerChannelQueueDepths, RunnerMetrics};
153use queue::{QueueMonitor, QueueStateTransition};
154use reconciliation::{
155 OpenOrderReportResult, OpenOrderReportTask, PositionReportTask, PositionReportTaskResult,
156 ReconciliationCheckIntervals, ReconciliationCheckState, ReportTaskOutcome,
157 TargetedOrderReportTask,
158};
159use state::{EngineConnectionStatus, RunningTransition};
160pub use state::{LiveNodeHandle, NodeRunMode, NodeState};
161
162const DISPATCHES_PER_YIELD: usize = 64;
168
169type StreamProcessorCallback = dyn Fn(&dyn Any, &serde_json::Value) -> anyhow::Result<()> + 'static;
170
171struct StreamProcessor(Box<StreamProcessorCallback>);
172
173impl Debug for StreamProcessor {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 f.debug_struct(stringify!(StreamProcessor)).finish()
176 }
177}
178
179#[derive(Debug)]
184pub struct LiveNode {
185 kernel: NautilusKernel,
186 runner: Option<AsyncRunner>,
187 config: LiveNodeConfig,
188 handle: LiveNodeHandle,
189 exec_manager: ExecutionManager,
190 exec_clients: Vec<LiveExecutionClient>,
191 socket_registry: SocketReconnectRegistry,
192 cache_database_factory: Option<Box<dyn CacheDatabaseFactory>>,
193 external_msgbus: Option<ExternalMessageBusIngress>,
194 stream_processors: Vec<StreamProcessor>,
195 shutdown_deadline: Option<dst::time::Instant>,
196 #[cfg(feature = "plugin")]
197 plugins: plugin::NodePlugins,
198}
199
200impl LiveNode {
201 #[must_use]
205 #[allow(
206 clippy::too_many_arguments,
207 reason = "builder components have distinct lifecycle roles"
208 )]
209 pub(crate) fn new_from_builder(
210 kernel: NautilusKernel,
211 runner: AsyncRunner,
212 config: LiveNodeConfig,
213 exec_manager: ExecutionManager,
214 exec_clients: Vec<LiveExecutionClient>,
215 socket_registry: SocketReconnectRegistry,
216 cache_database_factory: Option<Box<dyn CacheDatabaseFactory>>,
217 external_msgbus: Option<ExternalMessageBusIngress>,
218 ) -> Self {
219 Self {
220 kernel,
221 runner: Some(runner),
222 config,
223 handle: LiveNodeHandle::new(),
224 exec_manager,
225 exec_clients,
226 socket_registry,
227 cache_database_factory,
228 external_msgbus,
229 stream_processors: Vec::new(),
230 shutdown_deadline: None,
231 #[cfg(feature = "plugin")]
232 plugins: plugin::NodePlugins,
233 }
234 }
235
236 pub fn builder(
242 trader_id: TraderId,
243 environment: Environment,
244 ) -> anyhow::Result<LiveNodeBuilder> {
245 LiveNodeBuilder::new(trader_id, environment)
246 }
247
248 pub fn build(name: String, config: Option<LiveNodeConfig>) -> anyhow::Result<Self> {
258 let config = config.unwrap_or_default();
259 validate_live_environment(config.environment())?;
260
261 config.validate_runtime_support()?;
262
263 if config.event_store.is_some() {
264 anyhow::bail!(
265 "LiveNodeConfig.event_store is set but LiveNode::build cannot install a factory; \
266 use LiveNodeBuilder::with_event_store(...) instead"
267 );
268 }
269
270 let runner = AsyncRunner::new();
271 runner.bind_senders();
272
273 let kernel = NautilusKernel::new(name, config.clone())?;
274 #[cfg(feature = "python")]
275 if let Some(controller) = config.controller.as_ref() {
276 Trader::add_controller_from_importable_config(&kernel.trader, controller)?;
277 }
278
279 #[cfg(not(feature = "python"))]
280 if let Some(controller) = config.controller.as_ref() {
281 anyhow::bail!(
282 "LiveNodeConfig.controller for importable controller '{}' requires the python feature",
283 controller.controller_path
284 );
285 }
286
287 let exec_manager_config =
288 ExecutionManagerConfig::from(&config.exec_engine).with_trader_id(config.trader_id);
289
290 let exec_manager = ExecutionManager::new(
291 kernel.clock.clone(),
292 kernel.cache.clone(),
293 exec_manager_config,
294 )?;
295
296 let node = Self {
297 kernel,
298 runner: Some(runner),
299 config,
300 handle: LiveNodeHandle::new(),
301 exec_manager,
302 exec_clients: Vec::new(),
303 socket_registry: SocketReconnectRegistry::default(),
304 cache_database_factory: None,
305 external_msgbus: None,
306 stream_processors: Vec::new(),
307 shutdown_deadline: None,
308 #[cfg(feature = "plugin")]
309 plugins: plugin::NodePlugins,
310 };
311
312 node.load_configured_plugins()?;
313
314 log::info!("LiveNode built successfully with kernel config");
315
316 Ok(node)
317 }
318
319 pub(crate) fn load_configured_plugins(&self) -> anyhow::Result<()> {
325 if self.config.plugins.is_empty() {
326 return Ok(());
327 }
328
329 anyhow::bail!(
330 "LiveNodeConfig.plugins requires host-side plug-in support; nautilus-plugin is the guest SDK only"
331 )
332 }
333
334 #[expect(
340 clippy::needless_pass_by_value,
341 reason = "signature mirrors the host-enabled API"
342 )]
343 pub fn add_plugin(&mut self, config: PluginConfig) -> anyhow::Result<()> {
344 #[cfg(feature = "plugin")]
345 config.validate_runtime_support(self.config.plugins.len())?;
346 #[cfg(not(feature = "plugin"))]
347 let _ = config;
348
349 anyhow::bail!(
350 "LiveNode::add_plugin requires host-side plug-in support; nautilus-plugin is the guest SDK only"
351 )
352 }
353
354 #[must_use]
356 pub fn handle(&self) -> LiveNodeHandle {
357 self.handle.clone()
358 }
359
360 pub fn add_stream_processor<F>(&mut self, callback: F)
369 where
370 F: Fn(&dyn Any) + 'static,
371 {
372 self.add_stream_processor_with_mapping(move |message, _| {
373 callback(message);
374 Ok(())
375 });
376 }
377
378 pub(crate) fn add_stream_processor_with_mapping<F>(&mut self, callback: F)
379 where
380 F: Fn(&dyn Any, &serde_json::Value) -> anyhow::Result<()> + 'static,
381 {
382 self.stream_processors
383 .push(StreamProcessor(Box::new(callback)));
384 }
385
386 pub async fn start(&mut self) -> anyhow::Result<()> {
397 if self.state().is_running() {
398 anyhow::bail!("Already running");
399 }
400
401 if self.external_msgbus.is_some() {
402 log::warn!(
403 "External message bus ingress is configured but LiveNode::start() does not service it; use LiveNode::run()"
404 );
405 }
406
407 self.prepare_cache().await?;
408
409 if let Some(runner) = self.runner.as_ref() {
410 runner.bind_senders_for_node(self.handle.clone());
411 }
412
413 self.handle.set_starting();
414
415 self.kernel.reset_shutdown_flag();
416 self.kernel.start_async().await;
417
418 if self.kernel.is_event_store_replay() {
419 log::info!(
420 "Event-store replay loaded; skipping live client connection and reconciliation",
421 );
422
423 if !self.finish_startup_replay().await? {
424 return Ok(());
425 }
426
427 return Ok(());
428 }
429
430 if self.kernel.is_event_store_replay_configured() {
431 self.abort_startup("Event-store replay did not start")
432 .await?;
433 return Ok(());
434 }
435
436 let connection_deadline = dst::time::Instant::now() + self.config.timeout_connection;
437
438 if let Err(e) = self.connect_data_phase(connection_deadline).await {
440 return self
441 .abort_startup_with_error("Data client connection timed out", e)
442 .await;
443 }
444
445 let (startup_system_events, startup_system_commands) =
446 if let Some(runner) = self.runner.as_mut() {
447 runner.flush_pending_data();
448 (
449 runner.drain_pending_system_events(),
450 runner.drain_pending_system_commands(),
451 )
452 } else {
453 (Vec::new(), Vec::new())
454 };
455
456 if let Err(e) = self.connect_exec_clients(connection_deadline).await {
457 return self
458 .abort_startup_with_error("Execution client connection timed out", e)
459 .await;
460 }
461
462 if let Some(reason) = self.startup_abort_reason() {
463 self.abort_startup(reason).await?;
464 return Ok(());
465 }
466
467 match self.await_engines_connected(connection_deadline).await {
468 EngineConnectionStatus::Connected => {}
469 EngineConnectionStatus::TimedOut => {
470 return self
471 .abort_startup_with_error(
472 "Engine readiness timed out",
473 anyhow::anyhow!("readiness timeout while waiting for engine connections"),
474 )
475 .await;
476 }
477 EngineConnectionStatus::StopRequested => {
478 self.abort_startup("Stop signal received during startup")
479 .await?;
480 return Ok(());
481 }
482 EngineConnectionStatus::ShutdownRequested => {
483 self.abort_startup("Shutdown signal received during startup")
484 .await?;
485 return Ok(());
486 }
487 }
488
489 if let Err(e) = self.perform_startup_reconciliation().await {
490 if let Err(finalize_err) = self.abort_startup("Startup reconciliation failed").await {
491 anyhow::bail!(
492 "startup reconciliation failed: {e}; failed to finalize startup abort: {finalize_err}"
493 );
494 }
495
496 return Err(e);
497 }
498
499 if let Some(reason) = self.startup_abort_reason() {
500 self.abort_startup(reason).await?;
501 return Ok(());
502 }
503
504 if let Err(e) = self.kernel.start_trader() {
505 return self.abort_after_trader_start_failure(e).await;
506 }
507
508 #[cfg(feature = "plugin")]
509 if let Err(e) = self.plugins.start_controllers() {
510 return self.abort_after_trader_start_failure(e).await;
511 }
512
513 self.process_system_events(startup_system_events);
514 self.process_system_commands(startup_system_commands);
515
516 if !self.finish_startup_trader(None).await? {
517 return Ok(());
518 }
519
520 Ok(())
521 }
522
523 pub async fn stop(&mut self) -> anyhow::Result<()> {
532 if !self.state().is_running() {
533 anyhow::bail!("Not running");
534 }
535
536 self.handle.set_shutting_down();
537
538 #[cfg(feature = "plugin")]
539 let controller_stop_result = self.plugins.stop_controllers();
540 #[cfg(not(feature = "plugin"))]
541 let controller_stop_result: anyhow::Result<()> = Ok(());
542
543 self.kernel.stop_trader();
544 let delay = self.kernel.delay_post_stop();
545 log::info!("Awaiting residual events ({delay:?})...");
546
547 let residual_events = self.process_runner_for(delay).await;
548 if residual_events > 0 {
549 log::debug!("Processed {residual_events} residual events during shutdown");
550 }
551
552 let stop_result = self.finalize_stop().await;
553 let drained_events = self.drain_runner_pending();
554 if drained_events > 0 {
555 log::info!("Drained {drained_events} remaining events during shutdown");
556 }
557
558 match (controller_stop_result, stop_result) {
559 (Ok(()), Ok(())) => Ok(()),
560 (Err(controller_err), Ok(())) => Err(controller_err),
561 (Ok(()), Err(stop_err)) => Err(stop_err),
562 (Err(controller_err), Err(stop_err)) => {
563 log::error!("Error stopping plug-in controllers: {controller_err}");
564 Err(stop_err)
565 }
566 }
567 }
568
569 pub fn dispose(&mut self) {
574 self.close_external_ingress();
575 self.handle.set_stopped();
576 self.kernel.dispose();
577 drop(self.runner.take());
578
579 if let Some(e) = actor::callback_failure() {
580 log::error!("Callback dispatch failed before disposal cleanup: {e}");
581 }
582
583 if let Err(e) = actor::clear_callbacks() {
584 log::error!("Failed to clear callback dispatch during disposal: {e}");
585 }
586 }
587
588 async fn process_runner_for(&mut self, duration: Duration) -> usize {
589 let Some(mut runner) = self.runner.take() else {
590 dst::time::sleep(duration).await;
591 return 0;
592 };
593
594 runner.bind_senders_for_node(self.handle.clone());
595 let deadline = dst::time::Instant::now() + duration;
596 let mut processed = 0;
597
598 loop {
599 tokio::select! {
600 biased;
601
602 () = dst::time::sleep_until(deadline) => break,
603 event = runner.recv() => {
604 let Some(event) = event else {
605 dst::time::sleep_until(deadline).await;
606 break;
607 };
608
609 self.process_runner_event(event);
610 processed += 1;
611 }
612 }
613 }
614
615 self.runner = Some(runner);
616 processed
617 }
618
619 fn drain_runner_pending(&mut self) -> usize {
620 let Some(mut runner) = self.runner.take() else {
621 return 0;
622 };
623
624 let processed = runner.poll_pending(|event| self.process_runner_event(event));
625 self.runner = Some(runner);
626 processed
627 }
628
629 fn process_runner_event(&mut self, event: PendingRunnerEvent) {
630 match event {
631 PendingRunnerEvent::TimeEvent(message) => {
632 let _ = AsyncRunner::handle_time_event(message);
633 }
634 PendingRunnerEvent::SystemEvent(event) => {
635 event.dispatch(|event| self.process_system_event(event));
636 }
637 PendingRunnerEvent::SystemCommand(command) => {
638 command.dispatch(|command| self.process_system_command(command));
639 }
640 PendingRunnerEvent::ExecEvent(event) => {
641 event.dispatch(|event| self.process_exec_event(event));
642 }
643 PendingRunnerEvent::ExecCommand(command) => self.process_exec_command(command),
644 PendingRunnerEvent::DataEvent(event) => AsyncRunner::dispatch_data_event(event),
645 PendingRunnerEvent::DataCommand(command) => AsyncRunner::handle_data_command(command),
646 }
647 }
648
649 fn process_system_events(&self, events: Vec<DispatchMessage<SystemEvent>>) {
650 for event in events {
651 event.dispatch(|event| self.process_system_event(event));
652 }
653 }
654
655 fn process_system_commands(&self, commands: Vec<DispatchMessage<SystemCommand>>) {
656 for command in commands {
657 command.dispatch(|command| self.process_system_command(command));
658 }
659 }
660
661 fn process_system_command(&self, command: SystemCommand) {
662 match command {
663 SystemCommand::ReconnectSocket(command) => {
664 self.process_socket_reconnect(command);
665 }
666 }
667 }
668
669 fn process_socket_reconnect(&self, command: ReconnectSocket) {
670 let outcome = if command.trader_id == self.config.trader_id {
671 Self::request_socket_reconnect(
672 self.socket_registry
673 .get(command.client_id, command.endpoint),
674 )
675 } else {
676 SocketReconnectDispatchOutcome::InvalidTrader
677 };
678
679 if outcome == SocketReconnectDispatchOutcome::Accepted {
680 log::info!(
681 "Requested socket reconnect for client {} endpoint {}",
682 command.client_id,
683 command.endpoint
684 );
685 } else {
686 log::warn!(
687 "Rejected socket reconnect request for client {} endpoint {}: {outcome:?}",
688 command.client_id,
689 command.endpoint
690 );
691 }
692 }
693
694 fn request_socket_reconnect(lookup: SocketReconnectLookup) -> SocketReconnectDispatchOutcome {
695 match lookup {
696 SocketReconnectLookup::Handle(handle) => match handle.request_reconnect() {
697 ReconnectRequestOutcome::Accepted => SocketReconnectDispatchOutcome::Accepted,
698 ReconnectRequestOutcome::AlreadyReconnecting => {
699 SocketReconnectDispatchOutcome::AlreadyReconnecting
700 }
701 ReconnectRequestOutcome::Disconnected => {
702 SocketReconnectDispatchOutcome::Disconnected
703 }
704 ReconnectRequestOutcome::Closed => SocketReconnectDispatchOutcome::Closed,
705 ReconnectRequestOutcome::Unsupported => SocketReconnectDispatchOutcome::Unsupported,
706 },
707 SocketReconnectLookup::ClientNotFound => SocketReconnectDispatchOutcome::UnknownClient,
708 SocketReconnectLookup::Unsupported => SocketReconnectDispatchOutcome::Unsupported,
709 SocketReconnectLookup::EndpointNotFound => {
710 SocketReconnectDispatchOutcome::UnknownEndpoint
711 }
712 SocketReconnectLookup::AmbiguousEndpoint => {
713 SocketReconnectDispatchOutcome::AmbiguousEndpoint
714 }
715 }
716 }
717
718 fn process_system_event(&self, event: SystemEvent) {
719 match event {
720 SystemEvent::SocketState(change) => self.publish_socket_state_change(change),
721 }
722 }
723
724 fn publish_socket_state_change(&self, change: SocketStateChange) {
725 let timestamp = self.kernel.generate_timestamp_ns();
726
727 let event = SocketStateChanged::new(
728 self.config.trader_id,
729 change.client_id,
730 change.venue,
731 change.endpoint,
732 change.state,
733 UUID4::new(),
734 timestamp,
735 timestamp,
736 );
737
738 msgbus::publish_any(
739 MessagingSwitchboard::socket_state_changed_topic(
740 event.client_id,
741 event.endpoint.as_str(),
742 ),
743 event.as_any(),
744 );
745 }
746
747 async fn await_engines_connected(
751 &self,
752 deadline: dst::time::Instant,
753 ) -> EngineConnectionStatus {
754 log::info!(
755 "Awaiting engine connections ({:?} timeout)...",
756 self.config.timeout_connection
757 );
758
759 let interval = Duration::from_millis(100);
760
761 loop {
762 if self.handle.should_stop() {
763 log::warn!("Stop signal received, aborting connection wait");
764 return EngineConnectionStatus::StopRequested;
765 }
766
767 if self.kernel.is_shutdown_requested() {
768 log::warn!("Shutdown signal received, aborting connection wait");
769 return EngineConnectionStatus::ShutdownRequested;
770 }
771
772 if self.kernel.check_engines_connected() {
773 log::info!("All engine clients connected");
774 return EngineConnectionStatus::Connected;
775 }
776
777 let now = dst::time::Instant::now();
778 if now >= deadline {
779 break;
780 }
781
782 dst::time::sleep(interval.min(deadline - now)).await;
783 }
784
785 self.log_connection_status();
786 EngineConnectionStatus::TimedOut
787 }
788
789 async fn await_engines_disconnected(&self, deadline: dst::time::Instant) -> anyhow::Result<()> {
793 log::info!(
794 "Awaiting engine disconnections ({:?} timeout)...",
795 self.config.timeout_disconnection
796 );
797
798 let timeout = self.config.timeout_disconnection;
799 let interval = Duration::from_millis(100);
800
801 loop {
802 if self.kernel.check_engines_disconnected() {
803 log::info!("All engine clients disconnected");
804 return Ok(());
805 }
806
807 let now = dst::time::Instant::now();
808 if now >= deadline {
809 break;
810 }
811
812 dst::time::sleep(interval.min(deadline - now)).await;
813 }
814
815 log::error!(
816 "Timed out ({:?}) waiting for engines to disconnect\n\
817 DataEngine.check_disconnected() == {}\n\
818 ExecEngine.check_disconnected() == {}",
819 timeout,
820 self.kernel.data_engine().check_disconnected(),
821 self.kernel.exec_engine().borrow().check_disconnected(),
822 );
823 anyhow::bail!("disconnect readiness timeout while waiting for engine disconnections")
824 }
825
826 fn log_connection_status(&self) {
827 let data_status = self.kernel.data_client_connection_status();
828 let exec_status = self.kernel.exec_client_connection_status();
829
830 let mut rows: Vec<ClientStatus> = Vec::new();
831
832 for (client_id, connected) in data_status {
833 rows.push(ClientStatus {
834 client: client_id.to_string(),
835 client_type: "Data",
836 connected,
837 });
838 }
839
840 for (client_id, connected) in exec_status {
841 rows.push(ClientStatus {
842 client: client_id.to_string(),
843 client_type: "Execution",
844 connected,
845 });
846 }
847
848 let table = render_client_statuses(rows);
849
850 log::warn!(
851 "Timed out ({:?}) waiting for engines to connect\n\n{table}\n\n\
852 DataEngine.check_connected() == {}\n\
853 ExecEngine.check_connected() == {}",
854 self.config.timeout_connection,
855 self.kernel.data_engine().check_connected(),
856 self.kernel.exec_engine().borrow().check_connected(),
857 );
858 }
859
860 #[expect(clippy::await_holding_refcell_ref)] async fn perform_startup_reconciliation(&mut self) -> anyhow::Result<()> {
870 if !self.config.exec_engine.reconciliation {
871 log::info!("Startup reconciliation disabled");
872 self.kernel
873 .portfolio
874 .borrow_mut()
875 .initialize_wallet_orders()?;
876 return Ok(());
877 }
878
879 log_info!(
880 "Starting execution state reconciliation...",
881 color = LogColor::Blue
882 );
883
884 let lookback_mins = self
885 .config
886 .exec_engine
887 .reconciliation_lookback_mins
888 .map(u64::from);
889
890 let timeout = self.config.timeout_reconciliation;
891 let start = dst::time::Instant::now();
892 let client_ids = self.kernel.exec_engine.borrow().client_ids();
893
894 for client_id in client_ids {
895 let elapsed = start.elapsed();
896 if elapsed >= timeout {
897 anyhow::bail!("Startup reconciliation timeout reached");
898 }
899
900 let remaining = timeout
901 .checked_sub(elapsed)
902 .expect("elapsed checked against reconciliation timeout");
903
904 log_info!(
905 "Requesting mass status from {}...",
906 client_id,
907 color = LogColor::Blue
908 );
909
910 let mass_status_result = dst::time::timeout(remaining, async {
911 self.kernel
912 .exec_engine
913 .borrow_mut()
914 .generate_mass_status(&client_id, lookback_mins)
915 .await
916 })
917 .await
918 .map_err(|_| {
919 anyhow::anyhow!(
920 "Startup reconciliation timeout reached while requesting mass status from {client_id}"
921 )
922 })?;
923
924 match mass_status_result {
925 Ok(Some(mass_status)) => {
926 log_info!(
927 "Reconciling ExecutionMassStatus for {}",
928 client_id,
929 color = LogColor::Blue
930 );
931
932 let result = self
933 .exec_manager
934 .reconcile_execution_mass_status(&mass_status, &self.kernel.exec_engine);
935
936 anyhow::ensure!(
937 self.kernel
938 .exec_engine
939 .borrow()
940 .get_client(&client_id)
941 .is_some(),
942 "Execution client {client_id} disappeared during startup reconciliation",
943 );
944
945 anyhow::ensure!(
946 result.unresolved_positions.is_empty(),
947 "Unresolved positions during startup reconciliation for {client_id}: {}",
948 result.unresolved_positions.join("; "),
949 );
950
951 if result.events.is_empty() {
952 log_info!(
953 "Reconciliation for {} succeeded",
954 client_id,
955 color = LogColor::Blue
956 );
957 } else {
958 log::info!(
959 color = LogColor::Blue as u8;
960 "Reconciliation for {} processed {} events",
961 client_id,
962 result.events.len()
963 );
964 }
965
966 if !result.external_orders.is_empty() {
968 let exec_engine = self.kernel.exec_engine.borrow();
969
970 let source_client = exec_engine.get_client(&client_id).ok_or_else(|| {
971 anyhow::anyhow!(
972 "Execution client {client_id} disappeared during startup reconciliation"
973 )
974 })?;
975
976 for external in result.external_orders {
977 source_client.register_external_order(
978 external.client_order_id,
979 external.venue_order_id,
980 external.instrument_id,
981 external.strategy_id,
982 external.ts_init,
983 );
984 }
985 }
986 }
987 Ok(None) => {
988 log::warn!(
989 "No mass status available from {client_id} \
990 (likely adapter error when generating reports)"
991 );
992 }
993 Err(e) => {
994 return Err(e).context(format!("Failed to get mass status from {client_id}"));
995 }
996 }
997 }
998
999 self.kernel.portfolio.borrow_mut().initialize_orders();
1000 self.kernel.portfolio.borrow_mut().initialize_positions();
1001 self.kernel
1002 .portfolio
1003 .borrow_mut()
1004 .initialize_wallet_orders()?;
1005
1006 let elapsed_secs = start.elapsed().as_secs_f64();
1007 log_info!(
1008 "Startup reconciliation completed in {:.2}s",
1009 elapsed_secs,
1010 color = LogColor::Blue
1011 );
1012
1013 Ok(())
1014 }
1015
1016 pub async fn run(&mut self) -> anyhow::Result<()> {
1038 self.run_with_mode(NodeRunMode::Owned).await
1039 }
1040
1041 pub async fn run_with_mode(&mut self, mode: NodeRunMode) -> anyhow::Result<()> {
1051 if self.state().is_running() {
1052 anyhow::bail!("Already running");
1053 }
1054
1055 if self.runner.is_none() {
1056 anyhow::bail!("Runner already consumed - run() called twice");
1057 }
1058
1059 self.prepare_cache().await?;
1060
1061 let Some(runner) = self.runner.take() else {
1062 anyhow::bail!("Runner already consumed - run() called twice");
1063 };
1064
1065 runner.bind_senders_for_node(self.handle.clone());
1066
1067 let AsyncRunnerChannels {
1068 mut time_evt_rx,
1069 mut system_evt_rx,
1070 mut system_cmd_rx,
1071 mut exec_evt_rx,
1072 mut exec_cmd_rx,
1073 mut data_evt_rx,
1074 mut data_cmd_rx,
1075 } = runner.take_channels();
1076
1077 log::info!("Event loop starting");
1078
1079 self.handle.set_starting();
1080 self.kernel.reset_shutdown_flag();
1081 self.kernel.start_async().await;
1082
1083 if self.kernel.is_event_store_replay() {
1084 log::info!(
1085 "Event-store replay loaded; skipping live client connection and reconciliation",
1086 );
1087
1088 if !self.finish_startup_replay().await? {
1089 return Ok(());
1090 }
1091
1092 return Ok(());
1093 }
1094
1095 if self.kernel.is_event_store_replay_configured() {
1096 self.abort_startup("Event-store replay did not start")
1097 .await?;
1098 return Ok(());
1099 }
1100
1101 let mut external_msgbus_rx = match self.take_external_ingress_receiver() {
1102 Ok(rx) => rx,
1103 Err(e) => {
1104 let result = self
1105 .abort_startup("External message bus ingress failed to start")
1106 .await;
1107 Self::drain_channels(
1108 &mut time_evt_rx,
1109 &mut system_evt_rx,
1110 &mut system_cmd_rx,
1111 &mut exec_evt_rx,
1112 &mut exec_cmd_rx,
1113 &mut data_evt_rx,
1114 &mut data_cmd_rx,
1115 );
1116 log::info!("Event loop stopped");
1117
1118 if let Err(finalize_err) = result {
1119 anyhow::bail!(
1120 "failed to start external message bus ingress: {e}; failed to finalize startup abort: {finalize_err}"
1121 );
1122 }
1123
1124 return Err(e);
1125 }
1126 };
1127
1128 let stop_handle = self.handle.clone();
1129 let mut pending = PendingEvents::default();
1130 let mut startup_system_events = Vec::new();
1131 let mut startup_system_commands = Vec::new();
1132 let connection_deadline = dst::time::Instant::now() + self.config.timeout_connection;
1133
1134 let data_connect_result = drive_with_event_buffering(
1137 self.connect_data_phase(connection_deadline),
1138 &mut pending,
1139 &mut time_evt_rx,
1140 &mut system_evt_rx,
1141 &mut system_cmd_rx,
1142 &mut exec_evt_rx,
1143 &mut exec_cmd_rx,
1144 &mut data_evt_rx,
1145 &mut data_cmd_rx,
1146 )
1147 .await;
1148
1149 if let Err(e) = data_connect_result {
1150 flush_all_pending(
1151 &mut pending,
1152 &mut time_evt_rx,
1153 &mut system_evt_rx,
1154 &mut system_cmd_rx,
1155 &mut exec_evt_rx,
1156 &mut exec_cmd_rx,
1157 &mut data_evt_rx,
1158 &mut data_cmd_rx,
1159 );
1160 let result = self
1161 .abort_startup_with_error("Data client connection timed out", e)
1162 .await;
1163 Self::drain_channels(
1164 &mut time_evt_rx,
1165 &mut system_evt_rx,
1166 &mut system_cmd_rx,
1167 &mut exec_evt_rx,
1168 &mut exec_cmd_rx,
1169 &mut data_evt_rx,
1170 &mut data_cmd_rx,
1171 );
1172 log::info!("Event loop stopped");
1173 return result;
1174 }
1175
1176 flush_pending_data(&mut pending, &mut data_evt_rx, &mut data_cmd_rx);
1180 startup_system_events.extend(pending.take_system_events());
1181 startup_system_commands.extend(pending.take_system_commands());
1182 debug_assert!(
1183 pending.data_evts.is_empty() && pending.data_cmds.is_empty(),
1184 "data must be drained into cache before exec clients connect",
1185 );
1186
1187 let engine_connection_result = drive_with_event_buffering(
1189 self.connect_exec_phase(connection_deadline),
1190 &mut pending,
1191 &mut time_evt_rx,
1192 &mut system_evt_rx,
1193 &mut system_cmd_rx,
1194 &mut exec_evt_rx,
1195 &mut exec_cmd_rx,
1196 &mut data_evt_rx,
1197 &mut data_cmd_rx,
1198 )
1199 .await;
1200
1201 flush_all_pending(
1203 &mut pending,
1204 &mut time_evt_rx,
1205 &mut system_evt_rx,
1206 &mut system_cmd_rx,
1207 &mut exec_evt_rx,
1208 &mut exec_cmd_rx,
1209 &mut data_evt_rx,
1210 &mut data_cmd_rx,
1211 );
1212 startup_system_events.extend(pending.take_system_events());
1213 startup_system_commands.extend(pending.take_system_commands());
1214 debug_assert!(
1215 pending.is_empty(),
1216 "all startup events must be processed before reconciliation",
1217 );
1218
1219 let engine_connection_status = match engine_connection_result {
1220 Ok(status) => status,
1221 Err(e) => {
1222 let result = self
1223 .abort_startup_with_error("Execution client connection timed out", e)
1224 .await;
1225 Self::drain_channels(
1226 &mut time_evt_rx,
1227 &mut system_evt_rx,
1228 &mut system_cmd_rx,
1229 &mut exec_evt_rx,
1230 &mut exec_cmd_rx,
1231 &mut data_evt_rx,
1232 &mut data_cmd_rx,
1233 );
1234 log::info!("Event loop stopped");
1235 return result;
1236 }
1237 };
1238
1239 if engine_connection_status == EngineConnectionStatus::TimedOut {
1240 let result = self
1241 .abort_startup_with_error(
1242 "Engine readiness timed out",
1243 anyhow::anyhow!("readiness timeout while waiting for engine connections"),
1244 )
1245 .await;
1246 Self::drain_channels(
1247 &mut time_evt_rx,
1248 &mut system_evt_rx,
1249 &mut system_cmd_rx,
1250 &mut exec_evt_rx,
1251 &mut exec_cmd_rx,
1252 &mut data_evt_rx,
1253 &mut data_cmd_rx,
1254 );
1255 log::info!("Event loop stopped");
1256 return result;
1257 }
1258
1259 if let Some(reason) = engine_connection_status
1260 .abort_reason()
1261 .or_else(|| self.startup_abort_reason())
1262 {
1263 self.abort_startup(reason).await?;
1264 Self::drain_channels(
1265 &mut time_evt_rx,
1266 &mut system_evt_rx,
1267 &mut system_cmd_rx,
1268 &mut exec_evt_rx,
1269 &mut exec_cmd_rx,
1270 &mut data_evt_rx,
1271 &mut data_cmd_rx,
1272 );
1273 log::info!("Event loop stopped");
1274 return Ok(());
1275 }
1276
1277 debug_assert_eq!(engine_connection_status, EngineConnectionStatus::Connected);
1278
1279 if let Err(e) = self.perform_startup_reconciliation().await {
1281 let result = self.abort_startup("Startup reconciliation failed").await;
1282 Self::drain_channels(
1283 &mut time_evt_rx,
1284 &mut system_evt_rx,
1285 &mut system_cmd_rx,
1286 &mut exec_evt_rx,
1287 &mut exec_cmd_rx,
1288 &mut data_evt_rx,
1289 &mut data_cmd_rx,
1290 );
1291 log::info!("Event loop stopped");
1292
1293 if let Err(finalize_err) = result {
1294 anyhow::bail!(
1295 "startup reconciliation failed: {e}; failed to finalize startup abort: {finalize_err}"
1296 );
1297 }
1298
1299 return Err(e);
1300 }
1301
1302 if let Some(reason) = self.startup_abort_reason() {
1303 let result = self.abort_startup(reason).await;
1304 Self::drain_channels(
1305 &mut time_evt_rx,
1306 &mut system_evt_rx,
1307 &mut system_cmd_rx,
1308 &mut exec_evt_rx,
1309 &mut exec_cmd_rx,
1310 &mut data_evt_rx,
1311 &mut data_cmd_rx,
1312 );
1313 log::info!("Event loop stopped");
1314 return result;
1315 }
1316
1317 if let Err(e) = self.kernel.start_trader() {
1318 let result = self.abort_after_trader_start_failure(e).await;
1319 Self::drain_channels(
1320 &mut time_evt_rx,
1321 &mut system_evt_rx,
1322 &mut system_cmd_rx,
1323 &mut exec_evt_rx,
1324 &mut exec_cmd_rx,
1325 &mut data_evt_rx,
1326 &mut data_cmd_rx,
1327 );
1328 log::info!("Event loop stopped");
1329 return result;
1330 }
1331
1332 #[cfg(feature = "plugin")]
1333 if let Err(e) = self.plugins.start_controllers() {
1334 let result = self.abort_after_trader_start_failure(e).await;
1335 Self::drain_channels(
1336 &mut time_evt_rx,
1337 &mut system_evt_rx,
1338 &mut system_cmd_rx,
1339 &mut exec_evt_rx,
1340 &mut exec_cmd_rx,
1341 &mut data_evt_rx,
1342 &mut data_cmd_rx,
1343 );
1344 log::info!("Event loop stopped");
1345 return result;
1346 }
1347
1348 self.process_system_events(startup_system_events);
1349 self.process_system_commands(startup_system_commands);
1350
1351 let finish_result = {
1352 let mut receivers = RunnerReceivers {
1353 time_evt: &mut time_evt_rx,
1354 system_evt: &mut system_evt_rx,
1355 system_cmd: &mut system_cmd_rx,
1356 data_evt: &mut data_evt_rx,
1357 data_cmd: &mut data_cmd_rx,
1358 exec_evt: &mut exec_evt_rx,
1359 exec_cmd: &mut exec_cmd_rx,
1360 };
1361
1362 self.finish_startup_trader(Some(&mut receivers)).await
1363 };
1364
1365 match finish_result {
1366 Ok(true) => {}
1367 result => {
1368 log::info!("Event loop stopped");
1369 return result.map(|_| ());
1370 }
1371 }
1372
1373 let exec_config = &self.config.exec_engine;
1374 let inflight_interval =
1375 Duration::from_millis(u64::from(exec_config.inflight_check_interval_ms));
1376
1377 let open_interval = exec_config
1378 .open_check_interval_secs
1379 .filter(|&s| s > 0.0)
1380 .map_or(Duration::ZERO, |secs| {
1381 Duration::from_nanos(secs_to_nanos_unchecked(secs))
1382 });
1383
1384 let position_interval = exec_config
1385 .position_check_interval_secs
1386 .filter(|&s| s > 0.0)
1387 .map_or(Duration::ZERO, |secs| {
1388 Duration::from_nanos(secs_to_nanos_unchecked(secs))
1389 });
1390
1391 let has_clients = !self
1392 .kernel
1393 .exec_engine
1394 .borrow()
1395 .get_all_clients()
1396 .is_empty();
1397 let recon_enabled = has_clients
1398 && (!inflight_interval.is_zero()
1399 || !open_interval.is_zero()
1400 || !position_interval.is_zero());
1401
1402 let recon_min_interval = if recon_enabled {
1403 let mut intervals = Vec::new();
1404
1405 if !inflight_interval.is_zero() {
1406 intervals.push(inflight_interval);
1407 }
1408
1409 if !open_interval.is_zero() {
1410 intervals.push(open_interval);
1411 }
1412
1413 if !position_interval.is_zero() {
1414 intervals.push(position_interval);
1415 }
1416
1417 intervals
1418 .into_iter()
1419 .min()
1420 .unwrap_or(Duration::from_secs(1))
1421 } else {
1422 Duration::from_secs(1) };
1424
1425 let startup_delay = if self.config.exec_engine.reconciliation {
1430 Duration::from_secs_f64(exec_config.reconciliation_startup_delay_secs)
1431 } else {
1432 Duration::ZERO
1433 };
1434
1435 let recon_start = dst::time::Instant::now() + startup_delay;
1436
1437 let mut last_inflight_check = dst::time::Instant::now();
1438 let mut last_open_check = last_inflight_check;
1439 let mut last_position_check = last_inflight_check;
1440
1441 let far_future = Duration::from_hours(24 * 365 * 100);
1444
1445 let make_schedule = |opt_dur: Option<Duration>| -> (Duration, dst::time::Instant) {
1446 let dur = opt_dur.unwrap_or(far_future);
1447 (dur, recon_start + dur)
1448 };
1449
1450 let (recon_interval, mut recon_next) = make_schedule(if recon_enabled {
1451 Some(recon_min_interval)
1452 } else {
1453 None
1454 });
1455
1456 let (purge_orders_interval, mut purge_orders_next) = make_schedule(
1457 exec_config
1458 .purge_closed_orders_interval_mins
1459 .filter(|&m| m > 0)
1460 .map(|m| Duration::from_secs(mins_to_secs(u64::from(m)))),
1461 );
1462
1463 let (purge_positions_interval, mut purge_positions_next) = make_schedule(
1464 exec_config
1465 .purge_closed_positions_interval_mins
1466 .filter(|&m| m > 0)
1467 .map(|m| Duration::from_secs(mins_to_secs(u64::from(m)))),
1468 );
1469
1470 let (purge_account_interval, mut purge_account_next) = make_schedule(
1471 exec_config
1472 .purge_account_events_interval_mins
1473 .filter(|&m| m > 0)
1474 .map(|m| Duration::from_secs(mins_to_secs(u64::from(m)))),
1475 );
1476
1477 let (own_books_interval, mut own_books_next) = make_schedule(
1478 exec_config
1479 .own_books_audit_interval_secs
1480 .filter(|&s| s > 0.0)
1481 .map(Duration::from_secs_f64),
1482 );
1483
1484 let (prune_fills_interval, mut prune_fills_next) =
1485 make_schedule(Some(Duration::from_mins(1)));
1486
1487 let mut maintenance_timer = dst::time::interval(Duration::from_millis(100));
1488 maintenance_timer.set_missed_tick_behavior(dst::time::MissedTickBehavior::Skip);
1489
1490 let mut stop_check_timer = dst::time::interval(Duration::from_millis(100));
1495 stop_check_timer.set_missed_tick_behavior(dst::time::MissedTickBehavior::Skip);
1496
1497 let mut residual_events = 0usize;
1499 let mut open_order_report_task: Option<OpenOrderReportTask> = None;
1500 let mut targeted_order_report_task: Option<TargetedOrderReportTask> = None;
1501 let mut position_report_task: Option<PositionReportTask> = None;
1502
1503 let owns_signals = mode.owns_signals();
1506
1507 let ctrl_c = async move {
1508 if owns_signals {
1509 dst::signal::ctrl_c().await
1510 } else {
1511 std::future::pending::<std::io::Result<()>>().await
1512 }
1513 };
1514
1515 let terminate = async move {
1516 if owns_signals {
1517 dst::signal::terminate().await
1518 } else {
1519 std::future::pending::<std::io::Result<()>>().await
1520 }
1521 };
1522
1523 tokio::pin!(ctrl_c);
1524 tokio::pin!(terminate);
1525
1526 let metrics = self.handle.metrics.clone();
1527 let metrics_start = dst::time::Instant::now();
1528 metrics.reset();
1529
1530 let mut queue_monitor = self
1531 .config
1532 .queue_monitor
1533 .as_ref()
1534 .map(|config| QueueMonitor::new(config, metrics.snapshot()));
1535 let mut dispatches_since_yield = 0usize;
1536
1537 let dispatch_result = loop {
1538 let callbacks_pending = match drain_callbacks().await {
1539 Ok(pending) => pending,
1540 Err(e) => {
1541 if self.state() == NodeState::Running {
1542 self.initiate_shutdown();
1543 }
1544
1545 log::warn!(
1546 "Skipping residual events and final buffered dispatch after callback failure"
1547 );
1548
1549 break Err(e);
1550 }
1551 };
1552
1553 let shutdown_deadline = self.shutdown_deadline;
1554 let is_shutting_down = self.state() == NodeState::ShuttingDown;
1555 let is_running = self.state() == NodeState::Running;
1556
1557 tokio::select! {
1558 biased;
1559
1560 result = &mut ctrl_c, if is_running => {
1562 match result {
1563 Ok(()) => log::info!("Received SIGINT, shutting down"),
1564 Err(e) => log::error!("Failed to listen for SIGINT: {e}"),
1565 }
1566 self.initiate_shutdown();
1567 }
1568 result = &mut terminate, if is_running => {
1569 match result {
1570 Ok(()) => log::info!("Received SIGTERM, shutting down"),
1571 Err(e) => log::error!("Failed to listen for SIGTERM: {e}"),
1572 }
1573 self.initiate_shutdown();
1574 }
1575 _ = stop_check_timer.tick(), if is_running => {
1576 if stop_handle.should_stop() {
1577 log::info!("Received stop signal from handle");
1578 self.initiate_shutdown();
1579 } else if self.kernel.is_shutdown_requested() {
1580 log::info!("Received ShutdownSystem command, shutting down");
1581 self.initiate_shutdown();
1582 }
1583 }
1584 () = async {
1585 match shutdown_deadline {
1586 Some(deadline) => dst::time::sleep_until(deadline).await,
1587 None => std::future::pending::<()>().await,
1588 }
1589 }, if self.state() == NodeState::ShuttingDown => {
1590 break Ok(());
1591 }
1592 () = std::future::ready(()), if callbacks_pending => {},
1593 result = async {
1594 match open_order_report_task.as_mut() {
1595 Some(task) => task.future.as_mut().await,
1596 None => std::future::pending::<ReportTaskOutcome<OpenOrderReportResult>>().await,
1597 }
1598 }, if open_order_report_task.is_some() => {
1599 let maintenance_start = dst::time::Instant::now();
1600
1601 drop(open_order_report_task.take());
1602
1603 match result {
1604 ReportTaskOutcome::Completed(result) => {
1605 let client_refs = self
1606 .exec_clients
1607 .iter()
1608 .map(|client| client as &dyn ExecutionClient)
1609 .collect::<Vec<_>>();
1610 let reconciliation = self.exec_manager.reconcile_open_order_reports(
1611 &result.check,
1612 result.reports,
1613 &result.queried_clients,
1614 &result.failed_clients,
1615 &client_refs,
1616 );
1617 self.process_reconciliation_events(&reconciliation.events);
1618 if !reconciliation.targeted_queries.is_empty() {
1619 if is_shutting_down {
1620 let planned_client_order_ids = reconciliation
1621 .targeted_queries
1622 .iter()
1623 .map(TargetedOrderQuery::client_order_id)
1624 .collect::<Vec<_>>();
1625 self.cleanup_cancelled_report_tasks(
1626 &planned_client_order_ids,
1627 );
1628 } else {
1629 targeted_order_report_task = Some(
1630 self.start_targeted_order_report_check(
1631 reconciliation.targeted_queries,
1632 ),
1633 );
1634 }
1635 }
1636 }
1637 ReportTaskOutcome::TimedOut => {
1638 self.cleanup_cancelled_report_tasks(&[]);
1639 log::warn!(
1640 "Open-order report collection expired after {:?}",
1641 self.config.timeout_reconciliation,
1642 );
1643 }
1644 }
1645 record_runner_maintenance(&metrics, maintenance_start, metrics_start);
1646 }
1647 result = async {
1648 match targeted_order_report_task.as_mut() {
1649 Some(task) => task.future.as_mut().await,
1650 None => std::future::pending::<ReportTaskOutcome<Vec<TargetedOrderReportResult>>>().await,
1651 }
1652 }, if targeted_order_report_task.is_some() => {
1653 let maintenance_start = dst::time::Instant::now();
1654
1655 let planned_client_order_ids = targeted_order_report_task
1656 .as_ref()
1657 .map(|task| task.planned_client_order_ids.clone())
1658 .unwrap_or_default();
1659 drop(targeted_order_report_task.take());
1660
1661 match result {
1662 ReportTaskOutcome::Completed(result) => {
1663 let client_refs = self
1664 .exec_clients
1665 .iter()
1666 .map(|client| client as &dyn ExecutionClient)
1667 .collect::<Vec<_>>();
1668 let events = self
1669 .exec_manager
1670 .reconcile_targeted_order_reports(result, &client_refs);
1671 self.process_reconciliation_events(&events);
1672 }
1673 ReportTaskOutcome::TimedOut => {
1674 self.cleanup_cancelled_report_tasks(&planned_client_order_ids);
1675 log::warn!(
1676 "Targeted order report collection expired after {:?}",
1677 self.config.timeout_reconciliation,
1678 );
1679 }
1680 }
1681 record_runner_maintenance(&metrics, maintenance_start, metrics_start);
1682 }
1683 result = async {
1684 match position_report_task.as_mut() {
1685 Some(task) => task.future.as_mut().await,
1686 None => std::future::pending::<ReportTaskOutcome<PositionReportTaskResult>>().await,
1687 }
1688 }, if position_report_task.is_some() => {
1689 let maintenance_start = dst::time::Instant::now();
1690
1691 drop(position_report_task.take());
1692
1693 match result {
1694 ReportTaskOutcome::Completed(PositionReportTaskResult::Positions(result)) => {
1695 if is_shutting_down {
1696 self.cleanup_cancelled_report_tasks(&[]);
1697 } else {
1698 position_report_task = self.handle_position_report_result(result);
1699 }
1700 }
1701 ReportTaskOutcome::Completed(PositionReportTaskResult::Fills(result)) => {
1702 self.handle_position_fill_report_result(result);
1703 }
1704 ReportTaskOutcome::TimedOut => {
1705 self.cleanup_cancelled_report_tasks(&[]);
1706 log::warn!(
1707 "Position report collection expired after {:?}",
1708 self.config.timeout_reconciliation,
1709 );
1710 }
1711 }
1712 record_runner_maintenance(&metrics, maintenance_start, metrics_start);
1713 }
1714
1715 _ = maintenance_timer.tick(), if is_running => {
1718 let maintenance_start = dst::time::Instant::now();
1719 metrics.publish_queue_depths(
1720 RunnerChannelQueueDepths::from_receivers(
1721 &time_evt_rx,
1722 &exec_evt_rx,
1723 &exec_cmd_rx,
1724 &data_evt_rx,
1725 &data_cmd_rx,
1726 ),
1727 metrics_start.elapsed(),
1728 );
1729
1730 if let Some(queue_monitor) = queue_monitor.as_mut() {
1731 let transitions = queue_monitor.evaluate(metrics.snapshot());
1732 self.publish_queue_state_transitions(&transitions);
1733 }
1734
1735 let mut now = dst::time::Instant::now();
1736
1737 if recon_enabled && now >= recon_next {
1738 let recon_intervals = ReconciliationCheckIntervals {
1739 inflight: inflight_interval,
1740 open: open_interval,
1741 position: position_interval,
1742 };
1743 let mut recon_state = ReconciliationCheckState {
1744 last_inflight_check: &mut last_inflight_check,
1745 last_open_check: &mut last_open_check,
1746 last_position_check: &mut last_position_check,
1747 open_order_report_task: &mut open_order_report_task,
1748 targeted_order_report_task: &mut targeted_order_report_task,
1749 position_report_task: &mut position_report_task,
1750 };
1751
1752 self.run_reconciliation_checks(
1753 now,
1754 recon_intervals,
1755 &mut recon_state,
1756 );
1757
1758 now = dst::time::Instant::now();
1759 recon_next = now + recon_interval;
1760 }
1761
1762 if now >= purge_orders_next {
1763 self.exec_manager.purge_closed_orders();
1764 purge_orders_next = now + purge_orders_interval;
1765 }
1766
1767 if now >= purge_positions_next {
1768 self.exec_manager.purge_closed_positions();
1769 purge_positions_next = now + purge_positions_interval;
1770 }
1771
1772 if now >= purge_account_next {
1773 self.exec_manager.purge_account_events();
1774 purge_account_next = now + purge_account_interval;
1775 }
1776
1777 if now >= own_books_next {
1778 self.kernel.cache().borrow_mut().audit_own_order_books();
1779 own_books_next = now + own_books_interval;
1780 }
1781
1782 if now >= prune_fills_next {
1783 self.exec_manager.prune_recent_fills_cache(60.0);
1784 self.exec_manager.prune_processed_fills();
1785 self.exec_manager.prune_order_local_activity();
1786 prune_fills_next = now + prune_fills_interval;
1787 }
1788
1789 record_runner_maintenance(&metrics, maintenance_start, metrics_start);
1790 }
1791
1792 Some(handler) = time_evt_rx.recv() => {
1797 let dispatch_start = dst::time::Instant::now();
1798 let dispatched = AsyncRunner::handle_time_event(handler);
1799
1800 if dispatched && is_shutting_down {
1801 log::debug!("Residual time event");
1802 residual_events += 1;
1803 }
1804
1805 if dispatched {
1806 record_runner_dispatch(
1807 &metrics,
1808 SystemChannel::TimeEvents,
1809 dispatch_start,
1810 metrics_start,
1811 );
1812 }
1813 }
1814 Some(event) = system_evt_rx.recv() => {
1815 if is_shutting_down {
1816 log::debug!("Residual system event: {event}");
1817 residual_events += 1;
1818 }
1819 event.dispatch(|event| self.process_system_event(event));
1820 }
1821 Some(command) = system_cmd_rx.recv() => {
1822 if is_shutting_down {
1823 log::debug!("Residual system command: {command}");
1824 residual_events += 1;
1825 }
1826 command.dispatch(|command| self.process_system_command(command));
1827 }
1828 Some(evt) = exec_evt_rx.recv() => {
1829 let dispatch_start = dst::time::Instant::now();
1830
1831 if is_shutting_down {
1832 log::debug!("Residual exec event: {evt}");
1833 residual_events += 1;
1834 }
1835
1836 evt.dispatch(|evt| self.process_exec_event(evt));
1837 record_runner_dispatch(
1838 &metrics,
1839 SystemChannel::ExecEvents,
1840 dispatch_start,
1841 metrics_start,
1842 );
1843 }
1844 Some(cmd) = exec_cmd_rx.recv() => {
1845 let dispatch_start = dst::time::Instant::now();
1846
1847 if is_shutting_down {
1848 log::debug!("Residual exec command: {cmd}");
1849 residual_events += 1;
1850 }
1851
1852 self.process_exec_command(cmd);
1853 record_runner_dispatch(
1854 &metrics,
1855 SystemChannel::ExecCommands,
1856 dispatch_start,
1857 metrics_start,
1858 );
1859 }
1860 message = recv_external_msgbus_message(&mut external_msgbus_rx) => {
1861 let external_msgbus_start = dst::time::Instant::now();
1862
1863 match message {
1864 Some(message) => {
1865 if is_shutting_down {
1866 log::debug!("Residual external message bus message: {message}");
1867 residual_events += 1;
1868 }
1869 self.process_external_msgbus_message(&message);
1870 }
1871 None => {
1872 log::info!("External message bus ingress closed");
1873 external_msgbus_rx = None;
1874 self.close_external_ingress();
1875 }
1876 }
1877
1878 record_runner_external_msgbus(
1879 &metrics,
1880 external_msgbus_start,
1881 metrics_start,
1882 );
1883 }
1884 Some(evt) = data_evt_rx.recv() => {
1885 let dispatch_start = dst::time::Instant::now();
1886
1887 if is_shutting_down {
1888 log::debug!("Residual data event: {evt:?}");
1889 residual_events += 1;
1890 }
1891 AsyncRunner::dispatch_data_event(evt);
1892 record_runner_dispatch(
1893 &metrics,
1894 SystemChannel::DataEvents,
1895 dispatch_start,
1896 metrics_start,
1897 );
1898 }
1899 Some(cmd) = data_cmd_rx.recv() => {
1900 let dispatch_start = dst::time::Instant::now();
1901
1902 if is_shutting_down {
1903 log::debug!("Residual data command: {cmd:?}");
1904 residual_events += 1;
1905 }
1906 AsyncRunner::handle_data_command(cmd);
1907 record_runner_dispatch(
1908 &metrics,
1909 SystemChannel::DataCommands,
1910 dispatch_start,
1911 metrics_start,
1912 );
1913 }
1914 }
1915
1916 dispatches_since_yield += 1;
1917 if dispatches_since_yield >= DISPATCHES_PER_YIELD {
1918 dispatches_since_yield = 0;
1919 tokio::task::yield_now().await;
1920 }
1921 };
1922
1923 if residual_events > 0 {
1924 log::debug!("Processed {residual_events} residual events during shutdown");
1925 }
1926
1927 self.cancel_report_tasks(
1928 &mut open_order_report_task,
1929 &mut targeted_order_report_task,
1930 &mut position_report_task,
1931 );
1932 drop(external_msgbus_rx.take());
1933 let _ = self.kernel.cache().borrow().check_residuals();
1934
1935 let stop_result = self.finalize_stop().await;
1936
1937 if let Err(e) = dispatch_result {
1938 if let Err(stop_err) = stop_result {
1939 log::error!("Failed to finalize node after callback failure: {stop_err}");
1940 }
1941
1942 return Err(e.into());
1943 }
1944
1945 Self::drain_channels(
1947 &mut time_evt_rx,
1948 &mut system_evt_rx,
1949 &mut system_cmd_rx,
1950 &mut exec_evt_rx,
1951 &mut exec_cmd_rx,
1952 &mut data_evt_rx,
1953 &mut data_cmd_rx,
1954 );
1955
1956 log::info!("Event loop stopped");
1957
1958 stop_result
1959 }
1960
1961 fn publish_queue_state_transitions(&self, transitions: &[QueueStateTransition]) {
1962 for transition in transitions {
1963 let topic = MessagingSwitchboard::queue_state_changed_topic(transition.channel);
1964 let timestamp = self.kernel.generate_timestamp_ns();
1965
1966 let event = QueueStateChanged::new(
1967 self.config.trader_id,
1968 transition.channel,
1969 transition.condition,
1970 transition.state,
1971 transition.queue_depth,
1972 transition.mean_dispatch_ns,
1973 UUID4::new(),
1974 timestamp,
1975 timestamp,
1976 );
1977
1978 msgbus::publish_any(topic, event.as_any());
1979 }
1980 }
1981
1982 #[expect(
1983 clippy::await_holding_refcell_ref,
1984 reason = "cache loading is serialized before the single-threaded live node starts"
1985 )]
1986 async fn prepare_cache(&mut self) -> anyhow::Result<()> {
1987 self.install_cache_database().await?;
1988
1989 let cache = self.kernel.cache();
1990 if !cache.borrow().has_backing() {
1991 return Ok(());
1992 }
1993
1994 if self
1995 .config
1996 .cache
1997 .as_ref()
1998 .is_some_and(|config| config.flush_on_start)
1999 {
2000 cache.borrow_mut().flush_db();
2001 return Ok(());
2002 }
2003
2004 if self.config.exec_engine.load_cache {
2005 self.kernel
2006 .exec_engine()
2007 .borrow_mut()
2008 .load_cache()
2009 .await
2010 .context("Failed to load persistent cache")?;
2011 }
2012
2013 Ok(())
2014 }
2015
2016 #[must_use]
2018 pub const fn has_pending_cache_database(&self) -> bool {
2019 self.cache_database_factory.is_some()
2020 }
2021
2022 async fn install_cache_database(&mut self) -> anyhow::Result<()> {
2027 let Some(factory) = self.cache_database_factory.as_ref() else {
2028 return Ok(());
2029 };
2030
2031 let config = self.config.cache.clone().unwrap_or_default();
2032
2033 let database = factory
2036 .create(self.config.trader_id, self.kernel.instance_id, config)
2037 .await
2038 .context("failed to create cache database backing")?;
2039 self.kernel.cache().borrow_mut().set_database(database);
2040 self.cache_database_factory = None;
2041
2042 Ok(())
2043 }
2044
2045 fn take_external_ingress_receiver(
2046 &mut self,
2047 ) -> anyhow::Result<Option<tokio::sync::mpsc::Receiver<BusMessage>>> {
2048 let Some(external_ingress) = self.external_msgbus.as_mut() else {
2049 return Ok(None);
2050 };
2051
2052 let receiver = external_ingress.take_receiver()?;
2053 log::info!("External message bus ingress started");
2054 Ok(Some(receiver))
2055 }
2056
2057 fn republish_external_msgbus_message(message: &BusMessage) {
2058 if let Err(e) = msgbus::republish_external_message(message) {
2059 log::error!(
2060 "Failed to republish external message bus topic '{}': {e:#}",
2061 message.topic
2062 );
2063 }
2064 }
2065
2066 fn process_external_msgbus_message(&self, message: &BusMessage) {
2067 if self.stream_processors.is_empty() {
2068 Self::republish_external_msgbus_message(message);
2069 return;
2070 }
2071
2072 let mut process = |value: &dyn Any, mapping: &serde_json::Value| {
2073 for processor in &self.stream_processors {
2074 (processor.0)(value, mapping)?;
2075 }
2076
2077 Ok(())
2078 };
2079
2080 if let Err(e) = msgbus::process_external_typed_message(message, &mut process) {
2081 log::error!(
2082 "Failed to process external message bus topic '{}': {e:#}",
2083 message.topic
2084 );
2085 }
2086 }
2087
2088 fn close_external_ingress(&mut self) {
2089 if let Some(external_ingress) = self.external_msgbus.as_mut()
2090 && !external_ingress.is_closed()
2091 {
2092 external_ingress.close();
2093 }
2094 }
2095
2096 fn process_reconciliation_events(&mut self, events: &[OrderEventAny]) {
2097 if events.is_empty() {
2098 return;
2099 }
2100
2101 log::info!(
2102 "Processing {} reconciliation event{}",
2103 events.len(),
2104 if events.len() == 1 { "" } else { "s" }
2105 );
2106
2107 for event in events {
2108 self.exec_manager
2109 .record_local_activity(event.client_order_id());
2110 if let OrderEventAny::Filled(fill) = event {
2111 self.exec_manager
2112 .record_position_activity(fill.instrument_id, fill.account_id);
2113 }
2114
2115 self.kernel.exec_engine.borrow_mut().process(event);
2116 if let OrderEventAny::Filled(fill) = event {
2117 self.exec_manager.commit_recent_fill_if_applied(fill);
2118 }
2119 }
2120 }
2121
2122 fn process_exec_event(&mut self, event: ExecutionEvent) {
2123 let Some(close_ids) = self.observe_exec_event_before_dispatch(&event) else {
2124 return;
2125 };
2126
2127 self.dispatch_exec_event_and_commit_fill(event);
2128
2129 for client_order_id in &close_ids {
2130 let is_closed = self
2131 .kernel
2132 .cache()
2133 .borrow()
2134 .order(client_order_id)
2135 .is_some_and(|order| order.is_closed());
2136 if is_closed {
2137 self.exec_manager
2138 .clear_recon_tracking(client_order_id, true);
2139 }
2140 }
2141 }
2142
2143 fn process_exec_command(&mut self, message: DispatchMessage<TradingCommandMessage>) {
2144 message.dispatch_trading(|message| {
2145 if message.endpoint() == MessagingSwitchboard::exec_engine_execute() {
2146 self.observe_exec_command_before_dispatch(message.command());
2147 }
2148 });
2149 }
2150
2151 fn dispatch_exec_event_and_commit_fill(&mut self, evt: ExecutionEvent) {
2160 let recent_fill_candidate = match &evt {
2161 ExecutionEvent::Order(OrderEventAny::Filled(fill)) => Some(fill.clone()),
2162 _ => None,
2163 };
2164
2165 AsyncRunner::handle_exec_event(evt);
2166
2167 if let Some(fill) = &recent_fill_candidate {
2168 self.exec_manager.commit_recent_fill_if_applied(fill);
2169 }
2170 }
2171
2172 async fn connect_data_phase(&mut self, deadline: dst::time::Instant) -> anyhow::Result<()> {
2173 let remaining = deadline.saturating_duration_since(dst::time::Instant::now());
2179 dst::time::timeout(remaining, self.kernel.connect_data_clients())
2180 .await
2181 .map_err(|_| anyhow::anyhow!("data-connect timeout"))
2182 }
2183
2184 async fn connect_exec_clients(&mut self, deadline: dst::time::Instant) -> anyhow::Result<()> {
2185 let remaining = deadline.saturating_duration_since(dst::time::Instant::now());
2186 dst::time::timeout(remaining, self.kernel.connect_exec_clients())
2187 .await
2188 .map_err(|_| anyhow::anyhow!("exec-connect timeout"))
2189 }
2190
2191 async fn connect_exec_phase(
2196 &mut self,
2197 deadline: dst::time::Instant,
2198 ) -> anyhow::Result<EngineConnectionStatus> {
2199 self.connect_exec_clients(deadline).await?;
2200 Ok(self.await_engines_connected(deadline).await)
2201 }
2202
2203 fn startup_abort_reason(&self) -> Option<&'static str> {
2204 if self.handle.should_stop() {
2205 Some("Stop signal received during startup")
2206 } else if self.kernel.is_shutdown_requested() {
2207 Some("Shutdown signal received during startup")
2208 } else {
2209 None
2210 }
2211 }
2212
2213 async fn finish_startup_replay(&mut self) -> anyhow::Result<bool> {
2214 match self.handle.try_set_running() {
2215 RunningTransition::Entered => Ok(true),
2216 RunningTransition::StopRequested => {
2217 self.abort_startup("Stop signal received during startup")
2218 .await?;
2219 Ok(false)
2220 }
2221 RunningTransition::Invalid(control) => {
2222 self.abort_startup_with_error(
2223 "Invalid lifecycle state during startup",
2224 anyhow::anyhow!(
2225 "Invalid LiveNode control state {control:#04x} while entering Running"
2226 ),
2227 )
2228 .await?;
2229 Ok(false)
2230 }
2231 }
2232 }
2233
2234 async fn finish_startup_trader(
2235 &mut self,
2236 receivers: Option<&mut RunnerReceivers<'_>>,
2237 ) -> anyhow::Result<bool> {
2238 match self.handle.try_set_running() {
2239 RunningTransition::Entered => Ok(true),
2240 RunningTransition::StopRequested => {
2241 self.abort_started_trader("Stop signal received during startup", receivers)
2242 .await?;
2243 Ok(false)
2244 }
2245 RunningTransition::Invalid(control) => {
2246 let state_err = anyhow::anyhow!(
2247 "Invalid LiveNode control state {control:#04x} while entering Running"
2248 );
2249
2250 match self
2251 .abort_started_trader("Invalid lifecycle state during startup", receivers)
2252 .await
2253 {
2254 Ok(()) => Err(state_err),
2255 Err(finalize_err) => {
2256 anyhow::bail!(
2257 "{state_err}; failed to finalize startup abort: {finalize_err}"
2258 )
2259 }
2260 }
2261 }
2262 }
2263 }
2264
2265 async fn abort_startup(&mut self, reason: &str) -> anyhow::Result<()> {
2266 log::info!("{reason}, aborting startup");
2267 self.handle.set_shutting_down();
2268 self.finalize_stop().await
2269 }
2270
2271 async fn abort_startup_with_error(
2272 &mut self,
2273 reason: &str,
2274 startup_err: anyhow::Error,
2275 ) -> anyhow::Result<()> {
2276 match self.abort_startup(reason).await {
2277 Ok(()) => Err(startup_err),
2278 Err(finalize_err) => {
2279 anyhow::bail!("{startup_err}; failed to finalize startup abort: {finalize_err}")
2280 }
2281 }
2282 }
2283
2284 async fn abort_started_trader(
2285 &mut self,
2286 reason: &str,
2287 mut receivers: Option<&mut RunnerReceivers<'_>>,
2288 ) -> anyhow::Result<()> {
2289 log::info!("{reason}, aborting startup");
2290 self.handle.set_shutting_down();
2291
2292 #[cfg(feature = "plugin")]
2293 let controller_stop_result = self.plugins.stop_controllers();
2294 #[cfg(not(feature = "plugin"))]
2295 let controller_stop_result: anyhow::Result<()> = Ok(());
2296
2297 let trader_stop_result = self.kernel.stop_trader_after_start_failure();
2298 let delay = self.kernel.delay_post_stop();
2299 log::info!("Awaiting residual events ({delay:?})...");
2300
2301 let residual_events = match receivers.as_mut() {
2302 Some(receivers) => self.process_receivers_for(delay, receivers).await,
2303 None => self.process_runner_for(delay).await,
2304 };
2305
2306 if residual_events > 0 {
2307 log::debug!("Processed {residual_events} residual events during shutdown");
2308 }
2309
2310 let finalize_result = self.finalize_stop().await;
2311
2312 if let Some(receivers) = receivers {
2313 Self::drain_channels(
2314 receivers.time_evt,
2315 receivers.system_evt,
2316 receivers.system_cmd,
2317 receivers.exec_evt,
2318 receivers.exec_cmd,
2319 receivers.data_evt,
2320 receivers.data_cmd,
2321 );
2322 } else {
2323 let drained_events = self.drain_runner_pending();
2324 if drained_events > 0 {
2325 log::info!("Drained {drained_events} remaining events during shutdown");
2326 }
2327 }
2328
2329 let mut errors = Vec::new();
2330
2331 if let Err(e) = controller_stop_result {
2332 errors.push(format!("Failed to stop plug-in controllers: {e}"));
2333 }
2334
2335 if let Err(e) = trader_stop_result {
2336 errors.push(format!("Failed to stop trader: {e}"));
2337 }
2338
2339 if let Err(e) = finalize_result {
2340 errors.push(format!("Failed to finalize startup abort: {e}"));
2341 }
2342
2343 if errors.is_empty() {
2344 Ok(())
2345 } else {
2346 anyhow::bail!("{}", errors.join("; "))
2347 }
2348 }
2349
2350 async fn process_receivers_for(
2351 &mut self,
2352 duration: Duration,
2353 receivers: &mut RunnerReceivers<'_>,
2354 ) -> usize {
2355 let deadline = dst::time::Instant::now() + duration;
2356 let mut processed = 0;
2357
2358 loop {
2359 tokio::select! {
2360 biased;
2361
2362 () = dst::time::sleep_until(deadline) => break,
2363 Some(message) = receivers.time_evt.recv() => {
2364 let _ = AsyncRunner::handle_time_event(message);
2365 processed += 1;
2366 }
2367 Some(event) = receivers.system_evt.recv() => {
2368 event.dispatch(|event| self.process_system_event(event));
2369 processed += 1;
2370 }
2371 Some(command) = receivers.system_cmd.recv() => {
2372 command.dispatch(|command| self.process_system_command(command));
2373 processed += 1;
2374 }
2375 Some(event) = receivers.exec_evt.recv() => {
2376 event.dispatch(|event| self.process_exec_event(event));
2377 processed += 1;
2378 }
2379 Some(command) = receivers.exec_cmd.recv() => {
2380 self.process_exec_command(command);
2381 processed += 1;
2382 }
2383 Some(event) = receivers.data_evt.recv() => {
2384 AsyncRunner::dispatch_data_event(event);
2385 processed += 1;
2386 }
2387 Some(command) = receivers.data_cmd.recv() => {
2388 AsyncRunner::handle_data_command(command);
2389 processed += 1;
2390 }
2391 }
2392 }
2393
2394 processed
2395 }
2396
2397 async fn abort_after_trader_start_failure(
2398 &mut self,
2399 start_err: anyhow::Error,
2400 ) -> anyhow::Result<()> {
2401 log::info!("Trader startup failed, aborting startup");
2402 self.handle.set_shutting_down();
2403 let stop_result = self.kernel.stop_trader_after_start_failure();
2404 let finalize_result = self.finalize_stop().await;
2405
2406 match (stop_result, finalize_result) {
2407 (Ok(()), Ok(())) => Err(start_err),
2408 (Err(stop_err), Ok(())) => anyhow::bail!(
2409 "Failed during trader startup: {start_err}; failed to stop partial trader start: \
2410 {stop_err}"
2411 ),
2412 (Ok(()), Err(finalize_err)) => anyhow::bail!(
2413 "Failed during trader startup: {start_err}; failed to finalize startup abort: \
2414 {finalize_err}"
2415 ),
2416 (Err(stop_err), Err(finalize_err)) => anyhow::bail!(
2417 "Failed during trader startup: {start_err}; failed to stop partial trader start: \
2418 {stop_err}; failed to finalize startup abort: {finalize_err}"
2419 ),
2420 }
2421 }
2422
2423 fn initiate_shutdown(&mut self) {
2424 #[cfg(feature = "plugin")]
2425 if let Err(e) = self.plugins.stop_controllers() {
2426 log::error!("Error stopping plug-in controllers: {e}");
2427 }
2428
2429 self.kernel.stop_trader();
2430 let delay = self.kernel.delay_post_stop();
2431 log::info!("Awaiting residual events ({delay:?})...");
2432
2433 self.shutdown_deadline = Some(dst::time::Instant::now() + delay);
2434 self.handle.set_shutting_down();
2435 }
2436
2437 async fn finalize_stop(&mut self) -> anyhow::Result<()> {
2438 self.close_external_ingress();
2439
2440 let timeout = self.config.timeout_disconnection;
2441 let deadline = dst::time::Instant::now() + timeout;
2442
2443 let disconnect_result =
2444 match dst::time::timeout(timeout, self.kernel.disconnect_clients()).await {
2445 Ok(result) => result,
2446 Err(_) => Err(anyhow::anyhow!(
2447 "disconnect timeout while disconnecting clients"
2448 )),
2449 };
2450
2451 if let Err(ref e) = disconnect_result {
2452 log::error!("Error disconnecting clients: {e}");
2453 }
2454
2455 let readiness_result = self.await_engines_disconnected(deadline).await;
2456 let kernel_result = self.kernel.finalize_stop().await;
2457
2458 self.handle.set_stopped();
2459
2460 let mut errors = Vec::new();
2461 if let Err(e) = disconnect_result {
2462 errors.push(e.to_string());
2463 }
2464
2465 if let Err(e) = readiness_result {
2466 errors.push(format!("failed while awaiting engine disconnection: {e}"));
2467 }
2468
2469 if let Err(e) = kernel_result {
2470 errors.push(format!("failed while finalizing kernel shutdown: {e}"));
2471 }
2472
2473 if errors.is_empty() {
2474 Ok(())
2475 } else {
2476 anyhow::bail!("{}", errors.join("; "))
2477 }
2478 }
2479
2480 fn drain_channels(
2481 time_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<TimeEventMessage>>,
2482 system_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<SystemEvent>>,
2483 system_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<SystemCommand>>,
2484 exec_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<ExecutionEvent>>,
2485 exec_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<
2486 DispatchMessage<TradingCommandMessage>,
2487 >,
2488 data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<DataEvent>>,
2489 data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<DataCommand>>,
2490 ) {
2491 let mut drained = 0;
2492
2493 while let Ok(handler) = time_evt_rx.try_recv() {
2494 let _ = AsyncRunner::handle_time_event(handler);
2495 drained += 1;
2496 }
2497
2498 while system_evt_rx.try_recv().is_ok() {
2499 drained += 1;
2500 }
2501
2502 while system_cmd_rx.try_recv().is_ok() {
2503 drained += 1;
2504 }
2505
2506 while let Ok(evt) = data_evt_rx.try_recv() {
2507 AsyncRunner::dispatch_data_event(evt);
2508 drained += 1;
2509 }
2510
2511 while let Ok(cmd) = data_cmd_rx.try_recv() {
2512 AsyncRunner::handle_data_command(cmd);
2513 drained += 1;
2514 }
2515
2516 while let Ok(evt) = exec_evt_rx.try_recv() {
2517 AsyncRunner::dispatch_exec_event(evt);
2518 drained += 1;
2519 }
2520
2521 while let Ok(cmd) = exec_cmd_rx.try_recv() {
2522 AsyncRunner::handle_trading_command(cmd);
2523 drained += 1;
2524 }
2525
2526 if drained > 0 {
2527 log::info!("Drained {drained} remaining events during shutdown");
2528 }
2529 }
2530
2531 fn observe_exec_event_before_dispatch(
2532 &mut self,
2533 evt: &ExecutionEvent,
2534 ) -> Option<Vec<ClientOrderId>> {
2535 let mut close_ids = Vec::new();
2536
2537 match evt {
2538 ExecutionEvent::Order(order_evt) => {
2539 self.exec_manager.observe_order_event(order_evt);
2540 close_ids.push(order_evt.client_order_id());
2541 }
2542 ExecutionEvent::OrderSubmittedBatch(batch) => {
2543 for submitted in &batch.events {
2544 self.exec_manager
2545 .record_local_activity(submitted.client_order_id);
2546 }
2547 }
2548 ExecutionEvent::OrderAcceptedBatch(batch) => {
2549 for accepted in &batch.events {
2550 self.exec_manager
2551 .clear_recon_tracking(&accepted.client_order_id, true);
2552 self.exec_manager
2553 .record_local_activity(accepted.client_order_id);
2554 }
2555 }
2556 ExecutionEvent::OrderCanceledBatch(batch) => {
2557 for canceled in &batch.events {
2558 self.exec_manager
2559 .clear_recon_tracking(&canceled.client_order_id, true);
2560 self.exec_manager
2561 .record_local_activity(canceled.client_order_id);
2562 close_ids.push(canceled.client_order_id);
2563 }
2564 }
2565 ExecutionEvent::Report(report) => {
2566 if let ExecutionReport::Fill(fill_report) = report
2567 && self.exec_manager.is_fill_recently_processed(
2568 fill_report.account_id,
2569 fill_report.instrument_id,
2570 fill_report.trade_id,
2571 )
2572 {
2573 log::debug!(
2574 "Skipping recently processed fill report: {}",
2575 fill_report.trade_id,
2576 );
2577 return None;
2578 }
2579
2580 self.exec_manager.observe_execution_report(report);
2581
2582 if let Some(client_order_id) = Self::closed_order_report_client_order_id(report) {
2583 close_ids.push(client_order_id);
2584 }
2585 }
2586 ExecutionEvent::Account(_) => {}
2587 }
2588
2589 Some(close_ids)
2590 }
2591
2592 fn closed_order_report_client_order_id(report: &ExecutionReport) -> Option<ClientOrderId> {
2593 match report {
2594 ExecutionReport::Order(order_report)
2595 | ExecutionReport::OrderWithFills(order_report, _)
2596 if order_report.order_status.is_closed() =>
2597 {
2598 order_report.client_order_id
2599 }
2600 _ => None,
2601 }
2602 }
2603
2604 fn observe_exec_command_before_dispatch(&mut self, cmd: &TradingCommand) {
2605 match cmd {
2606 TradingCommand::SubmitOrder(submit) => {
2607 self.exec_manager.register_inflight(submit.client_order_id);
2608 }
2609 TradingCommand::SubmitOrderList(submit) => {
2610 for order_init in &submit.order_inits {
2611 self.exec_manager
2612 .register_inflight(order_init.client_order_id);
2613 }
2614 }
2615 TradingCommand::ModifyOrder(modify) => {
2616 self.exec_manager.register_inflight(modify.client_order_id);
2617 }
2618 TradingCommand::ModifyOrders(modify) => {
2619 for child in &modify.modifies {
2620 self.exec_manager.register_inflight(child.client_order_id);
2621 }
2622 }
2623 TradingCommand::CancelOrder(cancel) => {
2624 self.exec_manager.register_inflight(cancel.client_order_id);
2625 }
2626 TradingCommand::CancelOrders(cancel) => {
2627 for child in &cancel.cancels {
2628 self.exec_manager.register_inflight(child.client_order_id);
2629 }
2630 }
2631 _ => {}
2632 }
2633 }
2634
2635 #[must_use]
2637 pub fn environment(&self) -> Environment {
2638 self.kernel.environment()
2639 }
2640
2641 #[must_use]
2643 pub const fn kernel(&self) -> &NautilusKernel {
2644 &self.kernel
2645 }
2646
2647 #[must_use]
2649 pub const fn kernel_mut(&mut self) -> &mut NautilusKernel {
2650 &mut self.kernel
2651 }
2652
2653 #[must_use]
2655 pub fn trader_id(&self) -> TraderId {
2656 self.kernel.trader_id()
2657 }
2658
2659 #[must_use]
2661 pub const fn instance_id(&self) -> UUID4 {
2662 self.kernel.instance_id()
2663 }
2664
2665 #[must_use]
2667 pub fn state(&self) -> NodeState {
2668 self.handle.state()
2669 }
2670
2671 #[must_use]
2673 pub fn is_running(&self) -> bool {
2674 self.state().is_running()
2675 }
2676
2677 pub fn set_cache_database(
2687 &mut self,
2688 database: Box<dyn CacheDatabaseAdapter>,
2689 ) -> anyhow::Result<()> {
2690 if self.state() != NodeState::Idle {
2691 anyhow::bail!(
2692 "Cannot set cache database while node is running, set it before running the node"
2693 );
2694 }
2695
2696 self.kernel.cache().borrow_mut().set_database(database);
2697 Ok(())
2698 }
2699
2700 #[must_use]
2702 pub fn exec_manager(&self) -> &ExecutionManager {
2703 &self.exec_manager
2704 }
2705
2706 #[must_use]
2708 pub fn exec_manager_mut(&mut self) -> &mut ExecutionManager {
2709 &mut self.exec_manager
2710 }
2711
2712 pub fn add_actor<T>(&mut self, actor: T) -> anyhow::Result<()>
2725 where
2726 T: DataActor + DataActorNative + Component + Actor + 'static,
2727 {
2728 if self.state() != NodeState::Idle {
2729 anyhow::bail!(
2730 "Cannot add actor while node is running, add actors before running the node"
2731 );
2732 }
2733
2734 self.kernel.trader.borrow_mut().add_actor(actor)
2735 }
2736
2737 pub fn add_actor_from_factory<F, T>(&mut self, factory: F) -> anyhow::Result<()>
2749 where
2750 F: FnOnce() -> anyhow::Result<T>,
2751 T: DataActor + DataActorNative + Component + Actor + 'static,
2752 {
2753 if self.state() != NodeState::Idle {
2754 anyhow::bail!(
2755 "Cannot add actor while node is running, add actors before running the node"
2756 );
2757 }
2758
2759 self.kernel
2760 .trader
2761 .borrow_mut()
2762 .add_actor_from_factory(factory)
2763 }
2764
2765 pub fn add_strategy<T>(&mut self, mut strategy: T) -> anyhow::Result<()>
2781 where
2782 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
2783 {
2784 if self.state() != NodeState::Idle {
2785 anyhow::bail!(
2786 "Cannot add strategy while node is running, add strategies before running the node"
2787 );
2788 }
2789
2790 let strategy_id = self
2792 .kernel
2793 .trader
2794 .borrow()
2795 .prepare_strategy_for_registration(&mut strategy)?;
2796 let oms_type = StrategyNative::strategy_core(&strategy).config.oms_type;
2797 let instrument_ids = strategy.external_order_instrument_ids().unwrap_or_default();
2798
2799 if !instrument_ids.is_empty() {
2800 self.register_external_order_claims(strategy_id, &instrument_ids)?;
2801 }
2802
2803 let mut exec_engine = match oms_type
2804 .map(|_| {
2805 self.kernel
2806 .exec_engine
2807 .try_borrow_mut()
2808 .map_err(|e| anyhow::anyhow!("Cannot register OMS type: {e}"))
2809 })
2810 .transpose()
2811 {
2812 Ok(exec_engine) => exec_engine,
2813 Err(e) => {
2814 if !instrument_ids.is_empty()
2815 && let Err(rollback_error) =
2816 self.rollback_external_order_claims(strategy_id, &instrument_ids)
2817 {
2818 anyhow::bail!(
2819 "{e}; failed to roll back external order claims for {strategy_id}: {rollback_error}"
2820 );
2821 }
2822
2823 return Err(e);
2824 }
2825 };
2826
2827 if let Err(add_error) = self.kernel.trader.borrow_mut().add_strategy(strategy) {
2828 drop(exec_engine);
2829
2830 if !instrument_ids.is_empty()
2831 && let Err(rollback_error) =
2832 self.rollback_external_order_claims(strategy_id, &instrument_ids)
2833 {
2834 anyhow::bail!(
2835 "Failed to add strategy {strategy_id}: {add_error}; failed to roll back external order claims: {rollback_error}"
2836 );
2837 }
2838
2839 return Err(add_error);
2840 }
2841
2842 if let Some(exec_engine) = &mut exec_engine
2843 && let Some(oms_type) = oms_type
2844 {
2845 exec_engine.register_oms_type(strategy_id, oms_type);
2846 }
2847
2848 Ok(())
2849 }
2850
2851 pub fn register_external_order_claims(
2862 &self,
2863 strategy_id: StrategyId,
2864 instrument_ids: &[InstrumentId],
2865 ) -> anyhow::Result<()> {
2866 self.kernel
2867 .cache
2868 .try_borrow_mut()
2869 .map_err(|e| anyhow::anyhow!("Cannot register external order claims: {e}"))?
2870 .register_external_order_claims(strategy_id, instrument_ids)?;
2871
2872 if !instrument_ids.is_empty() {
2873 log::info!("Registered external order claims for {strategy_id}: {instrument_ids:?}");
2874 }
2875
2876 Ok(())
2877 }
2878
2879 pub fn deregister_external_order_claims(&self, strategy_id: StrategyId) -> anyhow::Result<()> {
2889 self.kernel
2890 .cache
2891 .try_borrow_mut()
2892 .map_err(|e| anyhow::anyhow!("Cannot deregister external order claims: {e}"))?
2893 .set_external_order_claims(strategy_id, &[])?;
2894
2895 Ok(())
2896 }
2897
2898 pub(crate) fn rollback_external_order_claims(
2899 &self,
2900 strategy_id: StrategyId,
2901 instrument_ids: &[InstrumentId],
2902 ) -> anyhow::Result<()> {
2903 let mut cache = self
2904 .kernel
2905 .cache
2906 .try_borrow_mut()
2907 .map_err(|e| anyhow::anyhow!("Cannot roll back external order claims: {e}"))?;
2908 let retained: Vec<_> = cache
2909 .external_order_claim_instrument_ids(Some(strategy_id))
2910 .into_iter()
2911 .filter(|instrument_id| !instrument_ids.contains(instrument_id))
2912 .collect();
2913 cache.set_external_order_claims(strategy_id, &retained)
2914 }
2915
2916 pub fn add_exec_algorithm<T>(&mut self, exec_algorithm: T) -> anyhow::Result<()>
2927 where
2928 T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
2929 {
2930 if self.state() != NodeState::Idle {
2931 anyhow::bail!(
2932 "Cannot add exec algorithm while node is running, add exec algorithms before running the node"
2933 );
2934 }
2935
2936 self.kernel
2937 .trader
2938 .borrow_mut()
2939 .add_exec_algorithm(exec_algorithm)
2940 }
2941
2942 }
2946
2947#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2948enum SocketReconnectDispatchOutcome {
2949 Accepted,
2950 AlreadyReconnecting,
2951 Disconnected,
2952 Closed,
2953 Unsupported,
2954 InvalidTrader,
2955 UnknownClient,
2956 UnknownEndpoint,
2957 AmbiguousEndpoint,
2958}
2959
2960fn record_runner_dispatch(
2961 metrics: &RunnerMetrics,
2962 channel: SystemChannel,
2963 dispatch_start: dst::time::Instant,
2964 metrics_start: dst::time::Instant,
2965) {
2966 let dispatch_end = dst::time::Instant::now();
2967 metrics.record_dispatch(
2968 channel,
2969 dispatch_end.duration_since(dispatch_start),
2970 dispatch_end.duration_since(metrics_start),
2971 );
2972}
2973
2974fn record_runner_maintenance(
2975 metrics: &RunnerMetrics,
2976 work_start: dst::time::Instant,
2977 metrics_start: dst::time::Instant,
2978) {
2979 let work_end = dst::time::Instant::now();
2980 metrics.record_maintenance(
2981 work_end.duration_since(work_start),
2982 work_end.duration_since(metrics_start),
2983 );
2984}
2985
2986fn record_runner_external_msgbus(
2987 metrics: &RunnerMetrics,
2988 work_start: dst::time::Instant,
2989 metrics_start: dst::time::Instant,
2990) {
2991 let work_end = dst::time::Instant::now();
2992 metrics.record_external_msgbus(
2993 work_end.duration_since(work_start),
2994 work_end.duration_since(metrics_start),
2995 );
2996}
2997
2998async fn recv_external_msgbus_message(
2999 rx: &mut Option<tokio::sync::mpsc::Receiver<BusMessage>>,
3000) -> Option<BusMessage> {
3001 match rx {
3002 Some(rx) => rx.recv().await,
3003 None => std::future::pending::<Option<BusMessage>>().await,
3004 }
3005}
3006
3007struct RunnerReceivers<'a> {
3008 time_evt: &'a mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<TimeEventMessage>>,
3009 system_evt: &'a mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<SystemEvent>>,
3010 system_cmd: &'a mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<SystemCommand>>,
3011 exec_evt: &'a mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<ExecutionEvent>>,
3012 exec_cmd: &'a mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<TradingCommandMessage>>,
3013 data_evt: &'a mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<DataEvent>>,
3014 data_cmd: &'a mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<DataCommand>>,
3015}
3016
3017fn flush_pending_data(
3024 pending: &mut PendingEvents,
3025 data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<DataEvent>>,
3026 data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<DataCommand>>,
3027) {
3028 loop {
3029 let mut progressed = pending.drain_data();
3030
3031 while let Ok(evt) = data_evt_rx.try_recv() {
3032 AsyncRunner::dispatch_data_event(evt);
3033 progressed = true;
3034 }
3035
3036 while let Ok(cmd) = data_cmd_rx.try_recv() {
3037 AsyncRunner::handle_data_command(cmd);
3038 progressed = true;
3039 }
3040
3041 if !progressed {
3042 break;
3043 }
3044 }
3045}
3046
3047#[expect(
3053 clippy::too_many_arguments,
3054 reason = "all runner receivers are drained together"
3055)]
3056fn flush_all_pending(
3057 pending: &mut PendingEvents,
3058 time_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<TimeEventMessage>>,
3059 system_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<SystemEvent>>,
3060 system_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<SystemCommand>>,
3061 exec_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<ExecutionEvent>>,
3062 exec_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<TradingCommandMessage>>,
3063 data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<DataEvent>>,
3064 data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<DataCommand>>,
3065) {
3066 while let Ok(handler) = time_evt_rx.try_recv() {
3068 let _ = AsyncRunner::handle_time_event(handler);
3069 }
3070
3071 while let Ok(event) = system_evt_rx.try_recv() {
3072 pending.system_events.push(event);
3073 }
3074
3075 while let Ok(command) = system_cmd_rx.try_recv() {
3076 pending.system_commands.push(command);
3077 }
3078
3079 while let Ok(evt) = data_evt_rx.try_recv() {
3080 pending.data_evts.push(evt);
3081 }
3082
3083 while let Ok(cmd) = data_cmd_rx.try_recv() {
3084 pending.data_cmds.push(cmd);
3085 }
3086
3087 while let Ok(evt) = exec_evt_rx.try_recv() {
3088 pending.push_exec_event(evt);
3089 }
3090
3091 while let Ok(cmd) = exec_cmd_rx.try_recv() {
3092 pending.exec_cmds.push(cmd);
3093 }
3094
3095 pending.drain();
3096}
3097
3098#[expect(
3103 clippy::too_many_arguments,
3104 reason = "startup buffering owns one future plus the pending state and all runner receivers"
3105)]
3106async fn drive_with_event_buffering<F: std::future::Future>(
3107 future: F,
3108 pending: &mut PendingEvents,
3109 time_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<TimeEventMessage>>,
3110 system_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<SystemEvent>>,
3111 system_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<SystemCommand>>,
3112 exec_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<ExecutionEvent>>,
3113 exec_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<TradingCommandMessage>>,
3114 data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<DataEvent>>,
3115 data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DispatchMessage<DataCommand>>,
3116) -> F::Output {
3117 tokio::pin!(future);
3118
3119 loop {
3120 tokio::select! {
3121 biased;
3122
3123 result = &mut future => {
3124 break result;
3125 }
3126 Some(handler) = time_evt_rx.recv() => {
3127 let _ = AsyncRunner::handle_time_event(handler);
3128 }
3129 Some(event) = system_evt_rx.recv() => {
3130 pending.system_events.push(event);
3131 }
3132 Some(command) = system_cmd_rx.recv() => {
3133 pending.system_commands.push(command);
3134 }
3135 Some(evt) = exec_evt_rx.recv() => {
3136 pending.push_exec_event(evt);
3137 }
3138 Some(cmd) = exec_cmd_rx.recv() => {
3139 pending.exec_cmds.push(cmd);
3140 }
3141 Some(evt) = data_evt_rx.recv() => {
3142 pending.data_evts.push(evt);
3143 }
3144 Some(cmd) = data_cmd_rx.recv() => {
3145 pending.data_cmds.push(cmd);
3146 }
3147 }
3148 }
3149}
3150
3151#[derive(Default)]
3152struct PendingEvents {
3153 system_events: Vec<DispatchMessage<SystemEvent>>,
3154 system_commands: Vec<DispatchMessage<SystemCommand>>,
3155 data_evts: Vec<DispatchMessage<DataEvent>>,
3156 data_cmds: Vec<DispatchMessage<DataCommand>>,
3157 exec_reports: Vec<DispatchMessage<ExecutionReport>>,
3158 order_evts: Vec<DispatchMessage<OrderEventAny>>,
3159 exec_cmds: Vec<DispatchMessage<TradingCommandMessage>>,
3160}
3161
3162impl PendingEvents {
3163 fn is_empty(&self) -> bool {
3164 self.system_events.is_empty()
3165 && self.system_commands.is_empty()
3166 && self.data_evts.is_empty()
3167 && self.data_cmds.is_empty()
3168 && self.exec_reports.is_empty()
3169 && self.order_evts.is_empty()
3170 && self.exec_cmds.is_empty()
3171 }
3172
3173 fn drain_data(&mut self) -> bool {
3177 let total = self.data_evts.len() + self.data_cmds.len();
3178
3179 if total > 0 {
3180 log::debug!(
3181 "Draining {total} data events/commands into cache \
3182 (data_evts={}, data_cmds={})",
3183 self.data_evts.len(),
3184 self.data_cmds.len(),
3185 );
3186 }
3187
3188 for evt in self.data_evts.drain(..) {
3189 AsyncRunner::dispatch_data_event(evt);
3190 }
3191
3192 for cmd in self.data_cmds.drain(..) {
3193 AsyncRunner::handle_data_command(cmd);
3194 }
3195
3196 total > 0
3197 }
3198
3199 fn drain(&mut self) {
3201 let total = self.data_evts.len()
3202 + self.data_cmds.len()
3203 + self.exec_reports.len()
3204 + self.order_evts.len()
3205 + self.exec_cmds.len();
3206
3207 if total > 0 {
3208 log::debug!(
3209 "Processing {total} events/commands queued during startup \
3210 (data_evts={}, data_cmds={}, exec_reports={}, order_evts={}, exec_cmds={})",
3211 self.data_evts.len(),
3212 self.data_cmds.len(),
3213 self.exec_reports.len(),
3214 self.order_evts.len(),
3215 self.exec_cmds.len()
3216 );
3217 }
3218
3219 for evt in self.data_evts.drain(..) {
3220 AsyncRunner::dispatch_data_event(evt);
3221 }
3222
3223 for cmd in self.data_cmds.drain(..) {
3224 AsyncRunner::handle_data_command(cmd);
3225 }
3226
3227 for report in self.exec_reports.drain(..) {
3228 report
3229 .dispatch(|report| AsyncRunner::handle_exec_event(ExecutionEvent::Report(report)));
3230 }
3231
3232 for evt in self.order_evts.drain(..) {
3233 evt.dispatch(|evt| AsyncRunner::handle_exec_event(ExecutionEvent::Order(evt)));
3234 }
3235
3236 for cmd in self.exec_cmds.drain(..) {
3237 AsyncRunner::handle_trading_command(cmd);
3238 }
3239 }
3240
3241 fn push_exec_event(&mut self, event: DispatchMessage<ExecutionEvent>) {
3242 let rooted = event.is_rooted();
3243 event.dispatch(|event| {
3244 let owner = std::thread::current().id();
3245
3246 let order = |event| {
3247 if rooted {
3248 DispatchMessage::new(event, owner)
3249 } else {
3250 DispatchMessage::from(event)
3251 }
3252 };
3253
3254 match event {
3257 ExecutionEvent::Account(_) => AsyncRunner::handle_exec_event(event),
3258 ExecutionEvent::Report(report) => self.exec_reports.push(if rooted {
3259 DispatchMessage::new(report, owner)
3260 } else {
3261 report.into()
3262 }),
3263 ExecutionEvent::Order(event) => self.order_evts.push(order(event)),
3264 ExecutionEvent::OrderSubmittedBatch(batch) => {
3265 for event in batch {
3266 self.order_evts.push(order(OrderEventAny::Submitted(event)));
3267 }
3268 }
3269 ExecutionEvent::OrderAcceptedBatch(batch) => {
3270 for event in batch {
3271 self.order_evts.push(order(OrderEventAny::Accepted(event)));
3272 }
3273 }
3274 ExecutionEvent::OrderCanceledBatch(batch) => {
3275 for event in batch {
3276 self.order_evts.push(order(OrderEventAny::Canceled(event)));
3277 }
3278 }
3279 }
3280 });
3281 }
3282
3283 fn take_system_events(&mut self) -> Vec<DispatchMessage<SystemEvent>> {
3284 std::mem::take(&mut self.system_events)
3285 }
3286
3287 fn take_system_commands(&mut self) -> Vec<DispatchMessage<SystemCommand>> {
3288 std::mem::take(&mut self.system_commands)
3289 }
3290}
3291
3292struct ClientStatus {
3293 client: String,
3294 client_type: &'static str,
3295 connected: bool,
3296}
3297
3298fn render_client_statuses(rows: Vec<ClientStatus>) -> String {
3299 let mut builder = Builder::with_capacity(rows.len() + 1, 3);
3300 builder.push_record(["Client", "Type", "Connected"]);
3301
3302 for row in rows {
3303 builder.push_record([
3304 row.client,
3305 row.client_type.to_string(),
3306 row.connected.to_string(),
3307 ]);
3308 }
3309
3310 builder.build().with(Style::rounded()).to_string()
3311}
3312
3313#[cfg(test)]
3314mod tests {
3315 use std::{
3316 cell::{Cell, RefCell},
3317 fmt::Debug,
3318 rc::Rc,
3319 sync::{
3320 Arc,
3321 atomic::{AtomicBool, Ordering},
3322 },
3323 };
3324
3325 use bytes::Bytes;
3326 use indexmap::{IndexMap, IndexSet};
3327 use log::{Level, LevelFilter, Log, Metadata, Record};
3328 #[cfg(feature = "python")]
3329 use nautilus_common::runner::{
3330 SyncDataCommandSender, replace_data_cmd_sender, replace_exec_cmd_sender,
3331 };
3332 use nautilus_common::{
3333 actor::{
3334 self, CallbackDispatchError, DataActor, DataActorCore, data_actor::DataActorConfig,
3335 },
3336 cache::Cache,
3337 clock::{Clock, VirtualClock},
3338 enums::SerializationEncoding,
3339 live::{
3340 runner::{get_data_event_sender, get_exec_event_sender, get_system_event_sender},
3341 sender::DispatchSender,
3342 },
3343 logging::{logger::LoggerConfig, logging_sync_to_disk, writer::FileWriterConfig},
3344 messages::{
3345 data::{SubscribeCommand, SubscribeQuotes},
3346 execution::{GenerateFillReports, QueryAccount, SubmitOrder, TradingCommand},
3347 system::{
3348 QueueCondition, QueueState, ReconnectSocket, SocketState, SocketStateChanged,
3349 },
3350 },
3351 msgbus::{
3352 self, BusMessage, BusPayloadType, MessageBusBacking, MessageBusBackingFactory,
3353 MessageBusConfig, MessageBusExternalEgress, MessageBusExternalIngress,
3354 MessagingSwitchboard, ShareableMessageHandler, TypedHandler, TypedIntoHandler,
3355 },
3356 nautilus_actor,
3357 runner::{SyncTradingCommandSender, TradingCommandSender},
3358 testing::wait_until_async,
3359 timer::{TimeEvent, TimeEventCallback},
3360 };
3361 use nautilus_core::{Params, UUID4, UnixNanos};
3362 use nautilus_execution::{
3363 engine::{ExecutionEngine, SnapshotAnchorer, stubs::StubExecutionClient},
3364 reconciliation::create_inferred_fill_for_qty,
3365 };
3366 use nautilus_model::{
3367 accounts::{AccountAny, MarginAccount},
3368 data::QuoteTick,
3369 enums::{
3370 AccountType, LiquiditySide, OmsType, OrderSide, OrderStatus, OrderType, PositionSide,
3371 TimeInForce,
3372 },
3373 events::{
3374 AccountState, OrderAcceptedBatch, OrderFilled,
3375 order::spec::{OrderAcceptedSpec, OrderPendingUpdateSpec, OrderUpdatedSpec},
3376 },
3377 identifiers::{
3378 AccountId, ActorId, ClientId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
3379 Venue, VenueOrderId,
3380 },
3381 instruments::{Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt},
3382 orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
3383 reports::{FillReport, PositionStatusReport},
3384 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
3385 };
3386 use nautilus_system::{KernelEventStore, RegisteredComponents, event_store::EventStoreConfig};
3387 use nautilus_testkit::{
3388 cache::TestCacheDatabaseControl,
3389 components::{StateActor, StateStrategy},
3390 };
3391 use nautilus_trading::{
3392 nautilus_strategy,
3393 strategy::{config::StrategyConfig, core::StrategyCore},
3394 };
3395 use parking_lot::Mutex;
3396 use rstest::*;
3397 use rust_decimal_macros::dec;
3398 use ustr::Ustr;
3399
3400 use super::{
3401 reconciliation::{
3402 PositionFillReportResult, PositionReportResult, reconciliation_check_due,
3403 request_position_fill_reports,
3404 },
3405 *,
3406 };
3407 use crate::{
3408 execution::manager::{PositionFillReportQuery, ReportClientCoverage},
3409 socket::SocketControl,
3410 };
3411
3412 struct ExternalIngressLogCapture {
3413 messages: Mutex<Vec<String>>,
3414 }
3415
3416 static EXTERNAL_INGRESS_LOG_CAPTURE: ExternalIngressLogCapture = ExternalIngressLogCapture {
3417 messages: Mutex::new(Vec::new()),
3418 };
3419
3420 #[derive(Debug)]
3421 enum FillReportClientOutcome {
3422 Reports(Vec<FillReport>),
3423 Failure,
3424 }
3425
3426 #[derive(Debug)]
3427 struct FillReportClient {
3428 client_id: ClientId,
3429 account_id: AccountId,
3430 venue: Venue,
3431 outcome: FillReportClientOutcome,
3432 commands: Rc<RefCell<Vec<GenerateFillReports>>>,
3433 }
3434
3435 #[async_trait::async_trait(?Send)]
3436 impl ExecutionClient for FillReportClient {
3437 fn is_connected(&self) -> bool {
3438 true
3439 }
3440
3441 fn client_id(&self) -> ClientId {
3442 self.client_id
3443 }
3444
3445 fn account_id(&self) -> AccountId {
3446 self.account_id
3447 }
3448
3449 fn venue(&self) -> Venue {
3450 self.venue
3451 }
3452
3453 fn oms_type(&self) -> OmsType {
3454 OmsType::Netting
3455 }
3456
3457 fn get_account(&self) -> Option<AccountAny> {
3458 None
3459 }
3460
3461 fn generate_account_state(
3462 &self,
3463 _balances: Vec<AccountBalance>,
3464 _margins: Vec<MarginBalance>,
3465 _reported: bool,
3466 _ts_event: UnixNanos,
3467 _info: Option<Params>,
3468 ) -> anyhow::Result<()> {
3469 Ok(())
3470 }
3471
3472 fn start(&mut self) -> anyhow::Result<()> {
3473 Ok(())
3474 }
3475
3476 fn stop(&mut self) -> anyhow::Result<()> {
3477 Ok(())
3478 }
3479
3480 async fn generate_fill_reports(
3481 &self,
3482 cmd: GenerateFillReports,
3483 ) -> anyhow::Result<Vec<FillReport>> {
3484 self.commands.borrow_mut().push(cmd);
3485
3486 match &self.outcome {
3487 FillReportClientOutcome::Reports(reports) => Ok(reports.clone()),
3488 FillReportClientOutcome::Failure => anyhow::bail!("fill reports unavailable"),
3489 }
3490 }
3491 }
3492
3493 #[derive(Debug)]
3494 struct StartupSocketActor {
3495 core: DataActorCore,
3496 received: Rc<RefCell<Vec<SocketStateChanged>>>,
3497 }
3498
3499 impl StartupSocketActor {
3500 fn new(received: Rc<RefCell<Vec<SocketStateChanged>>>) -> Self {
3501 Self {
3502 core: DataActorCore::new(DataActorConfig {
3503 actor_id: Some(ActorId::from("SOCKET-STARTUP-ACTOR")),
3504 ..Default::default()
3505 }),
3506 received,
3507 }
3508 }
3509 }
3510
3511 impl DataActor for StartupSocketActor {
3512 fn on_start(&mut self) -> anyhow::Result<()> {
3513 self.subscribe_socket_state(None, None, None);
3514 Ok(())
3515 }
3516
3517 fn on_socket_state(&mut self, event: &SocketStateChanged) -> anyhow::Result<()> {
3518 self.received.borrow_mut().push(event.clone());
3519 Ok(())
3520 }
3521 }
3522
3523 nautilus_actor!(StartupSocketActor);
3524
3525 impl Log for ExternalIngressLogCapture {
3526 fn enabled(&self, metadata: &Metadata<'_>) -> bool {
3527 metadata.level() == Level::Error && metadata.target() == "nautilus_live::node"
3528 }
3529
3530 fn log(&self, record: &Record<'_>) {
3531 if self.enabled(record.metadata()) {
3532 self.messages.lock().push(record.args().to_string());
3533 }
3534 }
3535
3536 fn flush(&self) {}
3537 }
3538
3539 #[derive(Debug)]
3540 struct FailingTimerActor {
3541 core: DataActorCore,
3542 received: Rc<RefCell<Vec<u64>>>,
3543 }
3544
3545 nautilus_actor!(FailingTimerActor);
3546
3547 impl DataActor for FailingTimerActor {
3548 fn on_start(&mut self) -> anyhow::Result<()> {
3549 for timestamp in [17, 23] {
3550 let received = self.received.clone();
3551
3552 let callback = TimeEventCallback::RustLocal(Rc::new(move |_| {
3553 received.borrow_mut().push(timestamp);
3554
3555 if timestamp == 17 {
3556 crate::dispatch::tests::latch_callback_failure();
3557 }
3558 }));
3559
3560 nautilus_common::runner::get_time_event_sender().send(TimeEventMessage::new(
3561 TimeEvent::new(
3562 "callback-failure".into(),
3563 UUID4::new(),
3564 timestamp.into(),
3565 timestamp.into(),
3566 ),
3567 callback,
3568 ));
3569 }
3570
3571 Ok(())
3572 }
3573 }
3574
3575 #[tokio::test]
3576 async fn test_callback_failure_stops_later_live_events() {
3577 actor::clear_callbacks().unwrap();
3578
3579 let config = LiveNodeConfig {
3580 exec_engine: crate::config::LiveExecutionEngineConfig {
3581 reconciliation: false,
3582 ..Default::default()
3583 },
3584 timeout_connection: Duration::ZERO,
3585 timeout_reconciliation: Duration::ZERO,
3586 timeout_portfolio: Duration::ZERO,
3587 timeout_disconnection: Duration::ZERO,
3588 delay_post_stop: Duration::ZERO,
3589 timeout_shutdown: Duration::ZERO,
3590 ..Default::default()
3591 };
3592
3593 let mut node = LiveNode::build("CallbackFailureNode".to_string(), Some(config)).unwrap();
3594 let received = Rc::new(RefCell::new(Vec::new()));
3595 node.add_actor(FailingTimerActor {
3596 core: DataActorCore::new(DataActorConfig {
3597 actor_id: Some(ActorId::from("CALLBACK-FAILURE")),
3598 ..Default::default()
3599 }),
3600 received: received.clone(),
3601 })
3602 .unwrap();
3603
3604 let result = node.run_with_mode(NodeRunMode::Hosted).await;
3605
3606 let error = result.unwrap_err();
3607 let state = node.state();
3608 let trader_stopped = node.kernel.trader.borrow().is_stopped();
3609 let failure = actor::callback_failure();
3610 node.dispose();
3611
3612 assert_eq!(
3613 error.downcast_ref::<CallbackDispatchError>(),
3614 Some(&CallbackDispatchError::DeliveryUnwound)
3615 );
3616 assert_eq!(*received.borrow(), [17]);
3617 assert_eq!(state, NodeState::Stopped);
3618 assert!(trader_stopped);
3619 assert_eq!(failure, Some(CallbackDispatchError::DeliveryUnwound));
3620 assert_eq!(actor::callback_failure(), None);
3621 assert_eq!(actor::clear_callbacks(), Ok(()));
3622 }
3623
3624 #[rstest]
3625 fn test_render_client_statuses() {
3626 let rows = vec![
3627 ClientStatus {
3628 client: "BINANCE".to_string(),
3629 client_type: "Data",
3630 connected: true,
3631 },
3632 ClientStatus {
3633 client: "SIM".to_string(),
3634 client_type: "Execution",
3635 connected: false,
3636 },
3637 ];
3638
3639 let output = render_client_statuses(rows);
3640 let expected = "â•─────────┬───────────┬───────────╮\n\
3641│ Client │ Type │ Connected │\n\
3642├─────────┼───────────┼───────────┤\n\
3643│ BINANCE │ Data │ true │\n\
3644│ SIM │ Execution │ false │\n\
3645╰─────────┴───────────┴───────────╯";
3646
3647 assert_eq!(output, expected);
3648 }
3649
3650 #[rstest]
3651 fn test_republish_external_msgbus_message_logs_topic_and_error_chain() {
3652 log::set_logger(&EXTERNAL_INGRESS_LOG_CAPTURE).expect("test logger already installed");
3653 log::set_max_level(LevelFilter::Error);
3654 EXTERNAL_INGRESS_LOG_CAPTURE.messages.lock().clear();
3655 let message = BusMessage::with_str_topic(
3656 "data.quotes.AUDUSD.SIM*",
3657 BusPayloadType::Custom(Ustr::from("UnregisteredCustomData")),
3658 Bytes::new(),
3659 SerializationEncoding::Json,
3660 );
3661
3662 LiveNode::republish_external_msgbus_message(&message);
3663
3664 assert_eq!(
3665 *EXTERNAL_INGRESS_LOG_CAPTURE.messages.lock(),
3666 vec![
3667 "Failed to republish external message bus topic 'data.quotes.AUDUSD.SIM*': invalid \
3668 external message topic: Topic `value` contained invalid characters, was \
3669 data.quotes.AUDUSD.SIM*"
3670 .to_string()
3671 ],
3672 );
3673 }
3674
3675 #[rstest]
3676 #[case(None, 2)]
3677 #[case(Some(SystemChannel::DataEvents), 1)]
3678 #[case(Some(SystemChannel::ExecCommands), 1)]
3679 fn test_publish_queue_state_transitions_reaches_typed_subscriber(
3680 #[case] channel: Option<SystemChannel>,
3681 #[case] expected_count: usize,
3682 ) {
3683 let config = LiveNodeConfig {
3684 trader_id: TraderId::from("QUEUE-001"),
3685 exec_engine: crate::config::LiveExecutionEngineConfig {
3686 reconciliation: false,
3687 ..Default::default()
3688 },
3689 ..Default::default()
3690 };
3691
3692 let node = LiveNode::build("QueuePublicationNode".to_string(), Some(config)).unwrap();
3693 let received = Rc::new(RefCell::new(Vec::<QueueStateChanged>::new()));
3694
3695 let handler = ShareableMessageHandler::from_typed({
3696 let received = received.clone();
3697 move |event: &QueueStateChanged| received.borrow_mut().push(event.clone())
3698 });
3699
3700 msgbus::subscribe_any(
3701 MessagingSwitchboard::queue_state_changed_pattern(channel),
3702 handler,
3703 None,
3704 );
3705
3706 let transitions = [
3707 QueueStateTransition {
3708 channel: SystemChannel::DataEvents,
3709 condition: QueueCondition::Backlogged,
3710 state: QueueState::Triggered,
3711 queue_depth: 17,
3712 mean_dispatch_ns: 23,
3713 },
3714 QueueStateTransition {
3715 channel: SystemChannel::ExecCommands,
3716 condition: QueueCondition::Slow,
3717 state: QueueState::Triggered,
3718 queue_depth: 17,
3719 mean_dispatch_ns: 23,
3720 },
3721 ];
3722
3723 node.publish_queue_state_transitions(&transitions);
3724
3725 let events = received.borrow();
3726 assert_eq!(events.len(), expected_count);
3727
3728 let expected = transitions
3729 .into_iter()
3730 .filter(|transition| channel.is_none_or(|channel| channel == transition.channel));
3731 for (event, transition) in events.iter().zip(expected) {
3732 assert_eq!(event.trader_id, TraderId::from("QUEUE-001"));
3733 assert_eq!(event.channel, transition.channel);
3734 assert_eq!(event.condition, transition.condition);
3735 assert_eq!(event.state, transition.state);
3736 assert_eq!(event.queue_depth, transition.queue_depth);
3737 assert_eq!(event.mean_dispatch_ns, transition.mean_dispatch_ns);
3738 assert_ne!(event.event_id, UUID4::default());
3739 assert_ne!(event.ts_event, UnixNanos::default());
3740 assert_eq!(event.ts_init, event.ts_event);
3741 }
3742
3743 if channel.is_none() {
3744 assert_ne!(events[0].event_id, events[1].event_id);
3745 }
3746
3747 drop(events);
3748 msgbus::get_message_bus().borrow_mut().dispose();
3749 }
3750
3751 #[rstest]
3752 #[case(None, None)]
3753 #[case(Some(ClientId::from("BINANCE")), None)]
3754 #[case(None, Some("binance-futures-market-streams"))]
3755 #[case(
3756 Some(ClientId::from("BINANCE")),
3757 Some("binance-futures-market-streams")
3758 )]
3759 fn test_process_socket_state_change_reaches_typed_subscriber(
3760 #[case] client_id: Option<ClientId>,
3761 #[case] endpoint: Option<&str>,
3762 ) {
3763 let config = LiveNodeConfig {
3764 trader_id: TraderId::from("SOCKET-001"),
3765 exec_engine: crate::config::LiveExecutionEngineConfig {
3766 reconciliation: false,
3767 ..Default::default()
3768 },
3769 ..Default::default()
3770 };
3771
3772 let node = LiveNode::build("SocketPublicationNode".to_string(), Some(config)).unwrap();
3773 let received = Rc::new(RefCell::new(Vec::<SocketStateChanged>::new()));
3774
3775 let handler = ShareableMessageHandler::from_typed({
3776 let received = received.clone();
3777 move |event: &SocketStateChanged| received.borrow_mut().push(event.clone())
3778 });
3779
3780 msgbus::subscribe_any(
3781 MessagingSwitchboard::socket_state_changed_pattern(client_id, endpoint),
3782 handler,
3783 None,
3784 );
3785
3786 let change = SocketStateChange::new(
3787 ClientId::from("BINANCE"),
3788 Some(Venue::from("BINANCE")),
3789 Ustr::from("binance-futures-market-streams"),
3790 SocketState::Disconnected,
3791 );
3792
3793 node.process_system_event(SystemEvent::SocketState(change));
3794
3795 let events = received.borrow();
3796 assert_eq!(events.len(), 1);
3797 assert_eq!(events[0].trader_id, TraderId::from("SOCKET-001"));
3798 assert_eq!(events[0].client_id, ClientId::from("BINANCE"));
3799 assert_eq!(events[0].venue, Some(Venue::from("BINANCE")));
3800 assert_eq!(
3801 events[0].endpoint,
3802 Ustr::from("binance-futures-market-streams")
3803 );
3804 assert_eq!(events[0].state, SocketState::Disconnected);
3805 assert_ne!(events[0].event_id, UUID4::default());
3806 assert_ne!(events[0].ts_event, UnixNanos::default());
3807 assert_eq!(events[0].ts_init, events[0].ts_event);
3808 drop(events);
3809 msgbus::get_message_bus().borrow_mut().dispose();
3810 }
3811
3812 #[rstest]
3813 #[case::accepted(
3814 ReconnectRequestOutcome::Accepted,
3815 SocketReconnectDispatchOutcome::Accepted
3816 )]
3817 #[case::already_reconnecting(
3818 ReconnectRequestOutcome::AlreadyReconnecting,
3819 SocketReconnectDispatchOutcome::AlreadyReconnecting
3820 )]
3821 #[case::disconnected(
3822 ReconnectRequestOutcome::Disconnected,
3823 SocketReconnectDispatchOutcome::Disconnected
3824 )]
3825 #[case::closed(
3826 ReconnectRequestOutcome::Closed,
3827 SocketReconnectDispatchOutcome::Closed
3828 )]
3829 #[case::unsupported(
3830 ReconnectRequestOutcome::Unsupported,
3831 SocketReconnectDispatchOutcome::Unsupported
3832 )]
3833 fn test_request_socket_reconnect_maps_transport_outcome(
3834 #[case] transport: ReconnectRequestOutcome,
3835 #[case] expected: SocketReconnectDispatchOutcome,
3836 ) {
3837 let registry = SocketReconnectRegistry::default();
3838 let client_id = ClientId::from("TEST");
3839 let endpoint = Ustr::from("test-streams");
3840 let control = SocketControl::with_registry(client_id, None, endpoint, ®istry);
3841 let _sink = control.sink();
3842 control.register(move || transport);
3843
3844 let outcome = LiveNode::request_socket_reconnect(registry.get(client_id, endpoint));
3845
3846 assert_eq!(outcome, expected);
3847 }
3848
3849 #[rstest]
3850 #[case::client_not_found(
3851 SocketReconnectLookup::ClientNotFound,
3852 SocketReconnectDispatchOutcome::UnknownClient
3853 )]
3854 #[case::unsupported(
3855 SocketReconnectLookup::Unsupported,
3856 SocketReconnectDispatchOutcome::Unsupported
3857 )]
3858 #[case::endpoint_not_found(
3859 SocketReconnectLookup::EndpointNotFound,
3860 SocketReconnectDispatchOutcome::UnknownEndpoint
3861 )]
3862 #[case::ambiguous(
3863 SocketReconnectLookup::AmbiguousEndpoint,
3864 SocketReconnectDispatchOutcome::AmbiguousEndpoint
3865 )]
3866 fn test_request_socket_reconnect_maps_lookup_failure(
3867 #[case] lookup: SocketReconnectLookup,
3868 #[case] expected: SocketReconnectDispatchOutcome,
3869 ) {
3870 assert_eq!(LiveNode::request_socket_reconnect(lookup), expected);
3871 }
3872
3873 #[rstest]
3874 fn test_system_dispatch_restores_context(#[values(false, true)] startup: bool) {
3875 let trader_id = TraderId::from("SOCKET-001");
3876
3877 let config = LiveNodeConfig {
3878 trader_id,
3879 ..Default::default()
3880 };
3881
3882 let mut node = LiveNode::build("SystemDispatchNode".to_string(), Some(config)).unwrap();
3883 let client_id = ClientId::from("TEST");
3884 let endpoint = Ustr::from("test-streams");
3885 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
3886 let sender = DispatchSender::new(tx);
3887 let events = sender.clone();
3888 msgbus::subscribe_any(
3889 MessagingSwitchboard::socket_state_changed_pattern(
3890 Some(client_id),
3891 Some(endpoint.as_str()),
3892 ),
3893 ShareableMessageHandler::from_typed(move |_: &SocketStateChanged| {
3894 events.send(17u32).unwrap();
3895 }),
3896 None,
3897 );
3898
3899 let control =
3900 SocketControl::with_registry(client_id, None, endpoint, &node.socket_registry);
3901 let _sink = control.sink();
3902 control.register(move || {
3903 sender.send(23u32).unwrap();
3904 ReconnectRequestOutcome::Accepted
3905 });
3906
3907 let event = SystemEvent::SocketState(SocketStateChange::new(
3908 client_id,
3909 None,
3910 endpoint,
3911 SocketState::Connected,
3912 ));
3913 let command = SystemCommand::ReconnectSocket(ReconnectSocket::new(
3914 trader_id,
3915 client_id,
3916 endpoint,
3917 31.into(),
3918 ));
3919
3920 if startup {
3921 let mut pending = PendingEvents::default();
3922 pending.system_events.push(event.into());
3923 pending.system_commands.push(command.into());
3924 node.process_system_events(pending.take_system_events());
3925 node.process_system_commands(pending.take_system_commands());
3926 assert!(pending.is_empty());
3927 } else {
3928 node.process_runner_event(PendingRunnerEvent::SystemEvent(event.into()));
3929 node.process_runner_event(PendingRunnerEvent::SystemCommand(command.into()));
3930 }
3931
3932 let event_child = rx.try_recv().unwrap();
3933 let command_child = rx.try_recv().unwrap();
3934 assert!(event_child.is_rooted());
3935 assert!(command_child.is_rooted());
3936 assert_eq!(event_child.dispatch(|value| value), 17);
3937 assert_eq!(command_child.dispatch(|value| value), 23);
3938 assert!(rx.is_empty());
3939 msgbus::get_message_bus().borrow_mut().dispose();
3940 }
3941
3942 #[rstest]
3943 fn test_process_socket_reconnect_routes_only_matching_trader() {
3944 let trader_id = TraderId::from("SOCKET-001");
3945
3946 let config = LiveNodeConfig {
3947 trader_id,
3948 exec_engine: crate::config::LiveExecutionEngineConfig {
3949 reconciliation: false,
3950 ..Default::default()
3951 },
3952 ..Default::default()
3953 };
3954
3955 let node = LiveNode::build("SocketReconnectNode".to_string(), Some(config)).unwrap();
3956 let client_id = ClientId::from("TEST");
3957 let endpoint = Ustr::from("test-streams");
3958 let requests = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3959 let request_count = Arc::clone(&requests);
3960 let control =
3961 SocketControl::with_registry(client_id, None, endpoint, &node.socket_registry);
3962 let _sink = control.sink();
3963 control.register(move || {
3964 request_count.fetch_add(1, Ordering::SeqCst);
3965 ReconnectRequestOutcome::Accepted
3966 });
3967
3968 node.process_system_command(SystemCommand::ReconnectSocket(ReconnectSocket::new(
3969 TraderId::from("OTHER-001"),
3970 client_id,
3971 endpoint,
3972 UnixNanos::default(),
3973 )));
3974 assert_eq!(requests.load(Ordering::SeqCst), 0);
3975
3976 node.process_system_command(SystemCommand::ReconnectSocket(ReconnectSocket::new(
3977 trader_id,
3978 client_id,
3979 endpoint,
3980 UnixNanos::default(),
3981 )));
3982 assert_eq!(requests.load(Ordering::SeqCst), 1);
3983 }
3984
3985 #[rstest]
3986 #[tokio::test]
3987 async fn test_start_publishes_socket_change_after_actor_subscribes() {
3988 let config = LiveNodeConfig {
3989 trader_id: TraderId::from("SOCKET-STARTUP-001"),
3990 exec_engine: crate::config::LiveExecutionEngineConfig {
3991 reconciliation: false,
3992 ..Default::default()
3993 },
3994 timeout_connection: Duration::ZERO,
3995 timeout_reconciliation: Duration::ZERO,
3996 timeout_portfolio: Duration::ZERO,
3997 timeout_disconnection: Duration::ZERO,
3998 delay_post_stop: Duration::ZERO,
3999 timeout_shutdown: Duration::ZERO,
4000 ..Default::default()
4001 };
4002
4003 let mut node = LiveNode::build("SocketStartupNode".to_string(), Some(config)).unwrap();
4004 let received = Rc::new(RefCell::new(Vec::new()));
4005 node.add_actor(StartupSocketActor::new(Rc::clone(&received)))
4006 .unwrap();
4007 node.runner.as_ref().unwrap().bind_senders();
4008
4009 let change = SocketStateChange::new(
4010 ClientId::from("BINANCE"),
4011 Some(Venue::from("BINANCE")),
4012 Ustr::from("binance-futures-market-streams"),
4013 SocketState::Connected,
4014 );
4015 get_system_event_sender()
4016 .send(SystemEvent::SocketState(change))
4017 .unwrap();
4018
4019 node.start().await.unwrap();
4020
4021 {
4022 let events = received.borrow();
4023 assert_eq!(events.len(), 1);
4024 assert_eq!(events[0].trader_id, TraderId::from("SOCKET-STARTUP-001"));
4025 assert_eq!(events[0].client_id, change.client_id);
4026 assert_eq!(events[0].venue, change.venue);
4027 assert_eq!(events[0].endpoint, change.endpoint);
4028 assert_eq!(events[0].state, change.state);
4029 assert_ne!(events[0].event_id, UUID4::default());
4030 assert_ne!(events[0].ts_event, UnixNanos::default());
4031 assert_eq!(events[0].ts_init, events[0].ts_event);
4032 }
4033
4034 node.stop().await.unwrap();
4035 node.dispose();
4036 }
4037
4038 #[rstest]
4039 #[tokio::test(flavor = "current_thread")]
4040 async fn test_run_publishes_queue_state_after_dispatch_sample() {
4041 let config = LiveNodeConfig {
4042 trader_id: TraderId::from("QUEUE-RUN-001"),
4043 queue_monitor: Some(crate::config::QueueMonitorConfig {
4044 queue_depth_trigger: usize::MAX,
4045 queue_depth_clear: 0,
4046 mean_dispatch_ns_trigger: 1,
4047 mean_dispatch_ns_clear: 0,
4048 }),
4049 exec_engine: crate::config::LiveExecutionEngineConfig {
4050 reconciliation: false,
4051 ..Default::default()
4052 },
4053 delay_post_stop: Duration::ZERO,
4054 ..Default::default()
4055 };
4056
4057 let mut node = LiveNode::build("QueueMonitorRunNode".to_string(), Some(config)).unwrap();
4058 let handle = node.handle();
4059 let received = Rc::new(RefCell::new(Vec::<QueueStateChanged>::new()));
4060
4061 let handler = ShareableMessageHandler::from_typed({
4062 let received = received.clone();
4063 let stop_handle = handle.clone();
4064
4065 move |event: &QueueStateChanged| {
4066 received.borrow_mut().push(event.clone());
4067 stop_handle.stop();
4068 }
4069 });
4070
4071 msgbus::subscribe_any(
4072 MessagingSwitchboard::queue_state_changed_pattern(None),
4073 handler,
4074 None,
4075 );
4076 let drive_handle = handle.clone();
4077
4078 let result = tokio::time::timeout(Duration::from_secs(5), async {
4079 let run = node.run();
4080 tokio::pin!(run);
4081
4082 let drive = async move {
4083 wait_until_async(
4084 || async { drive_handle.is_running() },
4085 Duration::from_secs(2),
4086 )
4087 .await;
4088 get_data_event_sender().send(stub_data_event()).unwrap();
4089 };
4090
4091 let (run_result, ()) = tokio::join!(run, drive);
4092 run_result
4093 })
4094 .await;
4095
4096 assert!(
4097 result.is_ok(),
4098 "queue state event should arrive before timeout"
4099 );
4100 assert!(result.unwrap().is_ok(), "run() should succeed");
4101 let events = received.borrow();
4102 assert_eq!(events.len(), 1);
4103 assert_eq!(events[0].trader_id, TraderId::from("QUEUE-RUN-001"));
4104 assert_eq!(events[0].channel, SystemChannel::DataEvents);
4105 assert_eq!(events[0].condition, QueueCondition::Slow);
4106 assert_eq!(events[0].state, QueueState::Triggered);
4107 assert_eq!(events[0].queue_depth, 0);
4108 assert!(events[0].mean_dispatch_ns > 0);
4109 assert_ne!(events[0].event_id, UUID4::default());
4110 assert_ne!(events[0].ts_event, UnixNanos::default());
4111 assert_eq!(events[0].ts_init, events[0].ts_event);
4112 drop(events);
4113 msgbus::get_message_bus().borrow_mut().dispose();
4114 }
4115
4116 #[rstest]
4117 fn test_observe_exec_event_before_dispatch_skips_recent_fill_report() {
4118 let config = LiveNodeConfig {
4119 exec_engine: crate::config::LiveExecutionEngineConfig {
4120 reconciliation: false,
4121 ..Default::default()
4122 },
4123 ..Default::default()
4124 };
4125
4126 let mut node = LiveNode::build("FillSkipNode".to_string(), Some(config)).unwrap();
4127 let event = stub_exec_event();
4128 let account_id = AccountId::from("TEST-001");
4129 let instrument_id = InstrumentId::from("TEST.VENUE");
4130 let trade_id = TradeId::from("T-001");
4131
4132 let close_ids = node.observe_exec_event_before_dispatch(&event);
4133 assert_eq!(close_ids, Some(Vec::new()));
4134 assert!(
4135 !node
4136 .exec_manager
4137 .is_fill_recently_processed(account_id, instrument_id, trade_id)
4138 );
4139
4140 node.exec_manager
4141 .mark_fill_processed(account_id, instrument_id, trade_id);
4142
4143 let close_ids = node.observe_exec_event_before_dispatch(&event);
4144 assert_eq!(close_ids, None);
4145 }
4146
4147 #[rstest]
4148 #[case(false, false, OrderStatus::Canceled, 1)]
4149 #[case(false, true, OrderStatus::Accepted, 0)]
4150 #[case(true, false, OrderStatus::Canceled, 1)]
4151 #[case(true, true, OrderStatus::Accepted, 0)]
4152 fn test_process_exec_event_clears_terminal_activity_only_after_cached_order_closes(
4153 #[case] with_fills: bool,
4154 #[case] superseded: bool,
4155 #[case] expected_status: OrderStatus,
4156 #[case] expected_query_count: usize,
4157 ) {
4158 let config = LiveNodeConfig {
4159 exec_engine: crate::config::LiveExecutionEngineConfig {
4160 reconciliation: true,
4161 open_check_threshold_ms: 5_000,
4162 single_order_query_delay_ms: 0,
4163 ..Default::default()
4164 },
4165 ..Default::default()
4166 };
4167
4168 let mut node = LiveNode::build("TerminalReportNode".to_string(), Some(config)).unwrap();
4169 let client_order_id = ClientOrderId::from("O-TERMINAL-REPORT");
4170 let old_venue_order_id = VenueOrderId::from("V-TERMINAL-REPORT-OLD");
4171 let new_venue_order_id = VenueOrderId::from("V-TERMINAL-REPORT-NEW");
4172 let account_id = AccountId::from("TEST-001");
4173 let client_id = ClientId::from("TEST");
4174 let instrument = crypto_perpetual_ethusdt();
4175 let instrument_id = instrument.id();
4176 let account = AccountAny::Margin(MarginAccount::new(
4177 AccountState::new(
4178 account_id,
4179 AccountType::Margin,
4180 vec![AccountBalance::new(
4181 Money::from("1000000 USDT"),
4182 Money::from("0 USDT"),
4183 Money::from("1000000 USDT"),
4184 )],
4185 Vec::new(),
4186 true,
4187 UUID4::new(),
4188 UnixNanos::default(),
4189 UnixNanos::default(),
4190 Some(Currency::USDT()),
4191 ),
4192 true,
4193 ));
4194 node.kernel.cache.borrow_mut().add_account(account).unwrap();
4195 node.kernel
4196 .cache
4197 .borrow_mut()
4198 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
4199 .unwrap();
4200 insert_accepted_limit_order_in_node(
4201 &node,
4202 account_id,
4203 client_id,
4204 instrument_id,
4205 client_order_id,
4206 old_venue_order_id,
4207 );
4208
4209 if superseded {
4210 let order = node
4211 .kernel
4212 .cache
4213 .borrow()
4214 .order_owned(&client_order_id)
4215 .unwrap();
4216 let pending_update = OrderPendingUpdateSpec::builder()
4217 .trader_id(order.trader_id())
4218 .strategy_id(order.strategy_id())
4219 .instrument_id(order.instrument_id())
4220 .client_order_id(client_order_id)
4221 .account_id(account_id)
4222 .venue_order_id(old_venue_order_id)
4223 .build();
4224 node.kernel
4225 .cache
4226 .borrow_mut()
4227 .update_order(&OrderEventAny::PendingUpdate(pending_update))
4228 .unwrap();
4229 let order = node
4230 .kernel
4231 .cache
4232 .borrow()
4233 .order_owned(&client_order_id)
4234 .unwrap();
4235 let updated = OrderUpdatedSpec::builder()
4236 .trader_id(order.trader_id())
4237 .strategy_id(order.strategy_id())
4238 .instrument_id(order.instrument_id())
4239 .client_order_id(client_order_id)
4240 .quantity(order.quantity())
4241 .venue_order_id(new_venue_order_id)
4242 .account_id(account_id)
4243 .build();
4244 node.kernel
4245 .cache
4246 .borrow_mut()
4247 .update_order(&OrderEventAny::Updated(updated))
4248 .unwrap();
4249 }
4250
4251 let report = OrderStatusReport::new(
4252 account_id,
4253 instrument_id,
4254 Some(client_order_id),
4255 old_venue_order_id,
4256 OrderSide::Buy.into(),
4257 OrderType::Limit,
4258 TimeInForce::Gtc,
4259 OrderStatus::Canceled,
4260 Quantity::from("10.0"),
4261 Quantity::from("0.0"),
4262 UnixNanos::from(1_000),
4263 UnixNanos::from(2_000),
4264 UnixNanos::from(3_000),
4265 None,
4266 );
4267
4268 let report = if with_fills {
4269 ExecutionReport::OrderWithFills(Box::new(report), Vec::new())
4270 } else {
4271 ExecutionReport::Order(Box::new(report))
4272 };
4273
4274 let event = ExecutionEvent::Report(report);
4275
4276 node.process_exec_event(event);
4277
4278 let order = node
4279 .kernel
4280 .cache
4281 .borrow()
4282 .order_owned(&client_order_id)
4283 .unwrap();
4284
4285 let expected_venue_order_id = if superseded {
4286 new_venue_order_id
4287 } else {
4288 old_venue_order_id
4289 };
4290
4291 assert_eq!(order.status(), expected_status);
4292 assert_eq!(order.venue_order_id(), Some(expected_venue_order_id));
4293
4294 if !superseded {
4295 let replacement = OrderTestBuilder::new(OrderType::Limit)
4296 .client_order_id(client_order_id)
4297 .instrument_id(instrument_id)
4298 .quantity(Quantity::from("20.0"))
4299 .price(Price::from("200.0"))
4300 .build();
4301 let submitted = TestOrderEventStubs::submitted(&replacement, account_id);
4302 node.kernel
4303 .cache
4304 .borrow_mut()
4305 .add_order(replacement, None, Some(client_id), true)
4306 .unwrap();
4307 let replacement = node
4308 .kernel
4309 .cache
4310 .borrow_mut()
4311 .update_order(&submitted)
4312 .unwrap();
4313 let accepted =
4314 TestOrderEventStubs::accepted(&replacement, account_id, old_venue_order_id);
4315 node.kernel
4316 .cache
4317 .borrow_mut()
4318 .update_order(&accepted)
4319 .unwrap();
4320 }
4321
4322 assert_eq!(
4323 node.exec_manager.check_open_order_queries().len(),
4324 expected_query_count,
4325 );
4326 }
4327
4328 #[rstest]
4329 fn test_rejected_direct_fill_stays_eligible_for_later_report() {
4330 let (mut node, mut fill_event, _) = recent_fill_test_fixture("RejectedDirectFillNode");
4331
4332 let OrderEventAny::Filled(fill) = &mut fill_event else {
4333 unreachable!();
4334 };
4335
4336 fill.client_order_id = ClientOrderId::from("O-UNKNOWN");
4337 fill.venue_order_id = VenueOrderId::from("V-UNKNOWN");
4338 let fill = fill.clone();
4339 let report_event = fill_report_event(&fill);
4340 let event = ExecutionEvent::Order(OrderEventAny::Filled(fill.clone()));
4341
4342 assert!(node.observe_exec_event_before_dispatch(&event).is_some());
4343 let marked_before_dispatch = is_recent_fill(&node, &fill);
4344
4345 node.dispatch_exec_event_and_commit_fill(event);
4346
4347 let marked_after_dispatch = is_recent_fill(&node, &fill);
4348 let later_report_is_eligible = node
4349 .observe_exec_event_before_dispatch(&report_event)
4350 .is_some();
4351 assert_eq!(
4352 (
4353 marked_before_dispatch,
4354 marked_after_dispatch,
4355 later_report_is_eligible,
4356 ),
4357 (false, false, true),
4358 );
4359 }
4360
4361 #[rstest]
4362 fn test_applied_direct_fill_commits_and_skips_later_report() {
4363 let (mut node, fill_event, _) = recent_fill_test_fixture("AppliedDirectFillNode");
4364
4365 let OrderEventAny::Filled(fill) = &fill_event else {
4366 unreachable!();
4367 };
4368
4369 let fill = fill.clone();
4370 let report_event = fill_report_event(&fill);
4371 let event = ExecutionEvent::Order(fill_event);
4372
4373 assert!(node.observe_exec_event_before_dispatch(&event).is_some());
4374 assert!(!is_recent_fill(&node, &fill));
4375
4376 node.dispatch_exec_event_and_commit_fill(event);
4377
4378 assert!(is_recent_fill(&node, &fill));
4379 assert_eq!(node.observe_exec_event_before_dispatch(&report_event), None);
4380 }
4381
4382 #[rstest]
4383 fn test_canonical_duplicate_fill_counts_as_applied() {
4384 let (mut node, fill_event, _) = recent_fill_test_fixture("DuplicateDirectFillNode");
4385
4386 let OrderEventAny::Filled(fill) = &fill_event else {
4387 unreachable!();
4388 };
4389
4390 let mut fill = fill.clone();
4391 node.kernel
4392 .cache
4393 .borrow_mut()
4394 .update_order(&fill_event)
4395 .unwrap();
4396 fill.client_order_id = ClientOrderId::from("O-DUPLICATE-UNKNOWN");
4397
4398 node.exec_manager.commit_recent_fill_if_applied(&fill);
4399
4400 assert!(is_recent_fill(&node, &fill));
4401 }
4402
4403 #[rstest]
4404 #[case(false)]
4405 #[case(true)]
4406 fn test_continuous_reconciliation_commits_only_applied_fill(#[case] applied: bool) {
4407 let (mut node, mut fill_event, _) = recent_fill_test_fixture(if applied {
4408 "AppliedContinuousFillNode"
4409 } else {
4410 "RejectedContinuousFillNode"
4411 });
4412
4413 if !applied {
4414 let OrderEventAny::Filled(fill) = &mut fill_event else {
4415 unreachable!();
4416 };
4417
4418 fill.client_order_id = ClientOrderId::from("O-CONTINUOUS-UNKNOWN");
4419 fill.venue_order_id = VenueOrderId::from("V-CONTINUOUS-UNKNOWN");
4420 }
4421
4422 let OrderEventAny::Filled(fill) = &fill_event else {
4423 unreachable!();
4424 };
4425
4426 let fill = fill.clone();
4427
4428 node.process_reconciliation_events(&[fill_event]);
4429
4430 assert_eq!(is_recent_fill(&node, &fill), applied);
4431 }
4432
4433 #[rstest]
4434 fn test_recent_fill_commit_requires_account_and_instrument_match() {
4435 let (mut node, fill_event, _) = recent_fill_test_fixture("MismatchedDirectFillNode");
4436 node.kernel
4437 .cache
4438 .borrow_mut()
4439 .update_order(&fill_event)
4440 .unwrap();
4441
4442 let OrderEventAny::Filled(fill) = fill_event else {
4443 unreachable!();
4444 };
4445
4446 let mut account_mismatch = fill.clone();
4447 account_mismatch.account_id = AccountId::from("OTHER-001");
4448 let mut instrument_mismatch = fill;
4449 instrument_mismatch.instrument_id = InstrumentId::from("OTHER.VENUE");
4450
4451 node.exec_manager
4452 .commit_recent_fill_if_applied(&account_mismatch);
4453 node.exec_manager
4454 .commit_recent_fill_if_applied(&instrument_mismatch);
4455
4456 assert!(!is_recent_fill(&node, &account_mismatch));
4457 assert!(!is_recent_fill(&node, &instrument_mismatch));
4458 }
4459
4460 #[rstest]
4461 fn test_applied_inferred_fill_remains_recently_processed() {
4462 let (mut node, _, instrument) = recent_fill_test_fixture("InferredFillNode");
4463 let client_order_id = ClientOrderId::from("O-RECENT-FILL");
4464 let venue_order_id = VenueOrderId::from("V-RECENT-FILL");
4465 let account_id = AccountId::from("TEST-001");
4466 let order = node
4467 .kernel
4468 .cache
4469 .borrow()
4470 .order_owned(&client_order_id)
4471 .unwrap();
4472 let report = OrderStatusReport::new(
4473 account_id,
4474 instrument.id(),
4475 Some(client_order_id),
4476 venue_order_id,
4477 OrderSide::Buy.into(),
4478 OrderType::Limit,
4479 TimeInForce::Gtc,
4480 OrderStatus::PartiallyFilled,
4481 Quantity::from("10.0"),
4482 Quantity::from("1.0"),
4483 UnixNanos::from(1_000),
4484 UnixNanos::from(1_000),
4485 UnixNanos::from(1_000),
4486 None,
4487 )
4488 .with_avg_px(dec!(100.0));
4489 let inferred = create_inferred_fill_for_qty(
4490 &order,
4491 &report,
4492 &account_id,
4493 &instrument,
4494 Quantity::from("1.0"),
4495 UnixNanos::from(1_000),
4496 None,
4497 )
4498 .unwrap();
4499
4500 let OrderEventAny::Filled(fill) = &inferred else {
4501 unreachable!();
4502 };
4503
4504 let fill = fill.clone();
4505
4506 node.process_reconciliation_events(&[inferred]);
4507
4508 assert!(fill.reconciliation);
4509 assert!(is_recent_fill(&node, &fill));
4510 }
4511
4512 #[rstest]
4513 #[tokio::test]
4514 async fn test_request_position_fill_reports_keeps_failed_query_unsuccessful() {
4515 let client_id = ClientId::from("POSITION-FILLS");
4516 let account_id = AccountId::from("POSITION-FILLS-001");
4517 let instrument_id = crypto_perpetual_ethusdt().id();
4518 let commands = Rc::new(RefCell::new(Vec::new()));
4519
4520 let client = LiveExecutionClient::new(Box::new(FillReportClient {
4521 client_id,
4522 account_id,
4523 venue: instrument_id.venue,
4524 outcome: FillReportClientOutcome::Failure,
4525 commands: commands.clone(),
4526 }));
4527
4528 let command = GenerateFillReports::new(
4529 UUID4::new(),
4530 UnixNanos::from(2_000),
4531 Some(instrument_id),
4532 None,
4533 Some(UnixNanos::from(1_000)),
4534 Some(UnixNanos::from(2_000)),
4535 None,
4536 None,
4537 );
4538
4539 let result = request_position_fill_reports(
4540 vec![client],
4541 vec![PositionFillReportQuery {
4542 key: (instrument_id, account_id),
4543 client_id,
4544 command: command.clone(),
4545 }],
4546 )
4547 .await;
4548
4549 assert_eq!(*commands.borrow(), vec![command]);
4550 assert!(result.successful_keys.is_empty());
4551 assert!(result.reports.is_empty());
4552 }
4553
4554 #[rstest]
4555 #[tokio::test]
4556 async fn test_request_position_fill_reports_accepts_scoped_response() {
4557 let client_id = ClientId::from("POSITION-FILLS");
4558 let account_id = AccountId::from("POSITION-FILLS-001");
4559 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
4560 let instrument_id = instrument.id();
4561
4562 let report_b = FillReport::new(
4563 account_id,
4564 instrument_id,
4565 VenueOrderId::from("V-POSITION-FILLS"),
4566 TradeId::from("T-POSITION-FILLS-B"),
4567 OrderSide::Buy,
4568 Quantity::from("1.0"),
4569 Price::from("100.0"),
4570 Money::zero(instrument.quote_currency()),
4571 LiquiditySide::Taker,
4572 Some(ClientOrderId::from("O-POSITION-FILLS")),
4573 None,
4574 UnixNanos::from(1_500),
4575 UnixNanos::from(2_000),
4576 None,
4577 );
4578 let mut report_a = report_b.clone();
4579 report_a.trade_id = TradeId::from("T-POSITION-FILLS-A");
4580 let commands = Rc::new(RefCell::new(Vec::new()));
4581
4582 let client = LiveExecutionClient::new(Box::new(FillReportClient {
4583 client_id,
4584 account_id,
4585 venue: instrument_id.venue,
4586 outcome: FillReportClientOutcome::Reports(vec![report_b.clone(), report_a.clone()]),
4587 commands,
4588 }));
4589
4590 let command = GenerateFillReports::new(
4591 UUID4::new(),
4592 UnixNanos::from(2_000),
4593 Some(instrument_id),
4594 None,
4595 Some(UnixNanos::from(1_000)),
4596 Some(UnixNanos::from(2_000)),
4597 None,
4598 None,
4599 );
4600 let key = (instrument_id, account_id);
4601
4602 let result = request_position_fill_reports(
4603 vec![client],
4604 vec![PositionFillReportQuery {
4605 key,
4606 client_id,
4607 command,
4608 }],
4609 )
4610 .await;
4611
4612 assert_eq!(result.successful_keys, IndexSet::from([key]));
4613 assert_eq!(
4614 result.reports,
4615 IndexMap::from([(key, vec![report_a, report_b])])
4616 );
4617 }
4618
4619 #[rstest]
4620 #[case("OTHER-001", "ETHUSDT-PERP.BINANCE", 1_500, "1.0")]
4621 #[case("POSITION-FILLS-001", "BTCUSDT-PERP.BINANCE", 1_500, "1.0")]
4622 #[case("POSITION-FILLS-001", "ETHUSDT-PERP.BINANCE", 1_500, "0.0")]
4623 #[tokio::test]
4624 async fn test_request_position_fill_reports_rejects_out_of_scope_response(
4625 #[case] report_account: &str,
4626 #[case] report_instrument: &str,
4627 #[case] ts_event: u64,
4628 #[case] quantity: &str,
4629 ) {
4630 let client_id = ClientId::from("POSITION-FILLS");
4631 let account_id = AccountId::from("POSITION-FILLS-001");
4632 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
4633 let instrument_id = instrument.id();
4634
4635 let report = FillReport::new(
4636 AccountId::from(report_account),
4637 InstrumentId::from(report_instrument),
4638 VenueOrderId::from("V-POSITION-FILLS"),
4639 TradeId::from("T-POSITION-FILLS"),
4640 OrderSide::Buy,
4641 Quantity::from(quantity),
4642 Price::from("100.0"),
4643 Money::zero(instrument.quote_currency()),
4644 LiquiditySide::Taker,
4645 Some(ClientOrderId::from("O-POSITION-FILLS")),
4646 None,
4647 UnixNanos::from(ts_event),
4648 UnixNanos::from(2_000),
4649 None,
4650 );
4651
4652 let client = LiveExecutionClient::new(Box::new(FillReportClient {
4653 client_id,
4654 account_id,
4655 venue: instrument_id.venue,
4656 outcome: FillReportClientOutcome::Reports(vec![report]),
4657 commands: Rc::new(RefCell::new(Vec::new())),
4658 }));
4659
4660 let command = GenerateFillReports::new(
4661 UUID4::new(),
4662 UnixNanos::from(2_000),
4663 Some(instrument_id),
4664 None,
4665 Some(UnixNanos::from(1_000)),
4666 Some(UnixNanos::from(2_000)),
4667 None,
4668 None,
4669 );
4670 let key = (instrument_id, account_id);
4671
4672 let result = request_position_fill_reports(
4673 vec![client],
4674 vec![PositionFillReportQuery {
4675 key,
4676 client_id,
4677 command,
4678 }],
4679 )
4680 .await;
4681
4682 assert!(result.successful_keys.is_empty());
4683 assert!(result.reports.is_empty());
4684 }
4685
4686 #[rstest]
4687 #[case(999)]
4688 #[case(2_001)]
4689 #[tokio::test]
4690 async fn test_request_position_fill_reports_filters_time_window_superset(
4691 #[case] ts_event: u64,
4692 ) {
4693 let client_id = ClientId::from("POSITION-FILLS");
4694 let account_id = AccountId::from("POSITION-FILLS-001");
4695 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
4696 let instrument_id = instrument.id();
4697
4698 let report = FillReport::new(
4699 account_id,
4700 instrument_id,
4701 VenueOrderId::from("V-POSITION-FILLS"),
4702 TradeId::from("T-POSITION-FILLS"),
4703 OrderSide::Buy,
4704 Quantity::from("1.0"),
4705 Price::from("100.0"),
4706 Money::zero(instrument.quote_currency()),
4707 LiquiditySide::Taker,
4708 Some(ClientOrderId::from("O-POSITION-FILLS")),
4709 None,
4710 UnixNanos::from(ts_event),
4711 UnixNanos::from(2_000),
4712 None,
4713 );
4714
4715 let client = LiveExecutionClient::new(Box::new(FillReportClient {
4716 client_id,
4717 account_id,
4718 venue: instrument_id.venue,
4719 outcome: FillReportClientOutcome::Reports(vec![report]),
4720 commands: Rc::new(RefCell::new(Vec::new())),
4721 }));
4722
4723 let command = GenerateFillReports::new(
4724 UUID4::new(),
4725 UnixNanos::from(2_000),
4726 Some(instrument_id),
4727 None,
4728 Some(UnixNanos::from(1_000)),
4729 Some(UnixNanos::from(2_000)),
4730 None,
4731 None,
4732 );
4733 let key = (instrument_id, account_id);
4734
4735 let result = request_position_fill_reports(
4736 vec![client],
4737 vec![PositionFillReportQuery {
4738 key,
4739 client_id,
4740 command,
4741 }],
4742 )
4743 .await;
4744
4745 assert_eq!(result.successful_keys, IndexSet::from([key]));
4746 assert_eq!(result.reports, IndexMap::from([(key, Vec::new())]));
4747 }
4748
4749 #[rstest]
4750 #[case::failed_query(false)]
4751 #[case::local_activity(true)]
4752 fn test_position_fallback_preserves_deferred_venue_only_retries(#[case] local_activity: bool) {
4753 let (mut node, report, _) =
4754 position_fill_test_fixture("PositionRetryRetentionNode", Quantity::from("1.0"));
4755 let active_key = (report.instrument_id, report.account_id);
4756 let deferred_key = (report.instrument_id, AccountId::from("SECOND-001"));
4757 let deferred_reports = vec![
4758 PositionStatusReport::new(
4759 deferred_key.1,
4760 deferred_key.0,
4761 PositionSide::Long,
4762 Quantity::from("2.0"),
4763 UnixNanos::from(1_000),
4764 UnixNanos::from(1_000),
4765 None,
4766 None,
4767 Some(dec!(100.0)),
4768 ),
4769 PositionStatusReport::new(
4770 deferred_key.1,
4771 deferred_key.0,
4772 PositionSide::Short,
4773 Quantity::from("1.0"),
4774 UnixNanos::from(1_000),
4775 UnixNanos::from(1_000),
4776 None,
4777 None,
4778 Some(dec!(100.0)),
4779 ),
4780 ];
4781 let mut check = node
4782 .exec_manager
4783 .prepare_position_report_check(UUID4::new(), &[]);
4784 check.client_coverage.clear();
4785 let events = node.exec_manager.reconcile_position_reports(
4786 &check,
4787 deferred_reports.clone(),
4788 &IndexSet::new(),
4789 &IndexSet::new(),
4790 );
4791 assert!(events.is_empty());
4792 assert_eq!(
4793 node.exec_manager.position_recon_retry_count(&deferred_key),
4794 1
4795 );
4796
4797 let mut position_result = position_report_result(&node, report);
4798 position_result.reports.extend(deferred_reports);
4799 position_result.check.client_coverage.insert(
4800 deferred_key,
4801 ReportClientCoverage::Resolved(IndexSet::from([ClientId::from("SECOND")])),
4802 );
4803 position_result
4804 .check
4805 .activity_revisions
4806 .insert(deferred_key, 0);
4807 position_result
4808 .queried_clients
4809 .insert(ClientId::from("SECOND"));
4810 let mut successful_keys = IndexSet::from([active_key]);
4811 if local_activity {
4812 successful_keys.insert(deferred_key);
4813 node.exec_manager
4814 .record_position_activity(deferred_key.0, deferred_key.1);
4815 }
4816
4817 node.handle_position_fill_report_result(PositionFillReportResult {
4818 position_result,
4819 reports: IndexMap::from([(active_key, Vec::new())]),
4820 successful_keys,
4821 });
4822
4823 assert_eq!(
4824 node.exec_manager.position_recon_retry_count(&deferred_key),
4825 1
4826 );
4827 {
4828 let cache = node.kernel.cache.borrow();
4829 let positions =
4830 cache.positions_open(None, Some(&active_key.0), None, Some(&active_key.1), None);
4831 assert_eq!(
4832 positions
4833 .iter()
4834 .map(|position| position.quantity)
4835 .collect::<Vec<_>>(),
4836 vec![Quantity::from("1.0"), Quantity::from("1.0")],
4837 );
4838 assert_eq!(
4839 cache
4840 .positions_open(
4841 None,
4842 Some(&deferred_key.0),
4843 None,
4844 Some(&deferred_key.1),
4845 None,
4846 )
4847 .len(),
4848 0
4849 );
4850 }
4851
4852 let mut fresh_check = node
4853 .exec_manager
4854 .prepare_position_report_check(UUID4::new(), &[]);
4855 node.exec_manager.plan_position_fill_reports(
4856 &mut fresh_check,
4857 &[],
4858 &IndexSet::new(),
4859 &IndexSet::new(),
4860 &[],
4861 );
4862
4863 assert_eq!(
4864 node.exec_manager.position_recon_retry_count(&deferred_key),
4865 0
4866 );
4867 }
4868
4869 #[rstest]
4870 fn test_position_fill_report_result_applies_authoritative_fill_without_synthetic_order() {
4871 let (mut node, venue_report, fill_report) =
4872 position_fill_test_fixture("AuthoritativePositionFillNode", Quantity::from("1.0"));
4873 let key = (venue_report.instrument_id, venue_report.account_id);
4874 let revision = node.exec_manager.position_activity_revision(&key);
4875 let position_result = position_report_result(&node, venue_report);
4876
4877 node.handle_position_fill_report_result(PositionFillReportResult {
4878 position_result,
4879 reports: IndexMap::from([(key, vec![fill_report.clone()])]),
4880 successful_keys: IndexSet::from([key]),
4881 });
4882
4883 let cache = node.kernel.cache.borrow();
4884 let positions = cache.positions_open(None, Some(&key.0), None, Some(&key.1), None);
4885 assert_eq!(
4886 positions
4887 .iter()
4888 .map(|position| position.quantity)
4889 .sum::<Quantity>(),
4890 Quantity::from("2.0")
4891 );
4892 assert_eq!(
4893 cache.orders_total_count(None, Some(&key.0), None, Some(&key.1), None),
4894 1
4895 );
4896 drop(positions);
4897 drop(cache);
4898 assert!(
4899 node.exec_manager
4900 .position_contains_fill_report(&fill_report)
4901 );
4902 assert_eq!(
4903 node.exec_manager.position_activity_revision(&key),
4904 revision + 1
4905 );
4906 }
4907
4908 #[rstest]
4909 fn test_position_fill_report_match_ignores_adapter_timestamp_source() {
4910 let (mut node, venue_report, fill_report) =
4911 position_fill_test_fixture("PositionFillTimestampNode", Quantity::from("1.0"));
4912 let key = (venue_report.instrument_id, venue_report.account_id);
4913 let position_result = position_report_result(&node, venue_report);
4914
4915 node.handle_position_fill_report_result(PositionFillReportResult {
4916 position_result,
4917 reports: IndexMap::from([(key, vec![fill_report.clone()])]),
4918 successful_keys: IndexSet::from([key]),
4919 });
4920
4921 let mut rest_report = fill_report;
4922 rest_report.ts_event = UnixNanos::from(2_000);
4923
4924 assert!(
4925 node.exec_manager
4926 .position_contains_fill_report(&rest_report)
4927 );
4928 }
4929
4930 #[rstest]
4931 fn test_position_fill_report_result_falls_back_when_order_contains_inferred_fill() {
4932 let (mut node, venue_report, fill_report) =
4933 position_fill_test_fixture("InferredPositionFillNode", Quantity::from("1.0"));
4934 let key = (venue_report.instrument_id, venue_report.account_id);
4935 let client_order_id = fill_report.client_order_id.unwrap();
4936 apply_inferred_position_fill(&mut node, &fill_report);
4937
4938 let venue_report = PositionStatusReport::new(
4939 key.1,
4940 key.0,
4941 PositionSide::Long,
4942 Quantity::from("3.0"),
4943 venue_report.ts_last,
4944 venue_report.ts_init,
4945 None,
4946 None,
4947 venue_report.avg_px_open,
4948 );
4949 let mut later_fill_report = fill_report.clone();
4950 later_fill_report.trade_id = TradeId::from("T-POSITION-AUTHORITATIVE-LATER");
4951 later_fill_report.ts_event = UnixNanos::from(1_001);
4952 let position_result = position_report_result(&node, venue_report);
4953
4954 node.handle_position_fill_report_result(PositionFillReportResult {
4955 position_result,
4956 reports: IndexMap::from([(key, vec![fill_report.clone(), later_fill_report.clone()])]),
4957 successful_keys: IndexSet::from([key]),
4958 });
4959
4960 let cache = node.kernel.cache.borrow();
4961 let order = cache.order(&client_order_id).unwrap();
4962 let positions = cache.positions_open(None, Some(&key.0), None, Some(&key.1), None);
4963 assert_eq!(order.filled_qty(), Quantity::from("2.0"));
4964 assert!(!order.trade_ids().contains(&&fill_report.trade_id));
4965 assert!(!order.trade_ids().contains(&&later_fill_report.trade_id));
4966 assert_eq!(
4967 positions
4968 .iter()
4969 .map(|position| position.quantity)
4970 .sum::<Quantity>(),
4971 Quantity::from("3.0")
4972 );
4973 assert_eq!(
4974 cache.orders_total_count(None, Some(&key.0), None, Some(&key.1), None),
4975 2
4976 );
4977 }
4978
4979 #[rstest]
4980 fn test_position_fill_report_validates_hedge_identity_before_inferred_fallback() {
4981 let (mut node, _, mut fill_report) =
4982 position_fill_test_fixture("InferredHedgeIdentityNode", Quantity::from("1.0"));
4983 apply_inferred_position_fill(&mut node, &fill_report);
4984 let conflicting_position_id = PositionId::from("P-POSITION-CONFLICT");
4985 fill_report.venue_position_id = Some(conflicting_position_id);
4986
4987 let error = node
4988 .exec_manager
4989 .prepare_position_fill_report(&mut fill_report, &[])
4990 .unwrap_err();
4991
4992 assert!(
4993 error.to_string().contains(&format!(
4994 "position ID {conflicting_position_id} conflicts with cached order position"
4995 )),
4996 "{error:#}"
4997 );
4998 }
4999
5000 #[rstest]
5001 fn test_position_fill_report_result_synthesizes_only_residual_after_fresh_report() {
5002 let (mut node, venue_report, fill_report) =
5003 position_fill_test_fixture("ResidualPositionFillNode", Quantity::from("0.5"));
5004 let key = (venue_report.instrument_id, venue_report.account_id);
5005 let first_result = position_report_result(&node, venue_report.clone());
5006
5007 node.handle_position_fill_report_result(PositionFillReportResult {
5008 position_result: first_result,
5009 reports: IndexMap::from([(key, vec![fill_report])]),
5010 successful_keys: IndexSet::from([key]),
5011 });
5012
5013 let fresh_result = position_report_result(&node, venue_report);
5014 node.handle_position_fill_report_result(PositionFillReportResult {
5015 position_result: fresh_result,
5016 reports: IndexMap::from([(key, Vec::new())]),
5017 successful_keys: IndexSet::from([key]),
5018 });
5019
5020 let cache = node.kernel.cache.borrow();
5021 let positions = cache.positions_open(None, Some(&key.0), None, Some(&key.1), None);
5022 assert_eq!(
5023 positions
5024 .iter()
5025 .map(|position| position.quantity)
5026 .sum::<Quantity>(),
5027 Quantity::from("2.0")
5028 );
5029 assert_eq!(
5030 cache.orders_total_count(None, Some(&key.0), None, Some(&key.1), None),
5031 2
5032 );
5033 }
5034
5035 #[rstest]
5036 fn test_position_fill_report_failure_does_not_trigger_synthetic_fallback() {
5037 let (mut node, venue_report, _) =
5038 position_fill_test_fixture("FailedPositionFillNode", Quantity::from("1.0"));
5039 let key = (venue_report.instrument_id, venue_report.account_id);
5040 let position_result = position_report_result(&node, venue_report);
5041
5042 node.handle_position_fill_report_result(PositionFillReportResult {
5043 position_result,
5044 reports: IndexMap::new(),
5045 successful_keys: IndexSet::new(),
5046 });
5047
5048 let cache = node.kernel.cache.borrow();
5049 let positions = cache.positions_open(None, Some(&key.0), None, Some(&key.1), None);
5050 assert_eq!(positions.len(), 1);
5051 assert_eq!(positions[0].quantity, Quantity::from("1.0"));
5052 assert_eq!(
5053 cache.orders_total_count(None, Some(&key.0), None, Some(&key.1), None),
5054 1
5055 );
5056 }
5057
5058 #[rstest]
5059 fn test_position_fill_report_result_falls_back_for_unattributable_hedge_fill() {
5060 let (mut node, mut venue_report, mut fill_report) =
5061 position_fill_test_fixture("UnattributedHedgeFillNode", Quantity::from("1.0"));
5062 node.kernel
5063 .exec_engine
5064 .borrow_mut()
5065 .register_oms_type(StrategyId::from("EXTERNAL"), OmsType::Hedging);
5066 let key = (venue_report.instrument_id, venue_report.account_id);
5067
5068 let position_id = {
5069 let cache = node.kernel.cache.borrow();
5070 let positions = cache.positions_open(None, Some(&key.0), None, Some(&key.1), None);
5071 assert_eq!(positions.len(), 1);
5072 positions[0].id
5073 };
5074
5075 venue_report.venue_position_id = Some(position_id);
5076 fill_report.client_order_id = None;
5077 fill_report.venue_order_id = VenueOrderId::from("V-POSITION-EXTERNAL");
5078 let position_result = position_report_result(&node, venue_report);
5079
5080 node.handle_position_fill_report_result(PositionFillReportResult {
5081 position_result,
5082 reports: IndexMap::from([(key, vec![fill_report.clone()])]),
5083 successful_keys: IndexSet::from([key]),
5084 });
5085
5086 let cache = node.kernel.cache.borrow();
5087 let positions = cache.positions_open(None, Some(&key.0), None, Some(&key.1), None);
5088 assert_eq!(positions.len(), 1);
5089 assert_eq!(positions[0].id, position_id);
5090 assert_eq!(positions[0].quantity, Quantity::from("2.0"));
5091 assert_eq!(
5092 cache.orders_total_count(None, Some(&key.0), None, Some(&key.1), None),
5093 2
5094 );
5095 drop(positions);
5096 drop(cache);
5097 assert!(
5098 !node
5099 .exec_manager
5100 .position_contains_fill_report(&fill_report)
5101 );
5102 }
5103
5104 #[rstest]
5105 fn test_position_fill_report_result_defers_after_local_position_activity() {
5106 let (mut node, venue_report, fill_report) =
5107 position_fill_test_fixture("StalePositionFillNode", Quantity::from("1.0"));
5108 let key = (venue_report.instrument_id, venue_report.account_id);
5109 let position_result = position_report_result(&node, venue_report);
5110 node.exec_manager.record_position_activity(key.0, key.1);
5111
5112 node.handle_position_fill_report_result(PositionFillReportResult {
5113 position_result,
5114 reports: IndexMap::from([(key, vec![fill_report.clone()])]),
5115 successful_keys: IndexSet::from([key]),
5116 });
5117
5118 let cache = node.kernel.cache.borrow();
5119 let positions = cache.positions_open(None, Some(&key.0), None, Some(&key.1), None);
5120 assert_eq!(positions.len(), 1);
5121 assert_eq!(positions[0].quantity, Quantity::from("1.0"));
5122 assert_eq!(
5123 cache.orders_total_count(None, Some(&key.0), None, Some(&key.1), None),
5124 1
5125 );
5126 drop(positions);
5127 drop(cache);
5128 assert!(
5129 !node
5130 .exec_manager
5131 .position_contains_fill_report(&fill_report)
5132 );
5133 }
5134
5135 #[rstest]
5136 fn test_observe_exec_event_before_dispatch_accepted_batch_stamps_local_activity() {
5137 let config = LiveNodeConfig {
5138 exec_engine: crate::config::LiveExecutionEngineConfig {
5139 reconciliation: true,
5140 open_check_threshold_ms: 5_000,
5141 single_order_query_delay_ms: 0,
5142 ..Default::default()
5143 },
5144 ..Default::default()
5145 };
5146
5147 let mut node = LiveNode::build("AcceptedBatchNode".to_string(), Some(config)).unwrap();
5148 let account_id = AccountId::from("TEST-ACCEPTED-BATCH-001");
5149 let client_id = ClientId::from("TEST-ACCEPTED-BATCH");
5150 let instrument = crypto_perpetual_ethusdt();
5151 let instrument_id = instrument.id();
5152 let client_order_id = ClientOrderId::from("O-ACCEPTED-BATCH");
5153 let venue_order_id = VenueOrderId::from("V-ACCEPTED-BATCH");
5154
5155 node.kernel
5156 .cache
5157 .borrow_mut()
5158 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
5159 .unwrap();
5160 insert_accepted_limit_order_in_node(
5161 &node,
5162 account_id,
5163 client_id,
5164 instrument_id,
5165 client_order_id,
5166 venue_order_id,
5167 );
5168
5169 assert_eq!(node.exec_manager.check_open_order_queries().len(), 1);
5170
5171 let accepted = OrderAcceptedSpec::builder()
5172 .instrument_id(instrument_id)
5173 .client_order_id(client_order_id)
5174 .venue_order_id(venue_order_id)
5175 .account_id(account_id)
5176 .build();
5177 let event = ExecutionEvent::OrderAcceptedBatch(OrderAcceptedBatch::new(vec![accepted]));
5178
5179 let close_ids = node.observe_exec_event_before_dispatch(&event);
5180
5181 assert_eq!(close_ids, Some(Vec::new()));
5182 assert!(node.exec_manager.check_open_order_queries().is_empty());
5183 }
5184
5185 #[rstest]
5186 #[cfg_attr(
5187 not(all(feature = "simulation", madsim)),
5188 tokio::test(start_paused = true)
5189 )]
5190 #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
5191 async fn test_batch_cancel_command_registers_each_child_for_inflight_timeout() {
5192 use nautilus_common::messages::execution::{BatchCancelOrders, CancelOrder};
5193 use nautilus_model::{events::OrderPendingCancel, identifiers::ClientOrderId};
5194
5195 let config = LiveNodeConfig {
5196 exec_engine: crate::config::LiveExecutionEngineConfig {
5197 reconciliation: true,
5198 inflight_check_threshold_ms: 100,
5199 inflight_check_retries: 1,
5200 ..Default::default()
5201 },
5202 ..Default::default()
5203 };
5204
5205 let mut node = LiveNode::build("BatchCancelNode".to_string(), Some(config)).unwrap();
5206 let trader_id = TraderId::from("TESTER-001");
5207 let strategy_id = StrategyId::from("S-BATCH-CANCEL");
5208 let account_id = AccountId::from("TEST-001");
5209 let instrument = crypto_perpetual_ethusdt();
5210 let instrument_id = instrument.id();
5211 let child_ids = [
5212 ClientOrderId::from("O-BATCH-CANCEL-1"),
5213 ClientOrderId::from("O-BATCH-CANCEL-2"),
5214 ];
5215 node.kernel
5216 .cache
5217 .borrow_mut()
5218 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
5219 .unwrap();
5220
5221 for client_order_id in child_ids {
5222 let order = OrderTestBuilder::new(OrderType::Limit)
5223 .trader_id(trader_id)
5224 .strategy_id(strategy_id)
5225 .client_order_id(client_order_id)
5226 .instrument_id(instrument_id)
5227 .quantity(Quantity::from("10.0"))
5228 .price(Price::from("100.0"))
5229 .build();
5230 let venue_order_id = VenueOrderId::from(format!("V-{client_order_id}").as_str());
5231 let submitted = TestOrderEventStubs::submitted(&order, account_id);
5235 let accepted = TestOrderEventStubs::accepted(&order, account_id, venue_order_id);
5236 let pending_cancel = OrderEventAny::PendingCancel(OrderPendingCancel::new(
5237 trader_id,
5238 strategy_id,
5239 instrument_id,
5240 client_order_id,
5241 Some(account_id),
5242 UUID4::new(),
5243 UnixNanos::default(),
5244 UnixNanos::default(),
5245 false,
5246 Some(venue_order_id),
5247 ));
5248 let mut cache = node.kernel.cache.borrow_mut();
5249 cache.add_order(order, None, None, false).unwrap();
5250 cache.update_order(&submitted).unwrap();
5251 cache.update_order(&accepted).unwrap();
5252 cache.update_order(&pending_cancel).unwrap();
5253 }
5254
5255 let cancels = child_ids
5256 .into_iter()
5257 .map(|client_order_id| {
5258 CancelOrder::new(
5259 trader_id,
5260 None,
5261 strategy_id,
5262 instrument_id,
5263 client_order_id,
5264 None,
5265 UUID4::new(),
5266 UnixNanos::default(),
5267 None,
5268 None,
5269 )
5270 })
5271 .collect();
5272
5273 let command = TradingCommand::CancelOrders(BatchCancelOrders::new(
5274 trader_id,
5275 None,
5276 strategy_id,
5277 instrument_id,
5278 cancels,
5279 UUID4::new(),
5280 UnixNanos::default(),
5281 None,
5282 None,
5283 ));
5284
5285 node.observe_exec_command_before_dispatch(&command);
5286 advance_clock(Duration::from_millis(101)).await;
5287 let result = node.exec_manager.check_inflight_orders();
5288 let timed_out_ids = result
5289 .events
5290 .iter()
5291 .map(OrderEventAny::client_order_id)
5292 .collect::<IndexSet<_>>();
5293
5294 assert_eq!(timed_out_ids, IndexSet::from(child_ids));
5295 assert_eq!(result.events.len(), child_ids.len());
5296 assert!(
5297 result
5298 .events
5299 .iter()
5300 .all(|event| matches!(event, OrderEventAny::Canceled(_))),
5301 "batch-cancel children must time out as Canceled events",
5302 );
5303 }
5304
5305 #[rstest]
5306 #[cfg_attr(
5307 not(all(feature = "simulation", madsim)),
5308 tokio::test(start_paused = true)
5309 )]
5310 #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
5311 async fn test_risk_bound_command_does_not_register_inflight() {
5312 let config = LiveNodeConfig {
5313 exec_engine: crate::config::LiveExecutionEngineConfig {
5314 reconciliation: true,
5315 inflight_check_threshold_ms: 100,
5316 inflight_check_retries: 2,
5317 ..Default::default()
5318 },
5319 ..Default::default()
5320 };
5321
5322 let mut node = LiveNode::build("RiskBoundNode".to_string(), Some(config)).unwrap();
5323 msgbus::register_trading_command_endpoint(
5324 MessagingSwitchboard::risk_engine_execute(),
5325 TypedIntoHandler::from(|_: TradingCommand| {}),
5326 );
5327 let instrument = crypto_perpetual_ethusdt();
5328 let instrument_id = instrument.id();
5329 let order = OrderTestBuilder::new(OrderType::Limit)
5330 .trader_id(node.trader_id())
5331 .strategy_id(StrategyId::from("S-RISK-DENIED"))
5332 .instrument_id(instrument_id)
5333 .side(OrderSide::Buy)
5334 .quantity(Quantity::from("1.000"))
5335 .price(Price::from("100.00"))
5336 .build();
5337 let client_order_id = order.client_order_id();
5338
5339 {
5340 let mut cache = node.kernel.cache.borrow_mut();
5341 cache
5342 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
5343 .unwrap();
5344 cache.add_order(order.clone(), None, None, false).unwrap();
5345 }
5346
5347 let submit_order = SubmitOrder::new(
5348 order.trader_id(),
5349 None,
5350 order.strategy_id(),
5351 instrument_id,
5352 client_order_id,
5353 order.init_event().clone(),
5354 None,
5355 None,
5356 None,
5357 UUID4::new(),
5358 UnixNanos::default(),
5359 None,
5360 );
5361 node.process_exec_command(
5362 TradingCommandMessage::new(
5363 MessagingSwitchboard::risk_engine_execute(),
5364 TradingCommand::SubmitOrder(submit_order),
5365 )
5366 .into(),
5367 );
5368
5369 advance_clock(Duration::from_millis(101)).await;
5370 let result = node.exec_manager.check_inflight_orders();
5371 let status = node
5372 .kernel
5373 .cache
5374 .borrow()
5375 .order(&client_order_id)
5376 .unwrap()
5377 .status();
5378
5379 assert_eq!(status, OrderStatus::Initialized);
5380 assert_eq!(
5381 node.exec_manager.recon_check_retry_count(&client_order_id),
5382 0
5383 );
5384 assert!(result.events.is_empty());
5385 assert!(result.queries.is_empty());
5386 }
5387
5388 #[rstest]
5389 #[cfg_attr(
5390 not(all(feature = "simulation", madsim)),
5391 tokio::test(start_paused = true)
5392 )]
5393 #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
5394 async fn test_risk_approved_command_registers_inflight() {
5395 let config = LiveNodeConfig {
5396 risk_engine: crate::config::LiveRiskEngineConfig {
5397 bypass: true,
5398 ..Default::default()
5399 },
5400 exec_engine: crate::config::LiveExecutionEngineConfig {
5401 reconciliation: true,
5402 inflight_check_threshold_ms: 100,
5403 inflight_check_retries: 2,
5404 ..Default::default()
5405 },
5406 ..Default::default()
5407 };
5408
5409 let mut node = LiveNode::build("RiskApprovedNode".to_string(), Some(config)).unwrap();
5410 msgbus::register_trading_command_endpoint(
5411 MessagingSwitchboard::exec_engine_execute(),
5412 TypedIntoHandler::from(|_: TradingCommand| {}),
5413 );
5414 let instrument = crypto_perpetual_ethusdt();
5415 let instrument_id = instrument.id();
5416 let order = OrderTestBuilder::new(OrderType::Limit)
5417 .trader_id(node.trader_id())
5418 .strategy_id(StrategyId::from("S-RISK-APPROVED"))
5419 .instrument_id(instrument_id)
5420 .side(OrderSide::Buy)
5421 .quantity(Quantity::from("1.000"))
5422 .price(Price::from("100.00"))
5423 .build();
5424 let client_order_id = order.client_order_id();
5425
5426 {
5427 let mut cache = node.kernel.cache.borrow_mut();
5428 cache
5429 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
5430 .unwrap();
5431 cache.add_order(order.clone(), None, None, false).unwrap();
5432 }
5433
5434 let submit_order = SubmitOrder::new(
5435 order.trader_id(),
5436 None,
5437 order.strategy_id(),
5438 instrument_id,
5439 client_order_id,
5440 order.init_event().clone(),
5441 None,
5442 None,
5443 None,
5444 UUID4::new(),
5445 UnixNanos::default(),
5446 None,
5447 );
5448 node.process_exec_command(
5449 TradingCommandMessage::new(
5450 MessagingSwitchboard::risk_engine_execute(),
5451 TradingCommand::SubmitOrder(submit_order),
5452 )
5453 .into(),
5454 );
5455
5456 advance_clock(Duration::from_millis(101)).await;
5457 let result = node.exec_manager.check_inflight_orders();
5458
5459 let [TradingCommand::QueryOrder(query)] = result.queries.as_slice() else {
5460 panic!("expected one query order command");
5461 };
5462
5463 assert_eq!(query.client_order_id, client_order_id);
5464 assert_eq!(
5465 node.exec_manager.recon_check_retry_count(&client_order_id),
5466 1
5467 );
5468 assert!(result.events.is_empty());
5469 }
5470
5471 #[rstest]
5472 fn test_live_node_builder_clock_factory_drives_kernel_clock() {
5473 let calls = Rc::new(Cell::new(0usize));
5474 let calls_in_factory = calls.clone();
5475 let sentinel = UnixNanos::from(123_456_789_u64);
5476
5477 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5478 .unwrap()
5479 .with_reconciliation(false)
5480 .with_clock_factory(move || {
5481 calls_in_factory.set(calls_in_factory.get() + 1);
5482 let mut clock = VirtualClock::new();
5483 clock.advance_time(sentinel, true);
5484 Rc::new(RefCell::new(clock)) as Rc<RefCell<dyn Clock>>
5485 })
5486 .build()
5487 .unwrap();
5488
5489 assert_eq!(node.kernel().clock().borrow().timestamp_ns(), sentinel);
5490 assert_eq!(calls.get(), 1);
5491 }
5492
5493 #[derive(Debug)]
5494 struct ReplayKernelEventStore {
5495 fail_restore: bool,
5496 }
5497
5498 impl KernelEventStore for ReplayKernelEventStore {
5499 fn restore_parent_cache(
5500 &mut self,
5501 _instance_id: UUID4,
5502 _cache: &mut Cache,
5503 ) -> anyhow::Result<()> {
5504 if self.fail_restore {
5505 anyhow::bail!("replay restore failed");
5506 }
5507
5508 Ok(())
5509 }
5510
5511 fn open(
5512 &mut self,
5513 _instance_id: UUID4,
5514 _components: &RegisteredComponents,
5515 _environment: Environment,
5516 ) -> anyhow::Result<()> {
5517 Ok(())
5518 }
5519
5520 fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
5521 None
5522 }
5523
5524 fn seal(&mut self, _ts_init: UnixNanos) {}
5525
5526 fn run_id(&self) -> Option<&str> {
5527 Some("replay-child")
5528 }
5529
5530 fn parent_run_id(&self) -> Option<&str> {
5531 Some("seed-run")
5532 }
5533
5534 fn is_event_store_replay_configured(&self) -> bool {
5535 true
5536 }
5537
5538 fn is_halted(&self) -> bool {
5539 false
5540 }
5541 }
5542
5543 #[derive(Debug)]
5544 struct TestStrategy {
5545 core: StrategyCore,
5546 }
5547
5548 impl TestStrategy {
5549 fn new(config: StrategyConfig) -> Self {
5550 Self {
5551 core: StrategyCore::new(config),
5552 }
5553 }
5554 }
5555
5556 impl DataActor for TestStrategy {}
5557
5558 nautilus_strategy!(TestStrategy, {
5559 fn external_order_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
5560 self.core.config.external_order_instrument_ids.clone()
5561 }
5562 });
5563
5564 fn live_node_with_replay_store(fail_restore: bool) -> LiveNode {
5565 let builder = LiveNodeBuilder::new(TraderId::default(), Environment::Live)
5568 .unwrap()
5569 .with_exec_engine_config(crate::config::LiveExecutionEngineConfig {
5570 reconciliation: false,
5571 ..Default::default()
5572 })
5573 .with_load_state(true)
5574 .with_name("TestKernel")
5575 .with_event_store(move |_instance_id: UUID4, _clock: Rc<RefCell<dyn Clock>>| {
5576 Ok(Box::new(ReplayKernelEventStore { fail_restore }) as Box<dyn KernelEventStore>)
5577 });
5578
5579 builder.build().unwrap()
5580 }
5581
5582 #[rstest]
5583 fn test_add_strategy_registers_external_order_claims_with_manager_and_engine() {
5584 let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5585 .unwrap()
5586 .with_reconciliation(false)
5587 .with_delay_post_stop_secs(0)
5588 .with_timeout_connection(1)
5589 .build()
5590 .unwrap();
5591 let instrument_id = InstrumentId::from("AUDUSD.SIM");
5592 let strategy_id = StrategyId::from("CLAIMS-001");
5593
5594 node.add_strategy(TestStrategy::new(StrategyConfig {
5595 strategy_id: Some(strategy_id),
5596 external_order_instrument_ids: Some(vec![instrument_id]),
5597 ..Default::default()
5598 }))
5599 .unwrap();
5600
5601 assert_eq!(
5602 node.exec_manager.get_external_order_claim(&instrument_id),
5603 Some(strategy_id)
5604 );
5605
5606 {
5607 let exec_engine = node.kernel().exec_engine.borrow();
5608 assert_eq!(
5609 exec_engine.get_external_order_claim(&instrument_id),
5610 Some(strategy_id)
5611 );
5612 }
5613 }
5614
5615 #[rstest]
5616 fn test_register_external_order_claims_after_build_is_visible_to_manager_and_engine() {
5617 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5618 .unwrap()
5619 .with_reconciliation(false)
5620 .build()
5621 .unwrap();
5622 let instrument_id = InstrumentId::from("AUDUSD.SIM");
5623 let strategy_id = StrategyId::from("CLAIMS-001");
5624
5625 node.register_external_order_claims(strategy_id, &[instrument_id])
5626 .unwrap();
5627
5628 assert_eq!(
5629 node.exec_manager.get_external_order_claim(&instrument_id),
5630 Some(strategy_id)
5631 );
5632 assert_eq!(
5633 node.kernel
5634 .exec_engine
5635 .borrow()
5636 .get_external_order_claim(&instrument_id),
5637 Some(strategy_id)
5638 );
5639 }
5640
5641 #[rstest]
5642 #[tokio::test]
5643 async fn test_register_external_order_claims_while_running_is_visible_to_manager_and_engine() {
5644 let mut node = live_node_with_replay_store(false);
5645 let instrument_id = InstrumentId::from("AUDUSD.SIM");
5646 let strategy_id = StrategyId::from("CLAIMS-001");
5647
5648 node.start().await.unwrap();
5649 assert_eq!(node.state(), NodeState::Running);
5650
5651 node.register_external_order_claims(strategy_id, &[instrument_id])
5652 .unwrap();
5653
5654 assert_eq!(
5655 node.exec_manager.get_external_order_claim(&instrument_id),
5656 Some(strategy_id)
5657 );
5658 assert_eq!(
5659 node.kernel
5660 .exec_engine
5661 .borrow()
5662 .get_external_order_claim(&instrument_id),
5663 Some(strategy_id)
5664 );
5665 }
5666
5667 #[rstest]
5668 fn test_register_external_order_claims_conflicting_batch_leaves_new_claims_absent() {
5669 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5670 .unwrap()
5671 .with_reconciliation(false)
5672 .build()
5673 .unwrap();
5674 let existing_instrument = InstrumentId::from("AUDUSD.SIM");
5675 let new_instruments = [
5676 InstrumentId::from("EURUSD.SIM"),
5677 InstrumentId::from("GBPUSD.SIM"),
5678 ];
5679 let existing_strategy_id = StrategyId::from("CLAIMS-001");
5680 let new_strategy_id = StrategyId::from("CLAIMS-002");
5681 node.register_external_order_claims(existing_strategy_id, &[existing_instrument])
5682 .unwrap();
5683
5684 let result = node.register_external_order_claims(
5685 new_strategy_id,
5686 &[new_instruments[0], existing_instrument, new_instruments[1]],
5687 );
5688
5689 assert!(result.is_err());
5690 assert_eq!(
5691 node.exec_manager
5692 .get_external_order_claim(&existing_instrument),
5693 Some(existing_strategy_id)
5694 );
5695
5696 for instrument_id in new_instruments {
5697 assert_eq!(
5698 node.exec_manager.get_external_order_claim(&instrument_id),
5699 None
5700 );
5701 assert_eq!(
5702 node.kernel
5703 .exec_engine
5704 .borrow()
5705 .get_external_order_claim(&instrument_id),
5706 None
5707 );
5708 }
5709 }
5710
5711 #[rstest]
5712 fn test_deregister_external_order_claims_allows_successor_to_claim() {
5713 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5714 .unwrap()
5715 .with_reconciliation(false)
5716 .build()
5717 .unwrap();
5718 let instruments = [
5719 InstrumentId::from("AUDUSD.SIM"),
5720 InstrumentId::from("EURUSD.SIM"),
5721 ];
5722 let first_strategy_id = StrategyId::from("CLAIMS-001");
5723 let successor_strategy_id = StrategyId::from("CLAIMS-002");
5724 node.register_external_order_claims(first_strategy_id, &instruments)
5725 .unwrap();
5726
5727 node.deregister_external_order_claims(first_strategy_id)
5728 .unwrap();
5729 node.register_external_order_claims(successor_strategy_id, &instruments)
5730 .unwrap();
5731
5732 for instrument_id in instruments {
5733 assert_eq!(
5734 node.exec_manager.get_external_order_claim(&instrument_id),
5735 Some(successor_strategy_id)
5736 );
5737 assert_eq!(
5738 node.kernel
5739 .exec_engine
5740 .borrow()
5741 .get_external_order_claim(&instrument_id),
5742 Some(successor_strategy_id)
5743 );
5744 }
5745 }
5746
5747 #[rstest]
5748 fn test_deregister_external_order_claims_without_claims_is_idempotent() {
5749 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5750 .unwrap()
5751 .with_reconciliation(false)
5752 .build()
5753 .unwrap();
5754 let strategy_id = StrategyId::from("CLAIMS-001");
5755
5756 node.deregister_external_order_claims(strategy_id).unwrap();
5757 node.deregister_external_order_claims(strategy_id).unwrap();
5758 }
5759
5760 #[rstest]
5761 fn test_add_strategy_rejects_duplicate_external_order_claim_without_overwriting() {
5762 let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5763 .unwrap()
5764 .with_reconciliation(false)
5765 .with_delay_post_stop_secs(0)
5766 .with_timeout_connection(1)
5767 .build()
5768 .unwrap();
5769 let instrument_id = InstrumentId::from("AUDUSD.SIM");
5770 let strategy_id = StrategyId::from("CLAIMS-001");
5771 let duplicate_strategy_id = StrategyId::from("OTHER-002");
5772
5773 node.add_strategy(TestStrategy::new(StrategyConfig {
5774 strategy_id: Some(strategy_id),
5775 external_order_instrument_ids: Some(vec![instrument_id]),
5776 ..Default::default()
5777 }))
5778 .unwrap();
5779
5780 let result = node.add_strategy(TestStrategy::new(StrategyConfig {
5781 strategy_id: Some(duplicate_strategy_id),
5782 external_order_instrument_ids: Some(vec![instrument_id]),
5783 ..Default::default()
5784 }));
5785
5786 assert!(result.is_err());
5787 assert!(
5788 result
5789 .unwrap_err()
5790 .to_string()
5791 .contains("already exists for CLAIMS-001")
5792 );
5793 assert_eq!(
5794 node.exec_manager.get_external_order_claim(&instrument_id),
5795 Some(strategy_id)
5796 );
5797
5798 {
5799 let exec_engine = node.kernel().exec_engine.borrow();
5800 assert_eq!(
5801 exec_engine.get_external_order_claim(&instrument_id),
5802 Some(strategy_id)
5803 );
5804 }
5805 }
5806
5807 #[rstest]
5808 fn test_add_strategy_rejects_repeated_external_order_claim_without_registering() {
5809 let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5810 .unwrap()
5811 .with_reconciliation(false)
5812 .with_delay_post_stop_secs(0)
5813 .with_timeout_connection(1)
5814 .build()
5815 .unwrap();
5816 let instrument_id = InstrumentId::from("AUDUSD.SIM");
5817 let strategy_id = StrategyId::from("CLAIMS-001");
5818
5819 let result = node.add_strategy(TestStrategy::new(StrategyConfig {
5820 strategy_id: Some(strategy_id),
5821 external_order_instrument_ids: Some(vec![instrument_id, instrument_id]),
5822 ..Default::default()
5823 }));
5824
5825 assert!(result.is_err());
5826 assert!(
5827 result
5828 .unwrap_err()
5829 .to_string()
5830 .contains("appears more than once for CLAIMS-001")
5831 );
5832 assert_eq!(
5833 node.exec_manager.get_external_order_claim(&instrument_id),
5834 None
5835 );
5836
5837 {
5838 let exec_engine = node.kernel().exec_engine.borrow();
5839 assert_eq!(exec_engine.get_external_order_claim(&instrument_id), None);
5840 }
5841 }
5842
5843 #[rstest]
5844 fn test_add_strategy_failure_restores_external_order_claims() {
5845 let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5846 .unwrap()
5847 .with_reconciliation(false)
5848 .with_delay_post_stop_secs(0)
5849 .with_timeout_connection(1)
5850 .build()
5851 .unwrap();
5852 let existing_instrument_id = InstrumentId::from("AUDUSD.SIM");
5853 let configured_instrument_id = InstrumentId::from("EURUSD.SIM");
5854 let strategy_id = StrategyId::from("CLAIMS-001");
5855 node.register_external_order_claims(strategy_id, &[existing_instrument_id])
5856 .unwrap();
5857
5858 let mut strategy = TestStrategy::new(StrategyConfig {
5859 strategy_id: Some(strategy_id),
5860 external_order_instrument_ids: Some(vec![configured_instrument_id]),
5861 ..Default::default()
5862 });
5863
5864 strategy
5865 .core
5866 .register(
5867 node.trader_id(),
5868 node.kernel.clock(),
5869 node.kernel.cache.clone(),
5870 node.kernel.portfolio.clone(),
5871 )
5872 .unwrap();
5873
5874 let result = node.add_strategy(strategy);
5875
5876 assert!(result.is_err());
5877 assert!(
5878 result
5879 .unwrap_err()
5880 .to_string()
5881 .contains("already registered with trader")
5882 );
5883 assert_eq!(
5884 node.exec_manager
5885 .get_external_order_claim(&existing_instrument_id),
5886 Some(strategy_id)
5887 );
5888 assert_eq!(
5889 node.kernel
5890 .exec_engine
5891 .borrow()
5892 .get_external_order_claim(&existing_instrument_id),
5893 Some(strategy_id)
5894 );
5895 assert_eq!(
5896 node.exec_manager
5897 .get_external_order_claim(&configured_instrument_id),
5898 None
5899 );
5900 assert_eq!(
5901 node.kernel
5902 .exec_engine
5903 .borrow()
5904 .get_external_order_claim(&configured_instrument_id),
5905 None
5906 );
5907 }
5908
5909 #[rstest]
5910 fn test_add_strategy_without_claims_or_oms_type_does_not_require_engine_borrow() {
5911 let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5912 .unwrap()
5913 .with_reconciliation(false)
5914 .build()
5915 .unwrap();
5916 let exec_engine = node.kernel.exec_engine.clone();
5917 let _engine_borrow = exec_engine.borrow_mut();
5918
5919 node.add_strategy(TestStrategy::new(StrategyConfig {
5920 strategy_id: Some(StrategyId::from("NOCLAIMS-001")),
5921 ..Default::default()
5922 }))
5923 .unwrap();
5924 }
5925
5926 #[rstest]
5927 fn test_add_strategy_registers_configured_hedging_oms_type() {
5928 let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5929 .unwrap()
5930 .with_reconciliation(false)
5931 .with_delay_post_stop_secs(0)
5932 .with_timeout_connection(1)
5933 .build()
5934 .unwrap();
5935 let strategy_id = StrategyId::from("FUNDING_ARBITRAGE-001");
5936
5937 node.add_strategy(TestStrategy::new(StrategyConfig {
5938 strategy_id: Some(strategy_id),
5939 oms_type: Some(OmsType::Hedging),
5940 ..Default::default()
5941 }))
5942 .unwrap();
5943
5944 let instrument = crypto_perpetual_ethusdt();
5945 let instrument_id = instrument.id();
5946 let client_id = ClientId::from("STUB");
5947
5948 node.kernel
5949 .cache
5950 .borrow_mut()
5951 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
5952 .unwrap();
5953 node.kernel
5954 .exec_engine
5955 .borrow_mut()
5956 .register_client(Box::new(StubExecutionClient::new(
5957 client_id,
5958 AccountId::from("TEST-ACCOUNT"),
5959 instrument_id.venue,
5960 OmsType::Netting,
5961 None,
5962 )))
5963 .unwrap();
5964
5965 let order = OrderTestBuilder::new(OrderType::Market)
5966 .trader_id(node.trader_id())
5967 .strategy_id(strategy_id)
5968 .instrument_id(instrument_id)
5969 .quantity(Quantity::from("1.000"))
5970 .build();
5971 let position_id = PositionId::new("CUSTOM-POSITION-001");
5972
5973 node.kernel
5974 .cache
5975 .borrow_mut()
5976 .add_order(order.clone(), Some(position_id), Some(client_id), true)
5977 .unwrap();
5978
5979 let submit_order = SubmitOrder::new(
5980 order.trader_id(),
5981 Some(client_id),
5982 strategy_id,
5983 instrument_id,
5984 order.client_order_id(),
5985 order.init_event().clone(),
5986 order.exec_algorithm_id(),
5987 Some(position_id),
5988 None,
5989 UUID4::new(),
5990 UnixNanos::default(),
5991 None,
5992 );
5993
5994 node.kernel
5995 .exec_engine
5996 .borrow()
5997 .execute(TradingCommand::SubmitOrder(submit_order));
5998
5999 let exec_engine = node.kernel.exec_engine.borrow();
6000 let cache = exec_engine.cache().borrow();
6001 let cached_order = cache
6002 .order(&order.client_order_id())
6003 .expect("Order should be cached");
6004
6005 assert_eq!(cached_order.status(), OrderStatus::Initialized);
6006 }
6007
6008 #[cfg(all(feature = "simulation", madsim))]
6009 async fn advance_clock(d: Duration) {
6010 madsim::time::advance(d);
6011 madsim::task::yield_now().await;
6012 }
6013
6014 #[cfg(not(all(feature = "simulation", madsim)))]
6015 async fn advance_clock(d: Duration) {
6016 tokio::time::advance(d).await;
6017 }
6018
6019 #[cfg_attr(
6020 not(all(feature = "simulation", madsim)),
6021 tokio::test(start_paused = true)
6022 )]
6023 #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
6024 async fn test_reconciliation_check_due_uses_monotonic_elapsed_time() {
6025 let last = dst::time::Instant::now();
6026 let interval = Duration::from_millis(100);
6027
6028 assert!(!reconciliation_check_due(last, last, Duration::ZERO));
6029 assert!(!reconciliation_check_due(last, last, interval));
6030
6031 advance_clock(Duration::from_millis(99)).await;
6032 let before_interval = dst::time::Instant::now();
6033 assert!(!reconciliation_check_due(before_interval, last, interval));
6034
6035 advance_clock(Duration::from_millis(1)).await;
6036 let at_interval = dst::time::Instant::now();
6037 assert!(reconciliation_check_due(at_interval, last, interval));
6038
6039 assert!(!reconciliation_check_due(last, at_interval, interval));
6040 }
6041
6042 #[cfg_attr(
6043 not(all(feature = "simulation", madsim)),
6044 tokio::test(start_paused = true)
6045 )]
6046 #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
6047 async fn test_run_reconciliation_checks_does_not_publish_open_order_queries() {
6048 let config = LiveNodeConfig {
6049 exec_engine: crate::config::LiveExecutionEngineConfig {
6050 reconciliation: true,
6051 open_check_interval_secs: Some(1.0),
6052 position_check_interval_secs: Some(1.0),
6053 max_single_order_queries_per_cycle: 5,
6054 ..Default::default()
6055 },
6056 ..Default::default()
6057 };
6058
6059 let mut node =
6060 LiveNode::build("ReconciliationFallbackNode".to_string(), Some(config)).unwrap();
6061 let client_id = ClientId::from("TEST-QUERY");
6062 let account_id = AccountId::from("TEST-QUERY-001");
6063
6064 let trading_commands = Rc::new(RefCell::new(Vec::new()));
6065 msgbus::register_trading_command_endpoint(
6066 MessagingSwitchboard::exec_engine_execute(),
6067 TypedIntoHandler::from({
6068 let trading_commands = trading_commands.clone();
6069 move |command: TradingCommand| {
6070 trading_commands.borrow_mut().push(command);
6071 }
6072 }),
6073 );
6074
6075 let venue_order_id = VenueOrderId::from("V-NODE-QUERY-001");
6076 let instrument = crypto_perpetual_ethusdt();
6077 let instrument_id = instrument.id();
6078 let client_order_id = ClientOrderId::from("O-NODE-QUERY-001");
6079
6080 node.kernel
6081 .cache
6082 .borrow_mut()
6083 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
6084 .unwrap();
6085 insert_accepted_limit_order_in_node(
6086 &node,
6087 account_id,
6088 client_id,
6089 instrument_id,
6090 client_order_id,
6091 venue_order_id,
6092 );
6093
6094 let last = dst::time::Instant::now();
6095 advance_clock(Duration::from_nanos(1)).await;
6096 let now = dst::time::Instant::now();
6097 let mut last_inflight_check = last;
6098 let mut last_open_check = last;
6099 let mut last_position_check = last;
6100 let mut open_order_report_task = None;
6101 let mut targeted_order_report_task = None;
6102 let mut position_report_task = None;
6103
6104 node.run_reconciliation_checks(
6105 now,
6106 ReconciliationCheckIntervals {
6107 inflight: Duration::ZERO,
6108 open: Duration::from_nanos(1),
6109 position: Duration::ZERO,
6110 },
6111 &mut ReconciliationCheckState {
6112 last_inflight_check: &mut last_inflight_check,
6113 last_open_check: &mut last_open_check,
6114 last_position_check: &mut last_position_check,
6115 open_order_report_task: &mut open_order_report_task,
6116 targeted_order_report_task: &mut targeted_order_report_task,
6117 position_report_task: &mut position_report_task,
6118 },
6119 );
6120
6121 let commands = trading_commands.borrow();
6122
6123 assert!(commands.is_empty());
6124 assert!(open_order_report_task.is_none());
6125 assert!(targeted_order_report_task.is_none());
6126 assert!(position_report_task.is_none());
6127
6128 ExecutionEngine::register_msgbus_handlers(&node.kernel.exec_engine);
6129 }
6130
6131 fn insert_accepted_limit_order_in_node(
6132 node: &LiveNode,
6133 account_id: AccountId,
6134 client_id: ClientId,
6135 instrument_id: InstrumentId,
6136 client_order_id: ClientOrderId,
6137 venue_order_id: VenueOrderId,
6138 ) {
6139 let order = OrderTestBuilder::new(OrderType::Limit)
6140 .client_order_id(client_order_id)
6141 .instrument_id(instrument_id)
6142 .quantity(Quantity::from("10.0"))
6143 .price(Price::from("100.0"))
6144 .build();
6145 let submitted = TestOrderEventStubs::submitted(&order, account_id);
6146 node.kernel
6147 .cache
6148 .borrow_mut()
6149 .add_order(order, None, Some(client_id), false)
6150 .unwrap();
6151 let order = node
6152 .kernel
6153 .cache
6154 .borrow_mut()
6155 .update_order(&submitted)
6156 .unwrap();
6157 let accepted = TestOrderEventStubs::accepted(&order, account_id, venue_order_id);
6158 node.kernel
6159 .cache
6160 .borrow_mut()
6161 .update_order(&accepted)
6162 .unwrap();
6163 }
6164
6165 fn recent_fill_test_fixture(name: &str) -> (LiveNode, OrderEventAny, InstrumentAny) {
6166 let config = LiveNodeConfig {
6167 exec_engine: crate::config::LiveExecutionEngineConfig {
6168 reconciliation: true,
6169 ..Default::default()
6170 },
6171 ..Default::default()
6172 };
6173
6174 let node = LiveNode::build(name.to_string(), Some(config)).unwrap();
6175 let account_id = AccountId::from("TEST-001");
6176 let client_id = ClientId::from("TEST-RECENT-FILL");
6177 let client_order_id = ClientOrderId::from("O-RECENT-FILL");
6178 let venue_order_id = VenueOrderId::from("V-RECENT-FILL");
6179 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
6180 node.kernel
6181 .cache
6182 .borrow_mut()
6183 .add_instrument(instrument.clone())
6184 .unwrap();
6185 insert_accepted_limit_order_in_node(
6186 &node,
6187 account_id,
6188 client_id,
6189 instrument.id(),
6190 client_order_id,
6191 venue_order_id,
6192 );
6193 let order = node
6194 .kernel
6195 .cache
6196 .borrow()
6197 .order_owned(&client_order_id)
6198 .unwrap();
6199 let fill = TestOrderEventStubs::filled(
6200 &order,
6201 &instrument,
6202 Some(TradeId::from("T-RECENT-FILL")),
6203 None,
6204 Some(Price::from("100.0")),
6205 Some(Quantity::from("1.0")),
6206 Some(LiquiditySide::Taker),
6207 None,
6208 None,
6209 Some(account_id),
6210 );
6211
6212 (node, fill, instrument)
6213 }
6214
6215 fn apply_inferred_position_fill(node: &mut LiveNode, fill_report: &FillReport) {
6216 let client_order_id = fill_report.client_order_id.unwrap();
6217 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
6218 let order = node
6219 .kernel
6220 .cache
6221 .borrow()
6222 .order_owned(&client_order_id)
6223 .unwrap();
6224 let order_report = OrderStatusReport::new(
6225 fill_report.account_id,
6226 fill_report.instrument_id,
6227 Some(client_order_id),
6228 fill_report.venue_order_id,
6229 OrderSide::Buy.into(),
6230 OrderType::Limit,
6231 TimeInForce::Gtc,
6232 OrderStatus::PartiallyFilled,
6233 Quantity::from("10.0"),
6234 Quantity::from("2.0"),
6235 UnixNanos::from(1_000),
6236 UnixNanos::from(1_000),
6237 UnixNanos::from(1_000),
6238 None,
6239 )
6240 .with_avg_px(dec!(100.0));
6241 let inferred = create_inferred_fill_for_qty(
6242 &order,
6243 &order_report,
6244 &fill_report.account_id,
6245 &instrument,
6246 Quantity::from("1.0"),
6247 UnixNanos::from(1_000),
6248 None,
6249 )
6250 .unwrap();
6251
6252 node.process_reconciliation_events(&[inferred]);
6253 }
6254
6255 fn position_fill_test_fixture(
6256 name: &str,
6257 authoritative_qty: Quantity,
6258 ) -> (LiveNode, PositionStatusReport, FillReport) {
6259 let config = LiveNodeConfig {
6260 exec_engine: crate::config::LiveExecutionEngineConfig {
6261 reconciliation: true,
6262 position_check_threshold_ms: 0,
6263 ..Default::default()
6264 },
6265 ..Default::default()
6266 };
6267
6268 let mut node = LiveNode::build(name.to_string(), Some(config)).unwrap();
6269 let account_id = AccountId::from("TEST-001");
6270 let client_id = ClientId::from("POSITION-FILLS");
6271 let client_order_id = ClientOrderId::from("O-POSITION-FILLS");
6272 let venue_order_id = VenueOrderId::from("V-POSITION-FILLS");
6273 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
6274 let account = AccountAny::Margin(MarginAccount::new(
6275 AccountState::new(
6276 account_id,
6277 AccountType::Margin,
6278 vec![AccountBalance::new(
6279 Money::from("1000000 USDT"),
6280 Money::from("0 USDT"),
6281 Money::from("1000000 USDT"),
6282 )],
6283 Vec::new(),
6284 true,
6285 UUID4::new(),
6286 UnixNanos::default(),
6287 UnixNanos::default(),
6288 Some(Currency::USDT()),
6289 ),
6290 true,
6291 ));
6292 node.kernel.cache.borrow_mut().add_account(account).unwrap();
6293 node.kernel
6294 .cache
6295 .borrow_mut()
6296 .add_instrument(instrument.clone())
6297 .unwrap();
6298 insert_accepted_limit_order_in_node(
6299 &node,
6300 account_id,
6301 client_id,
6302 instrument.id(),
6303 client_order_id,
6304 venue_order_id,
6305 );
6306 let order = node
6307 .kernel
6308 .cache
6309 .borrow()
6310 .order_owned(&client_order_id)
6311 .unwrap();
6312 let mut initial_fill = TestOrderEventStubs::filled(
6313 &order,
6314 &instrument,
6315 Some(TradeId::from("T-POSITION-INITIAL")),
6316 None,
6317 Some(Price::from("100.0")),
6318 Some(Quantity::from("1.0")),
6319 Some(LiquiditySide::Taker),
6320 None,
6321 None,
6322 Some(account_id),
6323 );
6324
6325 let OrderEventAny::Filled(fill) = &mut initial_fill else {
6326 unreachable!();
6327 };
6328
6329 fill.commission = Some(Money::zero(instrument.quote_currency()));
6330 node.process_reconciliation_events(&[initial_fill]);
6331
6332 let ts_event = UnixNanos::from(1_000);
6333
6334 let fill_report = FillReport::new(
6335 account_id,
6336 instrument.id(),
6337 venue_order_id,
6338 TradeId::from("T-POSITION-AUTHORITATIVE"),
6339 OrderSide::Buy,
6340 authoritative_qty,
6341 Price::from("100.0"),
6342 Money::zero(instrument.quote_currency()),
6343 LiquiditySide::Taker,
6344 Some(client_order_id),
6345 None,
6346 ts_event,
6347 ts_event,
6348 None,
6349 );
6350
6351 let venue_report = PositionStatusReport::new(
6352 account_id,
6353 instrument.id(),
6354 PositionSide::Long,
6355 Quantity::from("2.0"),
6356 ts_event,
6357 ts_event,
6358 None,
6359 None,
6360 Some(dec!(100.0)),
6361 );
6362
6363 (node, venue_report, fill_report)
6364 }
6365
6366 fn position_report_result(
6367 node: &LiveNode,
6368 report: PositionStatusReport,
6369 ) -> PositionReportResult {
6370 let client_id = ClientId::from("POSITION-FILLS");
6371 let key = (report.instrument_id, report.account_id);
6372 let mut check = node
6373 .exec_manager
6374 .prepare_position_report_check(UUID4::new(), &[]);
6375 check.client_coverage.insert(
6376 key,
6377 ReportClientCoverage::Resolved(IndexSet::from([client_id])),
6378 );
6379
6380 PositionReportResult {
6381 check,
6382 reports: vec![report],
6383 queried_clients: IndexSet::from([client_id]),
6384 failed_clients: IndexSet::new(),
6385 }
6386 }
6387
6388 fn fill_report_event(fill: &OrderFilled) -> ExecutionEvent {
6389 ExecutionEvent::Report(ExecutionReport::Fill(Box::new(FillReport::new(
6390 fill.account_id,
6391 fill.instrument_id,
6392 fill.venue_order_id,
6393 fill.trade_id,
6394 fill.order_side,
6395 fill.last_qty,
6396 fill.last_px,
6397 fill.commission
6398 .unwrap_or_else(|| Money::zero(fill.currency)),
6399 fill.liquidity_side,
6400 Some(fill.client_order_id),
6401 fill.position_id,
6402 fill.ts_event,
6403 fill.ts_init,
6404 None,
6405 ))))
6406 }
6407
6408 fn is_recent_fill(node: &LiveNode, fill: &OrderFilled) -> bool {
6409 node.exec_manager.is_fill_recently_processed(
6410 fill.account_id,
6411 fill.instrument_id,
6412 fill.trade_id,
6413 )
6414 }
6415
6416 #[rstest]
6417 #[case(0, NodeState::Idle)]
6418 #[case(1, NodeState::Starting)]
6419 #[case(2, NodeState::Running)]
6420 #[case(3, NodeState::ShuttingDown)]
6421 #[case(4, NodeState::Stopped)]
6422 fn test_node_state_from_u8_valid(#[case] value: u8, #[case] expected: NodeState) {
6423 assert_eq!(NodeState::from_u8(value), expected);
6424 }
6425
6426 #[rstest]
6427 #[case(5)]
6428 #[case(255)]
6429 #[should_panic(expected = "Invalid NodeState value")]
6430 fn test_node_state_from_u8_invalid_panics(#[case] value: u8) {
6431 let _ = NodeState::from_u8(value);
6432 }
6433
6434 #[rstest]
6435 fn test_node_state_roundtrip() {
6436 for state in [
6437 NodeState::Idle,
6438 NodeState::Starting,
6439 NodeState::Running,
6440 NodeState::ShuttingDown,
6441 NodeState::Stopped,
6442 ] {
6443 assert_eq!(NodeState::from_u8(state.as_u8()), state);
6444 }
6445 }
6446
6447 #[rstest]
6448 fn test_node_state_is_running_only_for_running() {
6449 assert!(!NodeState::Idle.is_running());
6450 assert!(!NodeState::Starting.is_running());
6451 assert!(NodeState::Running.is_running());
6452 assert!(!NodeState::ShuttingDown.is_running());
6453 assert!(!NodeState::Stopped.is_running());
6454 }
6455
6456 #[rstest]
6457 #[tokio::test]
6458 async fn test_await_engines_connected_returns_stop_requested() {
6459 let node = LiveNode::build("TestNode".to_string(), None).unwrap();
6460 let handle = node.handle();
6461
6462 handle.stop();
6463
6464 let deadline = dst::time::Instant::now() + Duration::from_secs(1);
6465 let status = node.await_engines_connected(deadline).await;
6466
6467 assert_eq!(status, EngineConnectionStatus::StopRequested);
6468 assert!(handle.should_stop());
6469 }
6470
6471 #[rstest]
6472 #[tokio::test]
6473 async fn test_await_engines_connected_returns_shutdown_requested() {
6474 let node = LiveNode::build("TestNode".to_string(), None).unwrap();
6475
6476 node.kernel().shutdown_flag().set(true);
6477
6478 let deadline = dst::time::Instant::now() + Duration::from_secs(1);
6479 let status = node.await_engines_connected(deadline).await;
6480
6481 assert_eq!(status, EngineConnectionStatus::ShutdownRequested);
6482 }
6483
6484 #[rstest]
6485 #[tokio::test]
6486 async fn test_start_stop_request_aborts_startup_without_running() {
6487 let config = LiveNodeConfig {
6488 exec_engine: crate::config::LiveExecutionEngineConfig {
6489 reconciliation: false,
6490 ..Default::default()
6491 },
6492 timeout_disconnection: Duration::from_millis(50),
6493 ..Default::default()
6494 };
6495
6496 let mut node = LiveNode::build("TestNode".to_string(), Some(config)).unwrap();
6497 let handle = node.handle();
6498
6499 handle.stop();
6500 node.start().await.unwrap();
6501
6502 assert_eq!(handle.state(), NodeState::Stopped);
6503 assert!(handle.should_stop());
6504 assert!(!handle.is_running());
6505 }
6506
6507 #[rstest]
6508 #[tokio::test(start_paused = true)]
6509 async fn test_stop_processes_residual_exec_event_during_grace_period() {
6510 let config = LiveNodeConfig {
6511 exec_engine: crate::config::LiveExecutionEngineConfig {
6512 reconciliation: false,
6513 ..Default::default()
6514 },
6515 timeout_connection: Duration::ZERO,
6516 timeout_reconciliation: Duration::ZERO,
6517 timeout_portfolio: Duration::ZERO,
6518 timeout_disconnection: Duration::ZERO,
6519 delay_post_stop: Duration::from_millis(20),
6520 timeout_shutdown: Duration::ZERO,
6521 ..Default::default()
6522 };
6523
6524 let mut node = LiveNode::build("TestNode".to_string(), Some(config)).unwrap();
6525 let order = OrderTestBuilder::new(OrderType::Market)
6526 .instrument_id(InstrumentId::from("EUR/USD.SIM"))
6527 .quantity(Quantity::from("2"))
6528 .build();
6529 let client_order_id = order.client_order_id();
6530 let submitted = TestOrderEventStubs::submitted(&order, AccountId::from("POLL-STOP-001"));
6531
6532 node.kernel
6533 .cache()
6534 .borrow_mut()
6535 .add_order(order, None, None, false)
6536 .unwrap();
6537
6538 node.start().await.unwrap();
6539 let exec_event_sender = get_exec_event_sender();
6540
6541 let send_residual = tokio::spawn(async move {
6542 tokio::time::sleep(Duration::from_millis(1)).await;
6543 exec_event_sender
6544 .send(ExecutionEvent::Order(submitted))
6545 .unwrap();
6546 });
6547
6548 node.stop().await.unwrap();
6549 send_residual.await.unwrap();
6550
6551 assert_eq!(
6552 node.kernel
6553 .cache()
6554 .borrow()
6555 .order(&client_order_id)
6556 .unwrap()
6557 .status(),
6558 OrderStatus::Submitted
6559 );
6560
6561 node.dispose();
6562 }
6563
6564 #[rstest]
6565 #[tokio::test]
6566 async fn test_live_state_persistence_loads_before_start_and_saves_after_stop() {
6567 let actor_id = ActorId::from("LIVE-STATE-ACTOR");
6568 let strategy_id = StrategyId::from("LIVE-STATE-STRATEGY-001");
6569 let actor_load = IndexMap::from([("actor-load".to_string(), b"actor-loaded".to_vec())]);
6570 let strategy_load =
6571 IndexMap::from([("strategy-load".to_string(), b"strategy-loaded".to_vec())]);
6572 let actor_save = IndexMap::from([("actor-save".to_string(), b"actor-saved".to_vec())]);
6573 let strategy_save =
6574 IndexMap::from([("strategy-save".to_string(), b"strategy-saved".to_vec())]);
6575 let (database, control) = TestCacheDatabaseControl::create();
6576 control.set_actor_state(actor_id, &actor_load);
6577 control.set_strategy_state(strategy_id, &strategy_load);
6578
6579 let config = LiveNodeConfig {
6580 load_state: true,
6581 save_state: true,
6582 exec_engine: crate::config::LiveExecutionEngineConfig {
6583 reconciliation: false,
6584 ..Default::default()
6585 },
6586 timeout_connection: Duration::ZERO,
6587 timeout_reconciliation: Duration::ZERO,
6588 timeout_portfolio: Duration::ZERO,
6589 timeout_disconnection: Duration::ZERO,
6590 delay_post_stop: Duration::ZERO,
6591 timeout_shutdown: Duration::ZERO,
6592 ..Default::default()
6593 };
6594
6595 let mut node = LiveNode::build("StatePersistenceNode".to_string(), Some(config)).unwrap();
6596 node.set_cache_database(Box::new(database)).unwrap();
6597 node.add_actor(StateActor::new(
6598 actor_id,
6599 control.clone(),
6600 actor_save.clone(),
6601 ))
6602 .unwrap();
6603 node.add_strategy(StateStrategy::new(
6604 strategy_id,
6605 control.clone(),
6606 strategy_save.clone(),
6607 ))
6608 .unwrap();
6609
6610 node.start().await.unwrap();
6611 node.stop().await.unwrap();
6612 node.dispose();
6613
6614 assert_eq!(
6615 control.events(),
6616 vec![
6617 "actor.load:LIVE-STATE-ACTOR",
6618 "actor.on_load",
6619 "strategy.load:LIVE-STATE-STRATEGY-001",
6620 "strategy.on_load",
6621 "actor.on_start",
6622 "strategy.on_start",
6623 "actor.on_stop",
6624 "strategy.on_stop",
6625 "actor.on_save",
6626 "actor.update:LIVE-STATE-ACTOR",
6627 "strategy.on_save",
6628 "strategy.update:LIVE-STATE-STRATEGY-001",
6629 "database.close",
6630 ]
6631 );
6632 assert_eq!(control.actor_state(&actor_id), Some(actor_save));
6633 assert_eq!(control.strategy_state(&strategy_id), Some(strategy_save));
6634 assert_eq!(node.state(), NodeState::Stopped);
6635 }
6636
6637 #[rstest]
6638 #[tokio::test]
6639 async fn test_live_state_persistence_reports_callback_errors_after_shutdown() {
6640 let actor_id = ActorId::from("LIVE-FAIL-SAVE-ACTOR");
6641 let strategy_id = StrategyId::from("LIVE-FAIL-SAVE-STRATEGY-001");
6642 let (database, control) = TestCacheDatabaseControl::create();
6643
6644 let config = LiveNodeConfig {
6645 save_state: true,
6646 exec_engine: crate::config::LiveExecutionEngineConfig {
6647 reconciliation: false,
6648 ..Default::default()
6649 },
6650 timeout_connection: Duration::ZERO,
6651 timeout_reconciliation: Duration::ZERO,
6652 timeout_portfolio: Duration::ZERO,
6653 timeout_disconnection: Duration::ZERO,
6654 delay_post_stop: Duration::ZERO,
6655 timeout_shutdown: Duration::ZERO,
6656 ..Default::default()
6657 };
6658
6659 let mut node =
6660 LiveNode::build("StatePersistenceErrorNode".to_string(), Some(config)).unwrap();
6661 node.set_cache_database(Box::new(database)).unwrap();
6662 node.add_actor(
6663 StateActor::new(actor_id, control.clone(), IndexMap::new()).with_fail_save(),
6664 )
6665 .unwrap();
6666 node.add_strategy(
6667 StateStrategy::new(strategy_id, control.clone(), IndexMap::new()).with_fail_save(),
6668 )
6669 .unwrap();
6670
6671 node.start().await.unwrap();
6672 let error = node.stop().await.unwrap_err();
6673 node.dispose();
6674
6675 assert_eq!(
6676 error.to_string(),
6677 "failed while finalizing kernel shutdown: Failed to save component state: actor \
6678 LIVE-FAIL-SAVE-ACTOR callback: test actor on_save failure; strategy \
6679 LIVE-FAIL-SAVE-STRATEGY-001 callback: test strategy on_save failure"
6680 );
6681 assert_eq!(
6682 control.events(),
6683 vec![
6684 "actor.on_start",
6685 "strategy.on_start",
6686 "actor.on_stop",
6687 "strategy.on_stop",
6688 "actor.on_save",
6689 "strategy.on_save",
6690 "database.close",
6691 ]
6692 );
6693 assert_eq!(node.state(), NodeState::Stopped);
6694 }
6695
6696 #[rstest]
6697 #[tokio::test]
6698 async fn test_stop_drains_queued_exec_event_after_zero_grace() {
6699 let config = LiveNodeConfig {
6700 exec_engine: crate::config::LiveExecutionEngineConfig {
6701 reconciliation: false,
6702 ..Default::default()
6703 },
6704 timeout_connection: Duration::ZERO,
6705 timeout_reconciliation: Duration::ZERO,
6706 timeout_portfolio: Duration::ZERO,
6707 timeout_disconnection: Duration::ZERO,
6708 delay_post_stop: Duration::ZERO,
6709 timeout_shutdown: Duration::ZERO,
6710 ..Default::default()
6711 };
6712
6713 let mut node = LiveNode::build("TestNode".to_string(), Some(config)).unwrap();
6714 let order = OrderTestBuilder::new(OrderType::Market)
6715 .instrument_id(InstrumentId::from("GBP/USD.SIM"))
6716 .quantity(Quantity::from("3"))
6717 .build();
6718 let client_order_id = order.client_order_id();
6719 let submitted = TestOrderEventStubs::submitted(&order, AccountId::from("POLL-DRAIN-001"));
6720
6721 node.kernel
6722 .cache()
6723 .borrow_mut()
6724 .add_order(order, None, None, false)
6725 .unwrap();
6726
6727 node.start().await.unwrap();
6728 get_exec_event_sender()
6729 .send(ExecutionEvent::Order(submitted))
6730 .unwrap();
6731
6732 node.stop().await.unwrap();
6733
6734 assert_eq!(
6735 node.kernel
6736 .cache()
6737 .borrow()
6738 .order(&client_order_id)
6739 .unwrap()
6740 .status(),
6741 OrderStatus::Submitted
6742 );
6743
6744 node.dispose();
6745 }
6746
6747 #[rstest]
6748 #[tokio::test]
6749 async fn test_start_event_store_replay_skips_live_connections() {
6750 let mut node = live_node_with_replay_store(false);
6751 let handle = node.handle();
6752
6753 node.start().await.unwrap();
6754
6755 assert_eq!(handle.state(), NodeState::Running);
6756 assert!(handle.is_running());
6757 assert!(node.kernel.is_event_store_replay());
6758 assert!(node.runner.is_some());
6759 }
6760
6761 #[rstest]
6762 #[tokio::test]
6763 async fn test_start_event_store_replay_preserves_stop_request() {
6764 let mut node = live_node_with_replay_store(false);
6765 let handle = node.handle();
6766 handle.stop();
6767
6768 node.start().await.unwrap();
6769
6770 assert_eq!(handle.state(), NodeState::Stopped);
6771 assert!(handle.should_stop());
6772 assert!(node.kernel.is_event_store_replay());
6773 assert!(node.runner.is_some());
6774 }
6775
6776 #[rstest]
6777 #[tokio::test]
6778 async fn test_start_event_store_replay_config_failure_aborts_startup() {
6779 let mut node = live_node_with_replay_store(true);
6780 let handle = node.handle();
6781
6782 node.start().await.unwrap();
6783
6784 assert_eq!(handle.state(), NodeState::Stopped);
6785 assert!(!handle.is_running());
6786 assert!(node.kernel.is_event_store_replay_configured());
6787 assert!(!node.kernel.is_event_store_replay());
6788 assert!(node.runner.is_some());
6789 }
6790
6791 #[rstest]
6792 #[tokio::test]
6793 async fn test_run_event_store_replay_consumes_runner_and_stops_before_connections() {
6794 let mut node = live_node_with_replay_store(false);
6795 let handle = node.handle();
6796
6797 node.run().await.unwrap();
6798
6799 assert_eq!(handle.state(), NodeState::Running);
6800 assert!(handle.is_running());
6801 assert!(node.kernel.is_event_store_replay());
6802 assert!(node.runner.is_none());
6803 }
6804
6805 #[rstest]
6806 #[tokio::test]
6807 async fn test_run_event_store_replay_preserves_stop_request() {
6808 let mut node = live_node_with_replay_store(false);
6809 let handle = node.handle();
6810 handle.stop();
6811
6812 node.run().await.unwrap();
6813
6814 assert_eq!(handle.state(), NodeState::Stopped);
6815 assert!(handle.should_stop());
6816 assert!(node.kernel.is_event_store_replay());
6817 assert!(node.runner.is_none());
6818 }
6819
6820 #[rstest]
6821 #[tokio::test]
6822 async fn test_run_event_store_replay_config_failure_aborts_startup() {
6823 let mut node = live_node_with_replay_store(true);
6824 let handle = node.handle();
6825
6826 node.run().await.unwrap();
6827
6828 assert_eq!(handle.state(), NodeState::Stopped);
6829 assert!(!handle.is_running());
6830 assert!(node.kernel.is_event_store_replay_configured());
6831 assert!(!node.kernel.is_event_store_replay());
6832 assert!(node.runner.is_none());
6833 }
6834
6835 #[rstest]
6836 fn test_build_rejects_event_store_config_without_factory() {
6837 let config = LiveNodeConfig {
6838 event_store: Some(EventStoreConfig::default()),
6839 exec_engine: crate::config::LiveExecutionEngineConfig {
6840 reconciliation: false,
6841 ..Default::default()
6842 },
6843 ..Default::default()
6844 };
6845
6846 let err = LiveNodeBuilder::from_config(config)
6847 .expect("builder")
6848 .build()
6849 .expect_err("should reject event_store config without factory");
6850
6851 assert!(
6852 err.to_string().contains("with_event_store"),
6853 "error message should mention with_event_store, was: {err}"
6854 );
6855 }
6856
6857 #[rstest]
6858 fn test_direct_build_rejects_event_store_config() {
6859 let config = LiveNodeConfig {
6860 event_store: Some(EventStoreConfig::default()),
6861 exec_engine: crate::config::LiveExecutionEngineConfig {
6862 reconciliation: false,
6863 ..Default::default()
6864 },
6865 ..Default::default()
6866 };
6867
6868 let err = LiveNode::build("TestNode".to_string(), Some(config))
6869 .expect_err("LiveNode::build should reject event_store config");
6870
6871 assert!(
6872 err.to_string().contains("with_event_store"),
6873 "error message should mention with_event_store, was: {err}"
6874 );
6875 }
6876
6877 #[rstest]
6878 fn test_dispose_before_start_is_idempotent() {
6879 let mut node = LiveNode::build("TestNode".to_string(), None).unwrap();
6880 node.add_strategy(TestStrategy::new(StrategyConfig {
6881 strategy_id: Some(StrategyId::from("DISPOSAL-001")),
6882 ..Default::default()
6883 }))
6884 .unwrap();
6885
6886 node.dispose();
6887 node.dispose();
6888
6889 assert!(node.kernel.trader().borrow().is_disposed());
6890 assert_eq!(node.kernel.trader().borrow().component_count(), 0);
6891 assert_eq!(node.state(), NodeState::Stopped);
6892 }
6893
6894 #[rstest]
6895 fn test_dispose_releases_retained_callback_roots(
6896 #[values(false, true)] fatal: bool,
6897 #[values(false, true)] external: bool,
6898 ) {
6899 actor::clear_callbacks().unwrap();
6900 let directory =
6901 std::env::temp_dir().join(format!("nautilus-callback-disposal-{}", UUID4::new()));
6902
6903 let config = LiveNodeConfig {
6904 logging: LoggerConfig {
6905 fileout_level: LevelFilter::Info,
6906 file_config: Some(FileWriterConfig {
6907 directory: Some(directory.to_str().unwrap().to_string()),
6908 file_name: Some("disposal".to_string()),
6909 ..Default::default()
6910 }),
6911 ..Default::default()
6912 },
6913 ..Default::default()
6914 };
6915
6916 let mut node = LiveNode::build("CallbackDisposalNode".to_string(), Some(config)).unwrap();
6917 let received = Rc::new(RefCell::new(Vec::new()));
6918 let observed = received.clone();
6919
6920 let retained = DispatchMessage::from(()).dispatch(|()| {
6921 nautilus_common::runner::get_time_event_sender().send(TimeEventMessage::new(
6922 TimeEvent::new("disposal".into(), UUID4::new(), 17.into(), 23.into()),
6923 TimeEventCallback::RustLocal(Rc::new(move |_| {
6924 observed.borrow_mut().push("delivered");
6925 })),
6926 ));
6927
6928 external.then(|| DispatchMessage::new((), std::thread::current().id()))
6929 });
6930
6931 if let Some(retained) = &retained {
6932 assert!(retained.is_rooted());
6933 }
6934
6935 assert_eq!(actor::clear_callbacks(), Err(CallbackDispatchError::Active));
6936
6937 if fatal {
6938 crate::dispatch::tests::latch_callback_failure();
6939 }
6940
6941 node.dispose();
6942
6943 assert!(node.runner.is_none());
6944 assert!(received.borrow().is_empty());
6945 assert_eq!(Rc::strong_count(&received), 1);
6946 assert_eq!(
6947 actor::callback_failure(),
6948 (fatal && external).then_some(CallbackDispatchError::DeliveryUnwound)
6949 );
6950 assert_eq!(
6951 actor::clear_callbacks(),
6952 if external {
6953 Err(CallbackDispatchError::Active)
6954 } else {
6955 Ok(())
6956 }
6957 );
6958
6959 logging_sync_to_disk().unwrap();
6960 let output = std::fs::read_to_string(directory.join("disposal.log")).unwrap();
6961 drop(retained);
6962 node.dispose();
6963
6964 assert_eq!(actor::callback_failure(), None);
6965 assert_eq!(actor::clear_callbacks(), Ok(()));
6966 drop(node);
6967 std::fs::remove_dir_all(directory).unwrap();
6968
6969 let errors: Vec<_> = output
6970 .lines()
6971 .filter(|line| line.contains("[ERROR]"))
6972 .filter_map(|line| {
6973 line.split_once(".nautilus_live::node: ")
6974 .map(|(_, text)| text)
6975 })
6976 .collect();
6977
6978 let mut expected = Vec::new();
6979
6980 if fatal {
6981 expected.push(
6982 "Callback dispatch failed before disposal cleanup: Callback delivery unwound",
6983 );
6984 }
6985
6986 if external {
6987 expected.push(
6988 "Failed to clear callback dispatch during disposal: Callback work or access is still active",
6989 );
6990 }
6991
6992 assert_eq!(errors, expected);
6993 }
6994
6995 #[tokio::test]
6996 async fn test_dispose_releases_stop_generated_callback_roots() {
6997 actor::clear_callbacks().unwrap();
6998
6999 let config = LiveNodeConfig {
7000 exec_engine: crate::config::LiveExecutionEngineConfig {
7001 reconciliation: false,
7002 ..Default::default()
7003 },
7004 timeout_connection: Duration::ZERO,
7005 timeout_reconciliation: Duration::ZERO,
7006 timeout_portfolio: Duration::ZERO,
7007 timeout_shutdown: Duration::ZERO,
7008 ..Default::default()
7009 };
7010
7011 let mut node =
7012 LiveNode::build("StopCallbackDisposalNode".to_string(), Some(config)).unwrap();
7013 let received = Rc::new(RefCell::new(Vec::new()));
7014 node.add_actor(StopCallbackActor {
7015 core: DataActorCore::new(DataActorConfig {
7016 actor_id: Some(ActorId::from("STOP-CALLBACK")),
7017 ..Default::default()
7018 }),
7019 received: received.clone(),
7020 })
7021 .unwrap();
7022
7023 node.start().await.unwrap();
7024
7025 assert!(node.kernel.trader().borrow().is_running());
7026 assert_eq!(actor::clear_callbacks(), Ok(()));
7027
7028 node.dispose();
7029
7030 assert_eq!(*received.borrow(), ["stop", "queued"]);
7031 assert_eq!(Rc::strong_count(&received), 1);
7032 assert!(node.runner.is_none());
7033 assert!(node.kernel.trader().borrow().is_disposed());
7034 assert_eq!(node.state(), NodeState::Stopped);
7035 assert_eq!(actor::clear_callbacks(), Ok(()));
7036 }
7037
7038 #[derive(Debug)]
7039 struct StopCallbackActor {
7040 core: DataActorCore,
7041 received: Rc<RefCell<Vec<&'static str>>>,
7042 }
7043
7044 nautilus_actor!(StopCallbackActor);
7045
7046 impl DataActor for StopCallbackActor {
7047 fn on_stop(&mut self) -> anyhow::Result<()> {
7048 self.received.borrow_mut().push("stop");
7049 let received = self.received.clone();
7050 DispatchMessage::from(()).dispatch(|()| {
7051 nautilus_common::runner::get_time_event_sender().send(TimeEventMessage::new(
7052 TimeEvent::new("stop-disposal".into(), UUID4::new(), 31.into(), 37.into()),
7053 TimeEventCallback::RustLocal(Rc::new(move |_| {
7054 received.borrow_mut().push("delivered");
7055 })),
7056 ));
7057 });
7058
7059 let clear_result = actor::clear_callbacks();
7060 anyhow::ensure!(
7061 clear_result == Err(CallbackDispatchError::Active),
7062 "Expected active callback roots during stop, received {clear_result:?}"
7063 );
7064 self.received.borrow_mut().push("queued");
7065 Ok(())
7066 }
7067 }
7068
7069 #[rstest]
7070 fn test_handle_initial_state() {
7071 let handle = LiveNodeHandle::new();
7072
7073 assert_eq!(handle.state(), NodeState::Idle);
7074 assert!(!handle.should_stop());
7075 assert!(!handle.is_running());
7076 }
7077
7078 #[rstest]
7079 fn test_handle_initial_metrics_snapshot_is_zero() {
7080 let handle = LiveNodeHandle::new();
7081
7082 assert_eq!(handle.metrics_snapshot(), RunnerMetricsSnapshot::default());
7083 }
7084
7085 #[rstest]
7086 fn test_record_runner_dispatch_updates_selected_channel() {
7087 let metrics = RunnerMetrics::default();
7088 let dispatch_start = dst::time::Instant::now();
7089 let metrics_start = dispatch_start
7090 .checked_sub(Duration::from_micros(1))
7091 .expect("test instant should support a one-microsecond lookback");
7092
7093 record_runner_dispatch(
7094 &metrics,
7095 SystemChannel::DataCommands,
7096 dispatch_start,
7097 metrics_start,
7098 );
7099 let snapshot = metrics.snapshot();
7100
7101 assert_eq!(snapshot.time_events.dispatched, 0);
7102 assert_eq!(snapshot.exec_events.dispatched, 0);
7103 assert_eq!(snapshot.exec_commands.dispatched, 0);
7104 assert_eq!(snapshot.data_events.dispatched, 0);
7105 assert_eq!(snapshot.data_commands.dispatched, 1);
7106 assert_eq!(
7107 snapshot.data_commands.last_dispatch_at_ns,
7108 snapshot.elapsed_ns
7109 );
7110 assert_eq!(
7111 snapshot.data_commands.dispatch_busy_ns,
7112 snapshot.dispatch_busy_ns
7113 );
7114 assert!(snapshot.dispatch_busy_ns < snapshot.elapsed_ns);
7115 }
7116
7117 #[rstest]
7118 fn test_handle_stop_sets_flag() {
7119 let handle = LiveNodeHandle::new();
7120
7121 handle.stop();
7122
7123 assert!(handle.should_stop());
7124 }
7125
7126 #[rstest]
7127 fn test_handle_stop_blocks_running_transition() {
7128 let handle = LiveNodeHandle::new();
7129 handle.set_starting();
7130 handle.stop();
7131
7132 let transition = handle.try_set_running();
7133
7134 assert_eq!(transition, RunningTransition::StopRequested);
7135 assert_eq!(handle.state(), NodeState::Starting);
7136 assert!(handle.should_stop());
7137 assert!(!handle.is_running());
7138 }
7139
7140 #[rstest]
7141 fn test_handle_stop_after_running_transition_remains_pending() {
7142 let handle = LiveNodeHandle::new();
7143 handle.set_starting();
7144
7145 let transition = handle.try_set_running();
7146 handle.stop();
7147
7148 assert_eq!(transition, RunningTransition::Entered);
7149 assert_eq!(handle.state(), NodeState::Running);
7150 assert!(handle.should_stop());
7151 assert!(handle.is_running());
7152 }
7153
7154 #[rstest]
7155 fn test_handle_node_state_transitions() {
7156 let handle = LiveNodeHandle::new();
7157 assert_eq!(handle.state(), NodeState::Idle);
7158
7159 handle.set_starting();
7160 assert_eq!(handle.state(), NodeState::Starting);
7161 assert!(!handle.is_running());
7162
7163 assert_eq!(handle.try_set_running(), RunningTransition::Entered);
7164 assert_eq!(handle.state(), NodeState::Running);
7165 assert!(handle.is_running());
7166
7167 handle.set_shutting_down();
7168 assert_eq!(handle.state(), NodeState::ShuttingDown);
7169 assert!(!handle.is_running());
7170
7171 handle.set_stopped();
7172 assert_eq!(handle.state(), NodeState::Stopped);
7173 assert!(!handle.is_running());
7174 }
7175
7176 #[rstest]
7177 fn test_handle_clone_shares_state_bidirectionally() {
7178 let handle1 = LiveNodeHandle::new();
7179 let handle2 = handle1.clone();
7180
7181 handle1.set_starting();
7182 let transition = handle2.try_set_running();
7183 handle1.stop();
7184
7185 assert_eq!(transition, RunningTransition::Entered);
7186 assert_eq!(handle1.state(), NodeState::Running);
7187 assert!(handle2.should_stop());
7188 }
7189
7190 #[rstest]
7191 fn test_handle_stop_flag_survives_non_running_state_changes() {
7192 let handle = LiveNodeHandle::new();
7193
7194 handle.set_starting();
7195 handle.stop();
7196 handle.set_shutting_down();
7197 handle.set_stopped();
7198
7199 assert_eq!(handle.state(), NodeState::Stopped);
7200 assert!(handle.should_stop());
7201 }
7202
7203 #[rstest]
7204 fn test_builder_creation() {
7205 let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox);
7206
7207 assert!(result.is_ok());
7208 }
7209
7210 #[rstest]
7211 fn test_stream_processor_receives_unregistered_typed_payload() {
7212 let mut node = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
7213 .unwrap()
7214 .build()
7215 .unwrap();
7216 let command = SubscribeCommand::Quotes(SubscribeQuotes::new(
7217 InstrumentId::from("AUD/USD.SIM"),
7218 Some(ClientId::from("EXTERNAL")),
7219 Some(Venue::from("SIM")),
7220 UUID4::from("00000000-0000-4000-8000-000000000001"),
7221 UnixNanos::from(1),
7222 None,
7223 None,
7224 ));
7225 let expected = serde_json::to_value(&command).unwrap();
7226 let received = Rc::new(RefCell::new(Vec::new()));
7227 let steps = Rc::new(RefCell::new(Vec::new()));
7228 let received_processor = received.clone();
7229 let first_steps = steps.clone();
7230 node.add_stream_processor(move |message| {
7231 let command = message
7232 .downcast_ref::<SubscribeCommand>()
7233 .expect("processor must receive the decoded concrete command");
7234 received_processor
7235 .borrow_mut()
7236 .push(serde_json::to_value(command).unwrap());
7237 first_steps.borrow_mut().push(1);
7238 });
7239
7240 let second_steps = steps.clone();
7241 node.add_stream_processor(move |_| second_steps.borrow_mut().push(2));
7242 let republished = Rc::new(RefCell::new(Vec::new()));
7243 let republished_handler = republished.clone();
7244 let subscriber_steps = steps.clone();
7245 msgbus::subscribe_any(
7246 "external.test".into(),
7247 ShareableMessageHandler::from_typed(move |command: &SubscribeCommand| {
7248 republished_handler
7249 .borrow_mut()
7250 .push(serde_json::to_value(command).unwrap());
7251 subscriber_steps.borrow_mut().push(3);
7252 }),
7253 None,
7254 );
7255
7256 let message = BusMessage::with_str_topic(
7257 "external.test",
7258 BusPayloadType::SubscribeCommand,
7259 Bytes::from(serde_json::to_vec(&command).unwrap()),
7260 SerializationEncoding::Json,
7261 );
7262
7263 assert!(
7264 !msgbus::get_message_bus()
7265 .borrow()
7266 .is_streaming_type(BusPayloadType::SubscribeCommand)
7267 );
7268 node.process_external_msgbus_message(&message);
7269
7270 assert_eq!(*received.borrow(), vec![expected.clone()]);
7271 assert_eq!(*steps.borrow(), vec![1, 2]);
7272 assert!(republished.borrow().is_empty());
7273
7274 received.borrow_mut().clear();
7275 steps.borrow_mut().clear();
7276 msgbus::get_message_bus()
7277 .borrow_mut()
7278 .add_streaming_type(BusPayloadType::SubscribeCommand);
7279 node.process_external_msgbus_message(&message);
7280
7281 assert_eq!(*received.borrow(), vec![expected.clone()]);
7282 assert_eq!(*republished.borrow(), vec![expected]);
7283 assert_eq!(*steps.borrow(), vec![1, 2, 3]);
7284 msgbus::get_message_bus().borrow_mut().dispose();
7285 }
7286
7287 #[rstest]
7288 fn test_builder_rejects_backtest() {
7289 let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Backtest);
7290
7291 assert!(result.is_err());
7292 assert!(result.unwrap_err().to_string().contains("Backtest"));
7293 }
7294
7295 #[rstest]
7296 fn test_builder_accepts_live_environment() {
7297 let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Live);
7298
7299 assert!(result.is_ok());
7300 }
7301
7302 #[rstest]
7303 fn test_builder_accepts_sandbox_environment() {
7304 let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox);
7305
7306 assert!(result.is_ok());
7307 }
7308
7309 #[rstest]
7310 fn test_builder_fluent_api_chaining() {
7311 let builder = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Live)
7312 .unwrap()
7313 .with_name("TestNode")
7314 .with_instance_id(UUID4::new())
7315 .with_load_state(false)
7316 .with_save_state(true)
7317 .with_timeout_connection(30)
7318 .with_timeout_reconciliation(60)
7319 .with_reconciliation(true)
7320 .with_reconciliation_lookback_mins(120)
7321 .with_timeout_portfolio(10)
7322 .with_timeout_disconnection_secs(5)
7323 .with_delay_post_stop_secs(3)
7324 .with_delay_shutdown_secs(10);
7325
7326 assert_eq!(builder.name(), "TestNode");
7327 }
7328
7329 #[rstest]
7330 fn test_builder_with_external_msgbus_egress_uses_configured_encoding() {
7331 let (external_egress, publications, closed) = CapturingExternalEgress::new();
7332
7333 let msgbus_config = MessageBusConfig {
7334 encoding: SerializationEncoding::Json,
7335 ..Default::default()
7336 };
7337
7338 let node = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
7339 .unwrap()
7340 .with_msgbus_config(msgbus_config)
7341 .with_external_msgbus_egress(Box::new(external_egress))
7342 .build()
7343 .expect("node builds with external message bus egress");
7344 let quote = QuoteTick::default();
7345
7346 msgbus::publish_quote("data.quotes.TEST".into(), "e);
7347
7348 let publications = publications.borrow();
7349 assert_eq!(publications.len(), 1);
7350 assert_eq!(publications[0].topic, "data.quotes.TEST");
7351 assert_eq!(
7352 serde_json::from_slice::<QuoteTick>(&publications[0].payload)
7353 .expect("JSON payload must decode as QuoteTick"),
7354 quote
7355 );
7356 drop(publications);
7357
7358 msgbus::get_message_bus().borrow_mut().dispose();
7359 assert!(closed.get());
7360 drop(node);
7361 }
7362
7363 #[rstest]
7364 #[tokio::test(flavor = "current_thread")]
7365 async fn test_builder_with_external_msgbus_factory_installs_egress_and_ingress() {
7366 let quote = QuoteTick::default();
7367 let (tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
7368 let publications = Arc::new(Mutex::new(Vec::new()));
7369 let closed = Arc::new(AtomicBool::new(false));
7370 let factory = CapturingBackingFactory::new(publications.clone(), closed.clone(), Some(rx));
7371
7372 let msgbus_config = MessageBusConfig {
7373 external_streams: Some(vec!["stream".to_string()]),
7374 ..Default::default()
7375 };
7376
7377 let config = LiveNodeConfig {
7378 environment: Environment::Sandbox,
7379 msgbus: Some(msgbus_config),
7380 exec_engine: crate::config::LiveExecutionEngineConfig {
7381 reconciliation: false,
7382 ..Default::default()
7383 },
7384 delay_post_stop: Duration::ZERO,
7385 timeout_connection: Duration::from_millis(500),
7386 timeout_disconnection: Duration::from_millis(500),
7387 ..Default::default()
7388 };
7389
7390 let mut node = LiveNodeBuilder::from_config(config)
7391 .unwrap()
7392 .with_external_msgbus_factory(Box::new(factory))
7393 .build()
7394 .expect("node builds with external message bus factory");
7395
7396 msgbus::publish_quote("data.quotes.TEST".into(), "e);
7397 {
7398 let publications = publications.lock();
7399 assert_eq!(publications.len(), 1);
7400 assert_eq!(publications[0].topic, "data.quotes.TEST");
7401 assert_eq!(
7402 serde_json::from_slice::<QuoteTick>(&publications[0].payload)
7403 .expect("JSON payload must decode as QuoteTick"),
7404 quote
7405 );
7406 }
7407
7408 let received = Rc::new(RefCell::new(Vec::<QuoteTick>::new()));
7409 let handle = node.handle();
7410
7411 let handler = TypedHandler::from({
7414 let received = received.clone();
7415 move |quote: &QuoteTick| {
7416 received.borrow_mut().push(*quote);
7417 }
7418 });
7419
7420 msgbus::subscribe_quotes("data.quotes.*".into(), handler, None);
7421 msgbus::get_message_bus()
7422 .borrow_mut()
7423 .add_streaming_type(BusPayloadType::QuoteTick);
7424
7425 let payload =
7426 Bytes::from(serde_json::to_vec("e).expect("QuoteTick should serialize as JSON"));
7427 let message = BusMessage::with_str_topic(
7428 "data.quotes.TEST",
7429 BusPayloadType::QuoteTick,
7430 payload,
7431 SerializationEncoding::Json,
7432 );
7433
7434 tokio::time::timeout(Duration::from_secs(30), async {
7435 let run = node.run();
7436 tokio::pin!(run);
7437
7438 let drive = async {
7439 wait_until_async(|| async { handle.is_running() }, Duration::from_secs(10)).await;
7440
7441 tx.send(message)
7442 .await
7443 .expect("external ingress receiver should be open");
7444
7445 wait_until_async(
7446 || async { received.borrow().len() == 1 },
7447 Duration::from_secs(10),
7448 )
7449 .await;
7450 assert_eq!(*received.borrow(), vec![quote]);
7451 handle.stop();
7452 };
7453
7454 tokio::select! {
7455 biased;
7456
7457 () = drive => {}
7458 result = &mut run => {
7459 panic!("node stopped before factory ingress was republished: {result:?}");
7460 }
7461 }
7462
7463 run.await.expect("node should stop cleanly");
7464 })
7465 .await
7466 .expect("live node should republish factory ingress and stop before timeout");
7467
7468 assert_eq!(handle.state(), NodeState::Stopped);
7469 assert!(closed.load(Ordering::Relaxed));
7470 msgbus::get_message_bus().borrow_mut().dispose();
7471 }
7472
7473 #[rstest]
7474 #[tokio::test(flavor = "current_thread")]
7475 async fn test_builder_with_external_msgbus_factory_without_streams_runs_without_ingress() {
7476 let quote = QuoteTick::default();
7477 let publications = Arc::new(Mutex::new(Vec::new()));
7478 let closed = Arc::new(AtomicBool::new(false));
7479 let factory = CapturingBackingFactory::new(publications.clone(), closed.clone(), None);
7480
7481 let config = LiveNodeConfig {
7482 environment: Environment::Sandbox,
7483 msgbus: Some(MessageBusConfig::default()),
7484 exec_engine: crate::config::LiveExecutionEngineConfig {
7485 reconciliation: false,
7486 ..Default::default()
7487 },
7488 delay_post_stop: Duration::ZERO,
7489 timeout_connection: Duration::from_millis(500),
7490 timeout_disconnection: Duration::from_millis(500),
7491 ..Default::default()
7492 };
7493
7494 let mut node = LiveNodeBuilder::from_config(config)
7495 .unwrap()
7496 .with_external_msgbus_factory(Box::new(factory))
7497 .build()
7498 .expect("node builds with egress-only message bus factory");
7499 let handle = node.handle();
7500
7501 msgbus::publish_quote("data.quotes.TEST".into(), "e);
7502 {
7503 let publications = publications.lock();
7504 assert_eq!(publications.len(), 1);
7505 assert_eq!(publications[0].topic, "data.quotes.TEST");
7506 }
7507
7508 tokio::time::timeout(Duration::from_secs(30), async {
7509 let run = node.run();
7510 tokio::pin!(run);
7511
7512 let drive = async {
7513 wait_until_async(|| async { handle.is_running() }, Duration::from_secs(10)).await;
7514 handle.stop();
7515 };
7516
7517 tokio::select! {
7518 biased;
7519
7520 () = drive => {}
7521 result = &mut run => {
7522 panic!("node stopped before egress-only factory run was observed: {result:?}");
7523 }
7524 }
7525
7526 run.await.expect("node should stop cleanly");
7527 })
7528 .await
7529 .expect("live node should run without external ingress before timeout");
7530
7531 assert_eq!(handle.state(), NodeState::Stopped);
7532 msgbus::get_message_bus().borrow_mut().dispose();
7533 assert!(closed.load(Ordering::Relaxed));
7534 }
7535
7536 #[rstest]
7537 fn test_builder_with_external_msgbus_factory_rejects_injected_surfaces() {
7538 let (external_egress, _publications, _closed) = CapturingExternalEgress::new();
7539
7540 let egress_factory = CapturingBackingFactory::new(
7541 Arc::new(Mutex::new(Vec::new())),
7542 Arc::new(AtomicBool::new(false)),
7543 None,
7544 );
7545 let egress_error = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
7546 .unwrap()
7547 .with_external_msgbus_factory(Box::new(egress_factory))
7548 .with_external_msgbus_egress(Box::new(external_egress))
7549 .build()
7550 .expect_err("builder should reject factory plus injected egress");
7551
7552 assert!(
7553 egress_error
7554 .to_string()
7555 .contains("cannot be combined with injected egress or ingress")
7556 );
7557
7558 let (_tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
7559
7560 let ingress_factory = CapturingBackingFactory::new(
7561 Arc::new(Mutex::new(Vec::new())),
7562 Arc::new(AtomicBool::new(false)),
7563 None,
7564 );
7565 let ingress = CapturingExternalIngress::new(rx, Rc::new(Cell::new(false)));
7566 let ingress_error = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
7567 .unwrap()
7568 .with_external_msgbus_factory(Box::new(ingress_factory))
7569 .with_external_ingress(Box::new(ingress))
7570 .build()
7571 .expect_err("builder should reject factory plus injected ingress");
7572
7573 assert!(
7574 ingress_error
7575 .to_string()
7576 .contains("cannot be combined with injected egress or ingress")
7577 );
7578 }
7579
7580 #[rstest]
7581 #[tokio::test(flavor = "current_thread")]
7582 async fn test_run_republishes_external_ingress_on_local_msgbus() {
7583 let quote = QuoteTick::default();
7584 let received = Rc::new(RefCell::new(Vec::<QuoteTick>::new()));
7585 let payload =
7586 Bytes::from(serde_json::to_vec("e).expect("QuoteTick should serialize as JSON"));
7587 let message = BusMessage::with_str_topic(
7588 "data.quotes.TEST",
7589 BusPayloadType::QuoteTick,
7590 payload,
7591 SerializationEncoding::Json,
7592 );
7593 let (tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
7594 let closed = Rc::new(Cell::new(false));
7595 let ingress = CapturingExternalIngress::new(rx, closed.clone());
7596
7597 let config = LiveNodeConfig {
7598 environment: Environment::Sandbox,
7599 exec_engine: crate::config::LiveExecutionEngineConfig {
7600 reconciliation: false,
7601 ..Default::default()
7602 },
7603 delay_post_stop: Duration::ZERO,
7604 timeout_connection: Duration::from_millis(500),
7605 timeout_disconnection: Duration::from_millis(500),
7606 ..Default::default()
7607 };
7608
7609 let mut node = LiveNodeBuilder::from_config(config)
7610 .unwrap()
7611 .with_external_ingress(Box::new(ingress))
7612 .build()
7613 .expect("node builds with external message bus ingress");
7614 let handle = node.handle();
7615
7616 let handler = TypedHandler::from({
7617 let received = received.clone();
7618 move |quote: &QuoteTick| {
7619 received.borrow_mut().push(*quote);
7620 }
7621 });
7622
7623 msgbus::subscribe_quotes("data.quotes.*".into(), handler, None);
7624 msgbus::get_message_bus()
7625 .borrow_mut()
7626 .add_streaming_type(BusPayloadType::QuoteTick);
7627
7628 tokio::time::timeout(Duration::from_secs(30), async {
7629 let run = node.run();
7630 tokio::pin!(run);
7631
7632 let drive = async {
7633 wait_until_async(|| async { handle.is_running() }, Duration::from_secs(10)).await;
7634
7635 tx.send(message)
7636 .await
7637 .expect("external ingress receiver should be open");
7638
7639 wait_until_async(
7640 || async { received.borrow().len() == 1 },
7641 Duration::from_secs(10),
7642 )
7643 .await;
7644 assert_eq!(*received.borrow(), vec![quote]);
7645 handle.stop();
7646 };
7647
7648 tokio::select! {
7649 biased;
7650
7651 () = drive => {}
7652 result = &mut run => {
7653 panic!("node stopped before external message was republished: {result:?}");
7654 }
7655 }
7656
7657 run.await.expect("node should stop cleanly");
7658 })
7659 .await
7660 .expect("live node should republish ingress and stop before timeout");
7661
7662 assert_eq!(handle.state(), NodeState::Stopped);
7663 assert!(closed.get());
7664 msgbus::get_message_bus().borrow_mut().dispose();
7665 }
7666
7667 #[rstest]
7668 #[tokio::test(flavor = "current_thread")]
7669 async fn test_run_closes_external_ingress_when_receiver_closes() {
7670 let (tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
7671 let closed = Rc::new(Cell::new(false));
7672 let ingress = CapturingExternalIngress::new(rx, closed.clone());
7673
7674 let config = LiveNodeConfig {
7675 environment: Environment::Sandbox,
7676 exec_engine: crate::config::LiveExecutionEngineConfig {
7677 reconciliation: false,
7678 ..Default::default()
7679 },
7680 delay_post_stop: Duration::ZERO,
7681 timeout_connection: Duration::from_millis(500),
7682 timeout_disconnection: Duration::from_millis(500),
7683 ..Default::default()
7684 };
7685
7686 let mut node = LiveNodeBuilder::from_config(config)
7687 .unwrap()
7688 .with_external_ingress(Box::new(ingress))
7689 .build()
7690 .expect("node builds with external message bus ingress");
7691 let handle = node.handle();
7692
7693 tokio::time::timeout(Duration::from_secs(30), async {
7694 let run = node.run();
7695 tokio::pin!(run);
7696
7697 let drive = async {
7698 wait_until_async(|| async { handle.is_running() }, Duration::from_secs(10)).await;
7699
7700 drop(tx);
7701
7702 wait_until_async(|| async { closed.get() }, Duration::from_secs(10)).await;
7703 assert!(
7704 handle.is_running(),
7705 "node should keep running after ingress closes"
7706 );
7707 handle.stop();
7708 };
7709
7710 tokio::select! {
7711 biased;
7712
7713 () = drive => {}
7714 result = &mut run => {
7715 panic!("node stopped before ingress close was observed: {result:?}");
7716 }
7717 }
7718
7719 run.await.expect("node should stop cleanly");
7720 })
7721 .await
7722 .expect("live node should close ingress and stop before timeout");
7723
7724 assert_eq!(handle.state(), NodeState::Stopped);
7725 }
7726
7727 #[rstest]
7728 #[tokio::test(flavor = "current_thread")]
7729 async fn test_run_aborts_startup_when_external_ingress_receiver_unavailable() {
7730 let closed = Rc::new(Cell::new(false));
7731 let ingress = FailingExternalIngress::new(closed.clone());
7732
7733 let config = LiveNodeConfig {
7734 environment: Environment::Sandbox,
7735 exec_engine: crate::config::LiveExecutionEngineConfig {
7736 reconciliation: false,
7737 ..Default::default()
7738 },
7739 delay_post_stop: Duration::ZERO,
7740 timeout_connection: Duration::from_millis(500),
7741 timeout_disconnection: Duration::from_millis(500),
7742 ..Default::default()
7743 };
7744
7745 let mut node = LiveNodeBuilder::from_config(config)
7746 .unwrap()
7747 .with_external_ingress(Box::new(ingress))
7748 .build()
7749 .expect("node builds with external message bus ingress");
7750 let handle = node.handle();
7751
7752 let err = node.run().await.expect_err("run should fail");
7753
7754 assert!(
7755 err.to_string()
7756 .contains("external ingress receiver unavailable")
7757 );
7758 assert_eq!(handle.state(), NodeState::Stopped);
7759 assert!(closed.get());
7760 }
7761
7762 #[cfg(feature = "python")]
7763 #[rstest]
7764 fn test_node_build_and_initial_state() {
7765 let node = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
7766 .unwrap()
7767 .with_name("TestNode")
7768 .build()
7769 .unwrap();
7770
7771 assert_eq!(node.state(), NodeState::Idle);
7772 assert!(!node.is_running());
7773 assert_eq!(node.environment(), Environment::Sandbox);
7774 assert_eq!(node.trader_id(), TraderId::from("TRADER-001"));
7775 }
7776
7777 #[cfg(feature = "python")]
7778 #[rstest]
7779 fn test_node_build_replaces_stale_runner_senders() {
7780 replace_data_cmd_sender(Arc::new(SyncDataCommandSender));
7781 replace_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
7782
7783 let first = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
7784 .unwrap()
7785 .with_name("FirstNode")
7786 .build()
7787 .unwrap();
7788
7789 assert_eq!(first.state(), NodeState::Idle);
7790 drop(first);
7791
7792 let second = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
7793 .unwrap()
7794 .with_name("SecondNode")
7795 .build()
7796 .unwrap();
7797
7798 assert_eq!(second.state(), NodeState::Idle);
7799 assert!(!second.is_running());
7800 }
7801
7802 #[cfg(feature = "python")]
7803 #[rstest]
7804 fn test_node_handle_reflects_node_state() {
7805 let node = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
7806 .unwrap()
7807 .with_name("TestNode")
7808 .build()
7809 .unwrap();
7810
7811 let handle = node.handle();
7812
7813 assert_eq!(handle.state(), NodeState::Idle);
7814 assert!(!handle.is_running());
7815 }
7816
7817 #[rstest]
7818 fn test_pending_drain_data_returns_false_when_empty() {
7819 let mut pending = PendingEvents::default();
7820
7821 assert!(!pending.drain_data());
7822 }
7823
7824 #[rstest]
7825 fn test_pending_drain_data_returns_true_when_non_empty() {
7826 use nautilus_model::instruments::{InstrumentAny, stubs::crypto_perpetual_ethusdt};
7827
7828 let mut pending = PendingEvents::default();
7829 pending.data_evts.push(
7830 DataEvent::Instrument(InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt()))
7831 .into(),
7832 );
7833
7834 assert!(pending.drain_data());
7835 assert!(pending.data_evts.is_empty());
7836 }
7837
7838 fn stub_data_event() -> DataEvent {
7839 use nautilus_model::instruments::{InstrumentAny, stubs::crypto_perpetual_ethusdt};
7840
7841 DataEvent::Instrument(InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt()))
7842 }
7843
7844 fn stub_data_command() -> DataCommand {
7845 use nautilus_common::messages::data::{SubscribeCommand, subscribe::SubscribeInstruments};
7846 use nautilus_core::{UUID4, UnixNanos};
7847 use nautilus_model::identifiers::Venue;
7848
7849 DataCommand::Subscribe(SubscribeCommand::Instruments(SubscribeInstruments::new(
7850 None,
7851 Venue::from("TEST"),
7852 UUID4::new(),
7853 UnixNanos::default(),
7854 None,
7855 None,
7856 )))
7857 }
7858
7859 fn stub_system_command() -> SystemCommand {
7860 SystemCommand::ReconnectSocket(ReconnectSocket::new(
7861 TraderId::from("TRADER-001"),
7862 ClientId::from("POLYMARKET"),
7863 Ustr::from("polymarket-market-streams"),
7864 UnixNanos::default(),
7865 ))
7866 }
7867
7868 #[rstest]
7869 fn test_flush_pending_data_drains_events_and_commands() {
7870 let (evt_tx, mut evt_rx) =
7871 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataEvent>>();
7872 let (cmd_tx, mut cmd_rx) =
7873 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataCommand>>();
7874
7875 let mut pending = PendingEvents::default();
7876
7877 pending.data_evts.push((stub_data_event()).into());
7879 pending.data_cmds.push(stub_data_command().into());
7880
7881 evt_tx.send((stub_data_event()).into()).unwrap();
7883 cmd_tx.send(stub_data_command().into()).unwrap();
7884
7885 flush_pending_data(&mut pending, &mut evt_rx, &mut cmd_rx);
7886
7887 assert!(pending.data_evts.is_empty());
7888 assert!(pending.data_cmds.is_empty());
7889 assert!(evt_rx.try_recv().is_err());
7890 assert!(cmd_rx.try_recv().is_err());
7891 }
7892
7893 #[rstest]
7894 fn test_flush_pending_data_drains_mixed_sources() {
7895 let (evt_tx, mut evt_rx) =
7896 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataEvent>>();
7897 let (cmd_tx, mut cmd_rx) =
7898 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataCommand>>();
7899
7900 let mut pending = PendingEvents::default();
7901
7902 pending.data_evts.push((stub_data_event()).into());
7904 cmd_tx.send(stub_data_command().into()).unwrap();
7905
7906 evt_tx.send((stub_data_event()).into()).unwrap();
7908 evt_tx.send((stub_data_event()).into()).unwrap();
7909 cmd_tx.send(stub_data_command().into()).unwrap();
7910
7911 flush_pending_data(&mut pending, &mut evt_rx, &mut cmd_rx);
7912
7913 assert!(pending.data_evts.is_empty());
7914 assert!(pending.data_cmds.is_empty());
7915 assert!(evt_rx.try_recv().is_err());
7916 assert!(cmd_rx.try_recv().is_err());
7917 }
7918
7919 #[rstest]
7920 fn test_pending_system_events_stay_separate_from_data() {
7921 let mut pending = PendingEvents::default();
7922
7923 let change = SocketStateChange::new(
7924 ClientId::from("BINANCE"),
7925 Some(Venue::from("BINANCE")),
7926 ustr::Ustr::from("binance-futures-market-streams"),
7927 SocketState::Connected,
7928 );
7929
7930 pending
7931 .system_events
7932 .push(SystemEvent::SocketState(change).into());
7933 let system_events = pending
7934 .take_system_events()
7935 .into_iter()
7936 .map(|message| message.dispatch(|value| value))
7937 .collect::<Vec<_>>();
7938
7939 assert_eq!(system_events, vec![SystemEvent::SocketState(change)]);
7940 assert!(pending.is_empty());
7941 }
7942
7943 #[rstest]
7944 fn test_pending_system_commands_stay_separate_from_data() {
7945 let mut pending = PendingEvents::default();
7946 let command = stub_system_command();
7947
7948 pending.system_commands.push(command.into());
7949 let system_commands = pending
7950 .take_system_commands()
7951 .into_iter()
7952 .map(|message| message.dispatch(|value| value))
7953 .collect::<Vec<_>>();
7954
7955 assert_eq!(system_commands, vec![command]);
7956 assert!(pending.is_empty());
7957 }
7958
7959 fn stub_time_event_handler() -> TimeEventMessage {
7960 use std::rc::Rc;
7961
7962 use nautilus_common::{
7963 runner::TimeEventMessage,
7964 timer::{TimeEvent, TimeEventCallback},
7965 };
7966 use nautilus_core::{UUID4, UnixNanos};
7967 use ustr::Ustr;
7968
7969 TimeEventMessage::new(
7970 TimeEvent::new(
7971 Ustr::from("test-timer"),
7972 UUID4::new(),
7973 UnixNanos::default(),
7974 UnixNanos::default(),
7975 ),
7976 TimeEventCallback::RustLocal(Rc::new(|_| {})),
7977 )
7978 }
7979
7980 fn stub_trading_command_message() -> TradingCommandMessage {
7981 use nautilus_common::messages::execution::query::QueryAccount;
7982 use nautilus_core::{UUID4, UnixNanos};
7983 use nautilus_model::identifiers::AccountId;
7984
7985 TradingCommandMessage::new(
7986 MessagingSwitchboard::exec_engine_execute(),
7987 TradingCommand::QueryAccount(QueryAccount::new(
7988 TraderId::from("TESTER-001"),
7989 None,
7990 AccountId::from("TEST-001"),
7991 UUID4::new(),
7992 UnixNanos::default(),
7993 None,
7994 None, )),
7996 )
7997 }
7998
7999 fn stub_exec_event() -> ExecutionEvent {
8000 use nautilus_model::{
8001 enums::{LiquiditySide, OrderSide},
8002 identifiers::{AccountId, InstrumentId, TradeId, VenueOrderId},
8003 reports::FillReport,
8004 types::{Money, Price, Quantity},
8005 };
8006
8007 ExecutionEvent::Report(ExecutionReport::Fill(Box::new(FillReport::new(
8008 AccountId::from("TEST-001"),
8009 InstrumentId::from("TEST.VENUE"),
8010 VenueOrderId::from("V-001"),
8011 TradeId::from("T-001"),
8012 OrderSide::Buy,
8013 Quantity::from("1.0"),
8014 Price::from("100.0"),
8015 Money::from("0.01 USD"),
8016 LiquiditySide::Maker,
8017 None,
8018 None,
8019 nautilus_core::UnixNanos::default(),
8020 nautilus_core::UnixNanos::default(),
8021 None,
8022 ))))
8023 }
8024
8025 #[rstest]
8026 fn test_flush_all_pending_drains_buffered_channels() {
8027 let (time_tx, mut time_rx) =
8028 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<TimeEventMessage>>();
8029 let (system_evt_tx, mut system_evt_rx) =
8030 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<SystemEvent>>();
8031 let (system_cmd_tx, mut system_cmd_rx) =
8032 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<SystemCommand>>();
8033 let (data_evt_tx, mut data_evt_rx) =
8034 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataEvent>>();
8035 let (data_cmd_tx, mut data_cmd_rx) =
8036 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataCommand>>();
8037 let (exec_evt_tx, mut exec_evt_rx) =
8038 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<ExecutionEvent>>();
8039 let (exec_cmd_tx, mut exec_cmd_rx) =
8040 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<TradingCommandMessage>>();
8041
8042 let mut pending = PendingEvents::default();
8043
8044 pending.data_evts.push((stub_data_event()).into());
8046 pending.data_cmds.push(stub_data_command().into());
8047
8048 time_tx.send((stub_time_event_handler()).into()).unwrap();
8050
8051 let change = SocketStateChange::new(
8052 ClientId::from("BINANCE"),
8053 Some(Venue::from("BINANCE")),
8054 Ustr::from("binance-futures-market-streams"),
8055 SocketState::Connected,
8056 );
8057 system_evt_tx
8058 .send((SystemEvent::SocketState(change)).into())
8059 .unwrap();
8060 system_cmd_tx.send((stub_system_command()).into()).unwrap();
8061 data_evt_tx.send((stub_data_event()).into()).unwrap();
8062 data_cmd_tx.send(stub_data_command().into()).unwrap();
8063 exec_evt_tx.send((stub_exec_event()).into()).unwrap();
8064 exec_cmd_tx
8065 .send(stub_trading_command_message().into())
8066 .unwrap();
8067
8068 flush_all_pending(
8069 &mut pending,
8070 &mut time_rx,
8071 &mut system_evt_rx,
8072 &mut system_cmd_rx,
8073 &mut exec_evt_rx,
8074 &mut exec_cmd_rx,
8075 &mut data_evt_rx,
8076 &mut data_cmd_rx,
8077 );
8078
8079 let system_events = pending
8080 .take_system_events()
8081 .into_iter()
8082 .map(|message| message.dispatch(|value| value))
8083 .collect::<Vec<_>>();
8084 let system_commands = pending
8085 .take_system_commands()
8086 .into_iter()
8087 .map(|message| message.dispatch(|value| value))
8088 .collect::<Vec<_>>();
8089 assert_eq!(system_events, vec![SystemEvent::SocketState(change)]);
8090 assert_eq!(system_commands, vec![stub_system_command()]);
8091 assert!(pending.data_evts.is_empty());
8092 assert!(pending.data_cmds.is_empty());
8093 assert!(pending.exec_reports.is_empty());
8094 assert!(pending.exec_cmds.is_empty());
8095 assert!(pending.order_evts.is_empty());
8096 assert!(time_rx.try_recv().is_err());
8097 assert!(system_evt_rx.try_recv().is_err());
8098 assert!(system_cmd_rx.try_recv().is_err());
8099 assert!(data_evt_rx.try_recv().is_err());
8100 assert!(data_cmd_rx.try_recv().is_err());
8101 assert!(exec_evt_rx.try_recv().is_err());
8102 assert!(exec_cmd_rx.try_recv().is_err());
8103 }
8104
8105 fn stub_order_event() -> ExecutionEvent {
8106 use nautilus_model::events::order::spec::OrderSubmittedSpec;
8107
8108 ExecutionEvent::Order(OrderEventAny::Submitted(
8109 OrderSubmittedSpec::builder().build(),
8110 ))
8111 }
8112
8113 fn stub_account_event() -> ExecutionEvent {
8114 use nautilus_core::{UUID4, UnixNanos};
8115 use nautilus_model::{
8116 enums::AccountType, events::account::state::AccountState, identifiers::AccountId,
8117 };
8118
8119 ExecutionEvent::Account(AccountState::new(
8120 AccountId::from("TEST-001"),
8121 AccountType::Cash,
8122 vec![],
8123 vec![],
8124 true,
8125 UUID4::new(),
8126 UnixNanos::default(),
8127 UnixNanos::default(),
8128 None,
8129 ))
8130 }
8131
8132 #[rstest]
8133 fn test_flush_all_pending_routes_order_event_to_order_evts() {
8134 let (_time_tx, mut time_rx) =
8135 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<TimeEventMessage>>();
8136 let (_system_evt_tx, mut system_evt_rx) =
8137 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<SystemEvent>>();
8138 let (_system_cmd_tx, mut system_cmd_rx) =
8139 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<SystemCommand>>();
8140 let (_data_evt_tx, mut data_evt_rx) =
8141 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataEvent>>();
8142 let (_data_cmd_tx, mut data_cmd_rx) =
8143 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataCommand>>();
8144 let (exec_evt_tx, mut exec_evt_rx) =
8145 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<ExecutionEvent>>();
8146 let (_exec_cmd_tx, mut exec_cmd_rx) =
8147 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<TradingCommandMessage>>();
8148
8149 let mut pending = PendingEvents::default();
8150
8151 exec_evt_tx.send((stub_order_event()).into()).unwrap();
8152 exec_evt_tx.send((stub_exec_event()).into()).unwrap();
8153
8154 flush_all_pending(
8155 &mut pending,
8156 &mut time_rx,
8157 &mut system_evt_rx,
8158 &mut system_cmd_rx,
8159 &mut exec_evt_rx,
8160 &mut exec_cmd_rx,
8161 &mut data_evt_rx,
8162 &mut data_cmd_rx,
8163 );
8164
8165 assert!(pending.order_evts.is_empty());
8167 assert!(pending.exec_reports.is_empty());
8168 assert!(exec_evt_rx.try_recv().is_err());
8169 }
8170
8171 #[rstest]
8172 fn test_flush_all_pending_routes_account_event_immediately() {
8173 let (_time_tx, mut time_rx) =
8174 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<TimeEventMessage>>();
8175 let (_system_evt_tx, mut system_evt_rx) =
8176 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<SystemEvent>>();
8177 let (_system_cmd_tx, mut system_cmd_rx) =
8178 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<SystemCommand>>();
8179 let (_data_evt_tx, mut data_evt_rx) =
8180 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataEvent>>();
8181 let (_data_cmd_tx, mut data_cmd_rx) =
8182 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataCommand>>();
8183 let (exec_evt_tx, mut exec_evt_rx) =
8184 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<ExecutionEvent>>();
8185 let (_exec_cmd_tx, mut exec_cmd_rx) =
8186 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<TradingCommandMessage>>();
8187
8188 let mut pending = PendingEvents::default();
8189
8190 exec_evt_tx.send((stub_account_event()).into()).unwrap();
8191
8192 flush_all_pending(
8193 &mut pending,
8194 &mut time_rx,
8195 &mut system_evt_rx,
8196 &mut system_cmd_rx,
8197 &mut exec_evt_rx,
8198 &mut exec_cmd_rx,
8199 &mut data_evt_rx,
8200 &mut data_cmd_rx,
8201 );
8202
8203 assert!(pending.exec_reports.is_empty());
8205 assert!(pending.order_evts.is_empty());
8206 assert!(pending.exec_cmds.is_empty());
8207 assert!(exec_evt_rx.try_recv().is_err());
8208 }
8209
8210 #[rstest]
8211 fn test_pending_is_empty_when_default() {
8212 let pending = PendingEvents::default();
8213
8214 assert!(pending.is_empty());
8215 }
8216
8217 #[rstest]
8218 fn test_pending_is_empty_false_with_data_evt() {
8219 let mut pending = PendingEvents::default();
8220 pending.data_evts.push((stub_data_event()).into());
8221
8222 assert!(!pending.is_empty());
8223 }
8224
8225 #[rstest]
8226 fn test_pending_is_empty_false_with_data_cmd() {
8227 let mut pending = PendingEvents::default();
8228 pending.data_cmds.push(stub_data_command().into());
8229
8230 assert!(!pending.is_empty());
8231 }
8232
8233 #[rstest]
8234 fn test_pending_is_empty_false_with_exec_cmd() {
8235 let mut pending = PendingEvents::default();
8236 pending
8237 .exec_cmds
8238 .push(stub_trading_command_message().into());
8239
8240 assert!(!pending.is_empty());
8241 }
8242
8243 #[rstest]
8244 fn test_pending_drain_preserves_trading_command_target() {
8245 std::thread::spawn(|| {
8246 msgbus::get_message_bus().borrow_mut().dispose();
8247 let risk_commands = Rc::new(RefCell::new(Vec::new()));
8248 let exec_commands = Rc::new(RefCell::new(Vec::new()));
8249
8250 let risk_commands_handler = risk_commands.clone();
8251 msgbus::register_trading_command_endpoint(
8252 MessagingSwitchboard::risk_engine_execute(),
8253 TypedIntoHandler::from(move |command: TradingCommand| {
8254 risk_commands_handler.borrow_mut().push(command);
8255 }),
8256 );
8257
8258 let exec_commands_handler = exec_commands.clone();
8259 msgbus::register_trading_command_endpoint(
8260 MessagingSwitchboard::exec_engine_execute(),
8261 TypedIntoHandler::from(move |command: TradingCommand| {
8262 exec_commands_handler.borrow_mut().push(command);
8263 }),
8264 );
8265
8266 let mut pending = PendingEvents::default();
8267 pending.exec_cmds.push(
8268 TradingCommandMessage::new(
8269 MessagingSwitchboard::risk_engine_execute(),
8270 TradingCommand::QueryAccount(QueryAccount::new(
8271 TraderId::from("TESTER-001"),
8272 None,
8273 AccountId::from("TEST-001"),
8274 UUID4::new(),
8275 UnixNanos::default(),
8276 None,
8277 None,
8278 )),
8279 )
8280 .into(),
8281 );
8282
8283 pending.drain();
8284
8285 assert!(pending.is_empty());
8286 assert_eq!(risk_commands.borrow().len(), 1);
8287 assert!(matches!(
8288 &risk_commands.borrow()[0],
8289 TradingCommand::QueryAccount(_)
8290 ));
8291 assert_eq!(exec_commands.borrow().as_slice(), &[]);
8292 })
8293 .join()
8294 .unwrap();
8295 }
8296
8297 #[rstest]
8298 fn test_pending_is_empty_false_with_exec_report() {
8299 let mut pending = PendingEvents::default();
8300
8301 if let ExecutionEvent::Report(report) = stub_exec_event() {
8302 pending.exec_reports.push((report).into());
8303 }
8304
8305 assert!(!pending.is_empty());
8306 }
8307
8308 #[rstest]
8309 fn test_pending_is_empty_false_with_order_evt() {
8310 let mut pending = PendingEvents::default();
8311
8312 if let ExecutionEvent::Order(order_evt) = stub_order_event() {
8313 pending.order_evts.push((order_evt).into());
8314 }
8315
8316 assert!(!pending.is_empty());
8317 }
8318
8319 fn stub_submitted_batch_event() -> ExecutionEvent {
8320 use nautilus_model::{
8321 events::{OrderSubmittedBatch, order::spec::OrderSubmittedSpec},
8322 identifiers::ClientOrderId,
8323 };
8324
8325 let events = vec![
8326 OrderSubmittedSpec::builder()
8327 .client_order_id(ClientOrderId::from("O-001"))
8328 .build(),
8329 OrderSubmittedSpec::builder()
8330 .client_order_id(ClientOrderId::from("O-002"))
8331 .build(),
8332 ];
8333
8334 ExecutionEvent::OrderSubmittedBatch(OrderSubmittedBatch::new(events))
8335 }
8336
8337 fn stub_canceled_batch_event() -> ExecutionEvent {
8338 use nautilus_model::{
8339 events::{OrderCanceledBatch, order::spec::OrderCanceledSpec},
8340 identifiers::ClientOrderId,
8341 };
8342
8343 let events = vec![
8344 OrderCanceledSpec::builder()
8345 .client_order_id(ClientOrderId::from("O-001"))
8346 .build(),
8347 OrderCanceledSpec::builder()
8348 .client_order_id(ClientOrderId::from("O-002"))
8349 .build(),
8350 ];
8351
8352 ExecutionEvent::OrderCanceledBatch(OrderCanceledBatch::new(events))
8353 }
8354
8355 #[rstest]
8356 fn test_flush_all_pending_buffers_submitted_batch_as_individual_events() {
8357 let (_time_tx, mut time_rx) =
8358 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<TimeEventMessage>>();
8359 let (_system_evt_tx, mut system_evt_rx) =
8360 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<SystemEvent>>();
8361 let (_system_cmd_tx, mut system_cmd_rx) =
8362 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<SystemCommand>>();
8363 let (_data_evt_tx, mut data_evt_rx) =
8364 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataEvent>>();
8365 let (_data_cmd_tx, mut data_cmd_rx) =
8366 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataCommand>>();
8367 let (exec_evt_tx, mut exec_evt_rx) =
8368 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<ExecutionEvent>>();
8369 let (_exec_cmd_tx, mut exec_cmd_rx) =
8370 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<TradingCommandMessage>>();
8371
8372 let mut pending = PendingEvents::default();
8373
8374 exec_evt_tx
8375 .send((stub_submitted_batch_event()).into())
8376 .unwrap();
8377
8378 flush_all_pending(
8379 &mut pending,
8380 &mut time_rx,
8381 &mut system_evt_rx,
8382 &mut system_cmd_rx,
8383 &mut exec_evt_rx,
8384 &mut exec_cmd_rx,
8385 &mut data_evt_rx,
8386 &mut data_cmd_rx,
8387 );
8388
8389 assert!(pending.order_evts.is_empty());
8391 assert!(exec_evt_rx.try_recv().is_err());
8392 }
8393
8394 #[rstest]
8395 fn test_flush_all_pending_buffers_canceled_batch_as_individual_events() {
8396 let (_time_tx, mut time_rx) =
8397 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<TimeEventMessage>>();
8398 let (_system_evt_tx, mut system_evt_rx) =
8399 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<SystemEvent>>();
8400 let (_system_cmd_tx, mut system_cmd_rx) =
8401 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<SystemCommand>>();
8402 let (_data_evt_tx, mut data_evt_rx) =
8403 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataEvent>>();
8404 let (_data_cmd_tx, mut data_cmd_rx) =
8405 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<DataCommand>>();
8406 let (exec_evt_tx, mut exec_evt_rx) =
8407 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<ExecutionEvent>>();
8408 let (_exec_cmd_tx, mut exec_cmd_rx) =
8409 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<TradingCommandMessage>>();
8410
8411 let mut pending = PendingEvents::default();
8412
8413 exec_evt_tx
8414 .send((stub_canceled_batch_event()).into())
8415 .unwrap();
8416
8417 flush_all_pending(
8418 &mut pending,
8419 &mut time_rx,
8420 &mut system_evt_rx,
8421 &mut system_cmd_rx,
8422 &mut exec_evt_rx,
8423 &mut exec_cmd_rx,
8424 &mut data_evt_rx,
8425 &mut data_cmd_rx,
8426 );
8427
8428 assert!(pending.order_evts.is_empty());
8430 assert!(exec_evt_rx.try_recv().is_err());
8431 }
8432
8433 #[rstest]
8434 fn test_flush_all_pending_expands_batch_into_order_evts_before_drain() {
8435 use nautilus_model::identifiers::ClientOrderId;
8436
8437 let (exec_evt_tx, mut exec_evt_rx) =
8438 tokio::sync::mpsc::unbounded_channel::<DispatchMessage<ExecutionEvent>>();
8439
8440 exec_evt_tx
8441 .send((stub_canceled_batch_event()).into())
8442 .unwrap();
8443
8444 let mut pending = PendingEvents::default();
8445
8446 while let Ok(evt) = exec_evt_rx.try_recv() {
8447 pending.push_exec_event(evt);
8448 }
8449
8450 let events: Vec<_> = pending
8451 .order_evts
8452 .drain(..)
8453 .map(|event| event.dispatch(|event| event))
8454 .collect();
8455 assert_eq!(events.len(), 2);
8456 assert!(
8457 matches!(&events[0], OrderEventAny::Canceled(c) if c.client_order_id == ClientOrderId::from("O-001"))
8458 );
8459 assert!(
8460 matches!(&events[1], OrderEventAny::Canceled(c) if c.client_order_id == ClientOrderId::from("O-002"))
8461 );
8462 }
8463
8464 #[rstest]
8465 #[case(stub_order_event, 1)]
8466 #[case(stub_submitted_batch_event, 2)]
8467 #[case(stub_accepted_batch_event, 2)]
8468 #[case(stub_canceled_batch_event, 2)]
8469 fn test_pending_events_preserve_roots_when_splitting_batches(
8470 #[values(false, true)] rooted: bool,
8471 #[case] make_event: fn() -> ExecutionEvent,
8472 #[case] orders: usize,
8473 ) {
8474 let runner = AsyncRunner::new();
8475 runner.bind_senders();
8476 let mut channels = runner.take_channels();
8477
8478 let send = move || {
8479 get_data_event_sender().send(stub_data_event()).unwrap();
8480 get_exec_event_sender().send(stub_exec_event()).unwrap();
8481 get_exec_event_sender().send(make_event()).unwrap();
8482 };
8483
8484 if rooted {
8485 msgbus::register_trading_command_endpoint(
8486 MessagingSwitchboard::risk_engine_execute(),
8487 TypedIntoHandler::from(move |_| send()),
8488 );
8489 TradingCommandSender::execute(
8490 &SyncTradingCommandSender,
8491 TradingCommandMessage::new(
8492 MessagingSwitchboard::risk_engine_execute(),
8493 TradingCommand::QueryAccount(QueryAccount::new(
8494 "TRADER-001".into(),
8495 None,
8496 "SIM-001".into(),
8497 UUID4::new(),
8498 37.into(),
8499 None,
8500 None,
8501 )),
8502 ),
8503 );
8504 nautilus_common::runner::drain_trading_cmd_queue();
8505 } else {
8506 send();
8507 }
8508
8509 let mut pending = PendingEvents::default();
8510 pending
8511 .data_evts
8512 .push(channels.data_evt_rx.try_recv().unwrap());
8513 while let Ok(event) = channels.exec_evt_rx.try_recv() {
8514 pending.push_exec_event(event);
8515 }
8516
8517 assert_eq!(pending.data_evts.len(), 1);
8518 assert_eq!(pending.exec_reports.len(), 1);
8519 assert_eq!(pending.order_evts.len(), orders);
8520 assert_eq!(pending.data_evts[0].is_rooted(), rooted);
8521 assert_eq!(pending.exec_reports[0].is_rooted(), rooted);
8522 assert!(
8523 pending
8524 .order_evts
8525 .iter()
8526 .all(|event| event.is_rooted() == rooted)
8527 );
8528
8529 let results = std::thread::spawn(move || {
8530 let mut results = Vec::new();
8531 for event in pending.data_evts {
8532 results.push(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
8533 || event.dispatch(drop),
8534 )));
8535 }
8536
8537 for event in pending.exec_reports {
8538 results.push(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
8539 || event.dispatch(drop),
8540 )));
8541 }
8542
8543 for event in pending.order_evts {
8544 results.push(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
8545 || event.dispatch(drop),
8546 )));
8547 }
8548
8549 results
8550 })
8551 .join()
8552 .unwrap();
8553
8554 assert_eq!(results.len(), orders + 2);
8555
8556 for result in results {
8557 if rooted {
8558 let error = result.unwrap_err();
8559 assert!(
8560 error
8561 .downcast_ref::<String>()
8562 .unwrap()
8563 .contains("command context dispatched outside its owner thread")
8564 );
8565 } else {
8566 result.unwrap();
8567 }
8568 }
8569 }
8570
8571 fn stub_accepted_batch_event() -> ExecutionEvent {
8572 ExecutionEvent::OrderAcceptedBatch(OrderAcceptedBatch::new(vec![
8573 OrderAcceptedSpec::builder()
8574 .client_order_id(ClientOrderId::from("O-017"))
8575 .build(),
8576 OrderAcceptedSpec::builder()
8577 .client_order_id(ClientOrderId::from("O-023"))
8578 .build(),
8579 ]))
8580 }
8581
8582 #[derive(Debug)]
8583 struct CapturedEgressMessage {
8584 topic: String,
8585 payload: Bytes,
8586 }
8587
8588 type CapturedEgressMessages = Rc<RefCell<Vec<CapturedEgressMessage>>>;
8589 type SharedClosed = Rc<Cell<bool>>;
8590
8591 #[derive(Debug)]
8592 struct CapturingExternalIngress {
8593 rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
8594 closed: SharedClosed,
8595 }
8596
8597 impl CapturingExternalIngress {
8598 fn new(rx: tokio::sync::mpsc::Receiver<BusMessage>, closed: SharedClosed) -> Self {
8599 Self {
8600 rx: Some(rx),
8601 closed,
8602 }
8603 }
8604 }
8605
8606 impl MessageBusExternalIngress for CapturingExternalIngress {
8607 fn is_closed(&self) -> bool {
8608 self.closed.get()
8609 }
8610
8611 fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
8612 self.rx
8613 .take()
8614 .ok_or_else(|| anyhow::anyhow!("external ingress receiver already taken"))
8615 }
8616
8617 fn close(&mut self) {
8618 self.closed.set(true);
8619 }
8620 }
8621
8622 #[derive(Debug)]
8623 struct FailingExternalIngress {
8624 closed: SharedClosed,
8625 }
8626
8627 impl FailingExternalIngress {
8628 fn new(closed: SharedClosed) -> Self {
8629 Self { closed }
8630 }
8631 }
8632
8633 impl MessageBusExternalIngress for FailingExternalIngress {
8634 fn is_closed(&self) -> bool {
8635 self.closed.get()
8636 }
8637
8638 fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
8639 anyhow::bail!("external ingress receiver unavailable")
8640 }
8641
8642 fn close(&mut self) {
8643 self.closed.set(true);
8644 }
8645 }
8646
8647 struct CapturingExternalEgress {
8648 publications: CapturedEgressMessages,
8649 closed: SharedClosed,
8650 }
8651
8652 impl CapturingExternalEgress {
8653 fn new() -> (Self, CapturedEgressMessages, SharedClosed) {
8654 let publications = Rc::new(RefCell::new(Vec::new()));
8655 let closed = Rc::new(Cell::new(false));
8656 (
8657 Self {
8658 publications: publications.clone(),
8659 closed: closed.clone(),
8660 },
8661 publications,
8662 closed,
8663 )
8664 }
8665 }
8666
8667 impl MessageBusExternalEgress for CapturingExternalEgress {
8668 fn is_closed(&self) -> bool {
8669 self.closed.get()
8670 }
8671
8672 fn publish(&self, message: BusMessage) {
8673 self.publications.borrow_mut().push(CapturedEgressMessage {
8674 topic: message.topic.to_string(),
8675 payload: message.payload,
8676 });
8677 }
8678
8679 fn close(&mut self) {
8680 self.closed.set(true);
8681 }
8682 }
8683
8684 struct CapturingBackingFactory {
8685 publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
8686 closed: Arc<AtomicBool>,
8687 rx: Mutex<Option<tokio::sync::mpsc::Receiver<BusMessage>>>,
8688 }
8689
8690 impl CapturingBackingFactory {
8691 fn new(
8692 publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
8693 closed: Arc<AtomicBool>,
8694 rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
8695 ) -> Self {
8696 Self {
8697 publications,
8698 closed,
8699 rx: Mutex::new(rx),
8700 }
8701 }
8702 }
8703
8704 impl Debug for CapturingBackingFactory {
8705 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8706 f.debug_struct(stringify!(CapturingBackingFactory))
8707 .finish_non_exhaustive()
8708 }
8709 }
8710
8711 impl MessageBusBackingFactory for CapturingBackingFactory {
8712 fn create(
8713 &self,
8714 _trader_id: TraderId,
8715 _instance_id: UUID4,
8716 _config: MessageBusConfig,
8717 ) -> anyhow::Result<Box<dyn MessageBusBacking>> {
8718 let rx = self.rx.lock().take();
8719 Ok(Box::new(CapturingBacking {
8720 publications: self.publications.clone(),
8721 closed: self.closed.clone(),
8722 rx,
8723 }))
8724 }
8725 }
8726
8727 struct CapturingBacking {
8728 publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
8729 closed: Arc<AtomicBool>,
8730 rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
8731 }
8732
8733 impl MessageBusBacking for CapturingBacking {
8734 fn is_closed(&self) -> bool {
8735 self.closed.load(Ordering::Relaxed)
8736 }
8737
8738 fn publish(&self, message: BusMessage) {
8739 self.publications.lock().push(CapturedEgressMessage {
8740 topic: message.topic.to_string(),
8741 payload: message.payload,
8742 });
8743 }
8744
8745 fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
8746 self.rx
8747 .take()
8748 .ok_or_else(|| anyhow::anyhow!("external ingress receiver unavailable"))
8749 }
8750
8751 fn close(&mut self) {
8752 self.closed.store(true, Ordering::Relaxed);
8753 }
8754 }
8755}