Skip to main content

nautilus_live/node/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Live trading node built on a single-threaded tokio event loop.
17//!
18//! The node owns system lifecycle and the event loop. Its reconciliation module schedules checks,
19//! manages report futures and deadlines, and dispatches results. The execution manager owns
20//! reconciliation state, discrepancy decisions, and individual reconciliation operations.
21//!
22//! `LiveNode::run()` drives the system through a `tokio::select!` loop that
23//! multiplexes data events, execution events, trading commands, timers, and
24//! periodic maintenance tasks (reconciliation, purge, prune, audit).
25//!
26//! # Threading model
27//!
28//! The core types (`ExecutionManager`, `ExecutionEngine`, `Cache`) use
29//! `Rc<RefCell<..>>` and are `!Send`. All access happens on the same thread.
30//! Pending report futures can retain client borrows while other select branches run.
31//! The client facade defers instrument updates until those borrows are released;
32//! completion and cancellation paths flush the deferred updates. Single-threaded
33//! execution does not by itself prevent conflicting borrows or reentrant callbacks.
34//!
35//! # Startup sequencing
36//!
37//! Startup connects clients in two phases so that instruments are in the
38//! cache before execution clients read them:
39//!
40//! 1. Connect data clients (instruments arrive as buffered `DataEvent`s).
41//! 2. Flush all pending data events and commands into the cache via
42//!    `flush_pending_data`, which loops `try_recv` on the channel receivers
43//!    until no items remain.
44//! 3. Connect execution clients (`load_instruments_from_cache` now finds
45//!    populated instruments).
46//! 4. Drain remaining events, then run reconciliation.
47//!
48//! Both `run()` (integrated event loop) and `start()` (manual lifecycle)
49//! follow this sequence.
50//!
51//! # Reconciliation
52//!
53//! Continuous inflight, open-order, and position checks run on independent intervals. The
54//! shared maintenance timer in the select loop dispatches reconciliation at
55//! the minimum enabled interval. Each dispatch the handler checks which
56//! sub-checks are due based on elapsed nanoseconds and schedules their work.
57//! Continuous checks do not await venue HTTP in the select loop: open-order
58//! and position checks poll bulk venue report futures from the loop.
59//!
60//! # Maintenance dispatcher
61//!
62//! Six periodic tasks share a single coarse `maintenance_timer`:
63//!
64//! - reconciliation (inflight, open, position sub-checks)
65//! - purge closed orders
66//! - purge closed positions
67//! - purge account events
68//! - own-books audit
69//! - recent-fills cache prune
70//!
71//! The runner wakes one timer per loop iteration regardless of how many
72//! maintenance tasks are configured. Each task tracks its own
73//! `next_fire: Instant` and the dispatcher fires the bodies whose deadline
74//! has passed, rescheduling `next = now + interval` (equivalent to
75//! `MissedTickBehavior::Delay`). Disabled tasks anchor on a far-future
76//! `next` that never trips.
77//!
78//! The 100ms timer cadence is the effective floor for any maintenance
79//! interval. Configured intervals below 100ms (the config types allow
80//! `inflight_check_interval_ms` and `own_books_audit_interval_secs` smaller)
81//! become eligible on the next maintenance tick. Event processing and runtime
82//! scheduling can delay dispatch further; the timer does not guarantee a maximum delay.
83
84use 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
162/// Dispatches the run loop performs before yielding to the executor.
163///
164/// A saturated channel keeps every select branch ready, so the loop would otherwise never return
165/// `Pending`. Under a host event loop that starves the adapter I/O tasks feeding those channels,
166/// which shows up as lapsed heartbeats and reconnects rather than as backpressure.
167const 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/// High-level abstraction for a live Nautilus system node.
180///
181/// Provides a simplified interface for running live systems
182/// with automatic client management and lifecycle handling.
183#[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    /// Creates a new `LiveNode` from builder components.
202    ///
203    /// This is an internal constructor used by `LiveNodeBuilder`.
204    #[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    /// Creates a new [`LiveNodeBuilder`] for fluent configuration.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if the environment is invalid for live trading.
241    pub fn builder(
242        trader_id: TraderId,
243        environment: Environment,
244    ) -> anyhow::Result<LiveNodeBuilder> {
245        LiveNodeBuilder::new(trader_id, environment)
246    }
247
248    /// Creates a new [`LiveNode`] directly from a kernel name and optional configuration.
249    ///
250    /// This is a convenience method for creating a live node with a pre-configured
251    /// kernel configuration, bypassing the builder pattern. If no config is provided,
252    /// a default configuration will be used.
253    ///
254    /// # Errors
255    ///
256    /// Returns an error if kernel construction fails.
257    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    /// Loads and registers plug-ins declared on the node config.
320    ///
321    /// # Errors
322    ///
323    /// Returns an error when plug-ins are configured without host-side support.
324    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    /// Loads and registers one plug-in instance.
335    ///
336    /// # Errors
337    ///
338    /// Returns an error because dynamic plug-in hosting lives in the host-side integration.
339    #[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    /// Returns a thread-safe handle to control this node.
355    #[must_use]
356    pub fn handle(&self) -> LiveNodeHandle {
357        self.handle.clone()
358    }
359
360    /// Adds a callback for supported typed external messages.
361    ///
362    /// While [`run`](Self::run) or [`run_with_mode`](Self::run_with_mode) services external ingress,
363    /// the node invokes processors in registration order before normal inbound streaming filters
364    /// for JSON or MessagePack payloads when [`msgbus::BusPayloadType::is_typed_message`] returns
365    /// `true`. Other encodings are skipped with a warning. Each callback receives the decoded
366    /// concrete value as [`Any`], allowing it to downcast to the concrete payload type. External
367    /// egress is suppressed while the processors run, so synchronous publications remain local.
368    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    /// Starts the live node without entering a select loop.
387    ///
388    /// Connects clients, runs reconciliation, and starts the trader, but does
389    /// not consume the runner or drive channel receivers, so channel traffic arriving after
390    /// startup is never serviced. This is a building block for tests and embedding, not a
391    /// lifecycle: use [`run`](Self::run) or [`run_with_mode`](Self::run_with_mode) to run a node.
392    ///
393    /// # Errors
394    ///
395    /// Returns an error if startup fails.
396    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        // Connect data clients first and flush instrument events into cache
439        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    /// Stop the live node.
524    ///
525    /// This method stops the trader, waits for the configured grace period to allow
526    /// residual events to be processed, then finalizes the shutdown sequence.
527    ///
528    /// # Errors
529    ///
530    /// Returns an error if shutdown fails.
531    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    /// Disposes the live node kernel and releases resources.
570    ///
571    /// Discards any retained runner messages and attempts callback cleanup. Logs latched callback
572    /// failures and cleanup rejection; externally retained work can prevent clearing.
573    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    /// Awaits engine clients to connect with timeout.
748    ///
749    /// Returns the final connection wait status.
750    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    /// Awaits engine clients to disconnect with timeout.
790    ///
791    /// Returns an error with client status on timeout.
792    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    /// Performs startup reconciliation to align internal state with venue state.
861    ///
862    /// This method queries each execution client for mass status (orders, fills, positions)
863    /// and reconciles any discrepancies with the local cache state.
864    ///
865    /// # Errors
866    ///
867    /// Returns an error if reconciliation fails or times out.
868    #[expect(clippy::await_holding_refcell_ref)] // Single-threaded runtime, intentional design
869    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                    // Register external orders with execution clients for tracking
967                    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    /// Run the live node with automatic shutdown handling.
1017    ///
1018    /// This method starts the node, runs indefinitely, and handles graceful shutdown
1019    /// on interrupt signals.
1020    ///
1021    /// # Thread Safety
1022    ///
1023    /// The event loop runs directly on the current thread (not spawned) because the
1024    /// msgbus uses thread-local storage. Endpoints registered by the kernel are only
1025    /// accessible from the same thread.
1026    ///
1027    /// # Shutdown Sequence
1028    ///
1029    /// 1. Signal received (SIGINT, SIGTERM, or handle stop).
1030    /// 2. Trader components stopped (triggers order cancellations, etc.).
1031    /// 3. Event loop continues processing residual events for the configured grace period.
1032    /// 4. Kernel finalized, clients disconnected, remaining events drained.
1033    ///
1034    /// # Errors
1035    ///
1036    /// Returns an error if the node fails to start or encounters a runtime error.
1037    pub async fn run(&mut self) -> anyhow::Result<()> {
1038        self.run_with_mode(NodeRunMode::Owned).await
1039    }
1040
1041    /// Run the live node under the given mode.
1042    ///
1043    /// [`NodeRunMode::Hosted`] leaves signal handling to the host application. Every other
1044    /// responsibility, including maintenance, reconciliation, external ingress, and the shutdown
1045    /// sequence, is identical across modes so that hosted and owned nodes cannot diverge.
1046    ///
1047    /// # Errors
1048    ///
1049    /// Returns an error if the node fails to start or encounters a runtime error.
1050    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        // Startup phase 1: Connect data clients and drain instrument events into cache.
1135        // This ensures the cache is populated before execution clients connect.
1136        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 any data events still queued in the channel receivers that the
1177        // select loop did not capture before the connect future resolved, then
1178        // drain everything into cache.
1179        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        // Startup phase 2: Connect execution clients (instruments now in cache)
1188        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 channel receivers and drain all remaining pending events
1202        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        // Run reconciliation now that instruments are in cache and start trader
1280        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) // Unused, timer won't fire
1423        };
1424
1425        // `reconciliation_startup_delay_secs` is a post-reconciliation grace period:
1426        // startup reconciliation has already completed above, and this delay offsets
1427        // the first periodic tick to let the system stabilize before continuous checks
1428        // begin.
1429        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        // Per-task `(interval, next_fire)` schedules dispatched by the
1442        // shared `maintenance_timer` below. See module docs for rationale.
1443        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        // Stop-check timer is not subject to the reconciliation startup delay,
1491        // so shutdown signals remain responsive from the moment the node reaches
1492        // `Running`. Set `MissedTickBehavior::Skip` so backlog ticks do not fire
1493        // a burst after the select arm was suspended by other branches.
1494        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        // Running phase: runs until shutdown deadline expires
1498        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        // A hosted node never installs signal handlers, so these futures stay pending and their
1504        // listeners are never registered. Both arms resolve to the same type as the real listeners.
1505        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                // Signal branches first so they are always checked
1561                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 dispatcher (before event processing to avoid
1716                // starvation). See module docs for design rationale.
1717                _ = 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                // Event processing branches. Exec commands and events are
1793                // ordered ahead of data events so a strategy action (cancel,
1794                // submit, etc.) is not delayed behind a market data backlog
1795                // when the biased select polls receivers each iteration.
1796                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        // Handle events that arrived during finalize_stop
1946        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    /// Returns whether a cache database backing is configured but not yet installed.
2017    #[must_use]
2018    pub const fn has_pending_cache_database(&self) -> bool {
2019        self.cache_database_factory.is_some()
2020    }
2021
2022    /// Constructs the configured cache database backing and installs it on the kernel cache.
2023    ///
2024    /// Construction is deferred to startup so the adapter is built inside the async runtime rather
2025    /// than by blocking the synchronous builder, and so the connection opens only when the node runs.
2026    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        // Cleared only after a successful install, so a failed startup can be retried rather than
2034        // silently starting without the backing the caller asked for.
2035        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    /// Dispatches a normal-ingress execution event, then commits a direct
2152    /// `OrderFilled` to the recent-fills dedup cache only once it is present on
2153    /// its canonical order.
2154    ///
2155    /// The fill candidate is captured from `&evt` BEFORE the value is moved into
2156    /// [`AsyncRunner::handle_exec_event`]; the gated commit runs AFTER dispatch,
2157    /// so a fill the execution engine rejects (unknown order, invalid
2158    /// transition) is never marked and its later `Fill` report stays eligible.
2159    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        // A zero remaining budget still admits an immediately-ready connect (an
2174        // empty/ready client set completes on the first poll); a pending connect
2175        // fails closed on that same poll. This keeps a zero `timeout_connection`
2176        // - a supported "do not wait, but allow ready work" configuration -
2177        // working, while still bounding a hung connect.
2178        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    /// Connects execution clients and checks all engines are connected.
2192    ///
2193    /// Returns the final connection wait status.
2194    /// Must be called after data clients are connected and instrument events drained.
2195    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    /// Gets the node's environment.
2636    #[must_use]
2637    pub fn environment(&self) -> Environment {
2638        self.kernel.environment()
2639    }
2640
2641    /// Gets a reference to the underlying kernel.
2642    #[must_use]
2643    pub const fn kernel(&self) -> &NautilusKernel {
2644        &self.kernel
2645    }
2646
2647    /// Gets an exclusive reference to the underlying kernel.
2648    #[must_use]
2649    pub const fn kernel_mut(&mut self) -> &mut NautilusKernel {
2650        &mut self.kernel
2651    }
2652
2653    /// Gets the node's trader ID.
2654    #[must_use]
2655    pub fn trader_id(&self) -> TraderId {
2656        self.kernel.trader_id()
2657    }
2658
2659    /// Gets the node's instance ID.
2660    #[must_use]
2661    pub const fn instance_id(&self) -> UUID4 {
2662        self.kernel.instance_id()
2663    }
2664
2665    /// Returns the current node state.
2666    #[must_use]
2667    pub fn state(&self) -> NodeState {
2668        self.handle.state()
2669    }
2670
2671    /// Checks if the live node is currently running.
2672    #[must_use]
2673    pub fn is_running(&self) -> bool {
2674        self.state().is_running()
2675    }
2676
2677    /// Sets the cache database adapter for persistence.
2678    ///
2679    /// This allows setting a database adapter (e.g., PostgreSQL, Redis) after the node
2680    /// is built but before it starts running. The database adapter is used to persist
2681    /// cache data for recovery and state management.
2682    ///
2683    /// # Errors
2684    ///
2685    /// Returns an error if the node is already running.
2686    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    /// Returns the execution manager.
2701    #[must_use]
2702    pub fn exec_manager(&self) -> &ExecutionManager {
2703        &self.exec_manager
2704    }
2705
2706    /// Returns a mutable reference to the execution manager.
2707    #[must_use]
2708    pub fn exec_manager_mut(&mut self) -> &mut ExecutionManager {
2709        &mut self.exec_manager
2710    }
2711
2712    /// Adds an actor to the trader.
2713    ///
2714    /// This method provides a high-level interface for adding actors to the underlying
2715    /// trader without requiring direct access to the kernel. Actors should be added
2716    /// after the node is built but before starting the node.
2717    ///
2718    /// # Errors
2719    ///
2720    /// Returns an error if:
2721    /// - The trader is not in a valid state for adding components.
2722    /// - An actor with the same ID is already registered.
2723    /// - The node is currently running.
2724    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    /// Adds an actor to the live node using a factory function.
2738    ///
2739    /// The factory function is called at registration time to create the actor,
2740    /// avoiding cloning issues with non-cloneable actor types.
2741    ///
2742    /// # Errors
2743    ///
2744    /// Returns an error if:
2745    /// - The node is currently running.
2746    /// - The factory function fails to create the actor.
2747    /// - The underlying trader registration fails.
2748    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    /// Adds a strategy to the trader.
2766    ///
2767    /// Strategies are registered in both the component registry (for lifecycle management)
2768    /// and the actor registry (for data callbacks via msgbus).
2769    ///
2770    /// # Errors
2771    ///
2772    /// Returns an error if:
2773    /// - The node is currently running.
2774    /// - A strategy with the same ID is already registered.
2775    /// - The configured external order instrument IDs repeat an instrument or the cache already
2776    ///   contains a requested claim.
2777    /// - The strategy configures one or more external order instrument IDs and the cache is already
2778    ///   borrowed.
2779    /// - The strategy configures an OMS type override and the execution engine is already borrowed.
2780    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        // Capture strategy-owned values before adding the strategy, which moves it
2791        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    /// Registers external order claims in the shared cache.
2852    ///
2853    /// It can be called while the node is idle, after manual [`start`](Self::start) returns, or
2854    /// after the node stops. A running strategy can update its own claims through
2855    /// `Strategy::set_external_order_instrument_ids`.
2856    ///
2857    /// # Errors
2858    ///
2859    /// Returns an error without changing the cache if the cache is already borrowed, the request
2860    /// repeats an instrument, or any requested instrument already has a claim.
2861    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    /// Deregisters all external order claims owned by `strategy_id` from the shared cache.
2880    ///
2881    /// The operation is synchronous and can be called while the node is idle, after manual
2882    /// [`start`](Self::start) returns, or after the node stops. It cannot be called while
2883    /// [`run`](Self::run) or [`run_with_mode`](Self::run_with_mode) owns the node.
2884    ///
2885    /// # Errors
2886    ///
2887    /// Returns an error if the cache is already borrowed.
2888    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    /// Adds an execution algorithm to the trader.
2917    ///
2918    /// Execution algorithms are registered in both the component registry (for lifecycle
2919    /// management) and the actor registry (for data callbacks via msgbus).
2920    ///
2921    /// # Errors
2922    ///
2923    /// Returns an error if:
2924    /// - The node is currently running.
2925    /// - An execution algorithm with the same ID is already registered.
2926    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    // Runs reconciliation sub-checks, each gated by its own interval.
2943    // Continuous checks only schedule venue work; they do not await venue I/O
2944    // in the event loop.
2945}
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
3017/// Flushes data events and commands from both `pending` and the channel receivers
3018/// into the cache, looping until no progress is made.
3019///
3020/// This closes the gap where `drive_with_event_buffering` exits as soon as its
3021/// driven future resolves (biased select), leaving items in the channel receivers
3022/// that were not captured into `pending`.
3023fn 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/// Flushes all channel receivers into `pending`, then drains everything.
3048///
3049/// Unlike [`flush_pending_data`] this is a single pass, not a drain-until-quiet
3050/// loop. Sufficient for phase 2 where the goal is to capture items the biased
3051/// select did not poll before the connect future resolved.
3052#[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    // Flush channel receivers into pending
3067    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/// Drives a future to completion while buffering channel events.
3099///
3100/// Time events are handled immediately. Account events are forwarded directly.
3101/// All other events are buffered in `pending` for later processing.
3102#[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    /// Drains only data events and commands into the cache.
3174    ///
3175    /// Returns `true` if any events or commands were drained.
3176    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    /// Drains all remaining pending events.
3200    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            // Account events are safe to process immediately. Reports and orders need
3255            // ExecEngine access, which may conflict with the driven startup future's borrow.
3256            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, &registry);
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            // Model the production batch-cancel path: each child is accepted, then
5232            // moved to PendingCancel, so an inflight timeout must emit a Canceled
5233            // event (not a Submitted-order rejection).
5234            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        // load_state must be true: the kernel rejects event-store replay otherwise,
5566        // and LiveNodeConfig defaults it to false.
5567        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(), &quote);
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(), &quote);
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        // Stopping from `drive` rather than here, so the node cannot finish `run` before the
7412        // republished quote is observed.
7413        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(&quote).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(), &quote);
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(&quote).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        // Pre-load pending (items captured by the select loop)
7878        pending.data_evts.push((stub_data_event()).into());
7879        pending.data_cmds.push(stub_data_command().into());
7880
7881        // Pre-load channels (items missed by the select loop)
7882        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        // First pass: pending has an event, channel has a command
7903        pending.data_evts.push((stub_data_event()).into());
7904        cmd_tx.send(stub_data_command().into()).unwrap();
7905
7906        // Second pass: channel has items that simulate arrival during first drain
7907        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, // correlation_id
7995            )),
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        // Pre-load pending with data items
8045        pending.data_evts.push((stub_data_event()).into());
8046        pending.data_cmds.push(stub_data_command().into());
8047
8048        // Pre-load all channel types
8049        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        // Both order and report events are drained by pending.drain()
8166        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        // Account events are forwarded immediately, never buffered in pending
8204        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        // Batch should be unpacked into individual Submitted events then drained
8390        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        // Batch should be unpacked into individual Canceled events then drained
8429        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}