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//! `LiveNode::run()` drives the system through a `tokio::select!` loop that
19//! multiplexes data events, execution events, trading commands, timers, and
20//! periodic maintenance tasks (reconciliation, purge, prune, audit).
21//!
22//! # Threading model
23//!
24//! The core types (`ExecutionManager`, `ExecutionEngine`, `Cache`) use
25//! `Rc<RefCell<..>>` and are `!Send`. All access happens on the same thread.
26//! The `select!` macro runs one branch to completion (including inner awaits)
27//! before polling the next, so `RefCell` borrows held across `.await` points
28//! within a single branch cannot conflict with borrows in other branches.
29//!
30//! # Startup sequencing
31//!
32//! Startup connects clients in two phases so that instruments are in the
33//! cache before execution clients read them:
34//!
35//! 1. Connect data clients (instruments arrive as buffered `DataEvent`s).
36//! 2. Flush all pending data events and commands into the cache via
37//!    `flush_pending_data`, which loops `try_recv` on the channel receivers
38//!    until no items remain.
39//! 3. Connect execution clients (`load_instruments_from_cache` now finds
40//!    populated instruments).
41//! 4. Drain remaining events, then run reconciliation.
42//!
43//! Both `run()` (integrated event loop) and `start()` (manual lifecycle)
44//! follow this sequence.
45//!
46//! # Reconciliation
47//!
48//! Continuous inflight and open-order checks run on independent intervals. The
49//! shared maintenance timer in the select loop dispatches reconciliation at
50//! the minimum enabled interval. Each dispatch the handler checks which
51//! sub-checks are due based on elapsed nanoseconds and schedules their work.
52//! Continuous checks do not await venue HTTP in the select loop: open-order
53//! and position checks poll bulk venue report futures from the loop.
54//!
55//! # Maintenance dispatcher
56//!
57//! Six periodic tasks share a single coarse `maintenance_timer`:
58//!
59//! - reconciliation (inflight, open, position sub-checks)
60//! - purge closed orders
61//! - purge closed positions
62//! - purge account events
63//! - own-books audit
64//! - recent-fills cache prune
65//!
66//! The runner wakes one timer per loop iteration regardless of how many
67//! maintenance tasks are configured. Each task tracks its own
68//! `next_fire: Instant` and the dispatcher fires the bodies whose deadline
69//! has passed, rescheduling `next = now + interval` (equivalent to
70//! `MissedTickBehavior::Delay`). Disabled tasks anchor on a far-future
71//! `next` that never trips.
72//!
73//! The 100ms timer cadence is the effective floor for any maintenance
74//! interval. Configured intervals below 100ms (the config types allow
75//! `inflight_check_interval_ms` and `own_books_audit_interval_secs` smaller)
76//! get rounded up to the next tick. Real workloads do not run venue or cache
77//! maintenance below 100ms (defaults are seconds to minutes). Cadence drifts
78//! by at most one body duration per fire.
79
80use std::{fmt::Debug, future::Future, pin::Pin, time::Duration};
81
82use indexmap::IndexSet;
83use nautilus_common::{
84    actor::{Actor, DataActor, DataActorNative},
85    cache::database::CacheDatabaseAdapter,
86    component::Component,
87    enums::{Environment, LogColor},
88    live::dst,
89    log_info,
90    messages::{
91        DataEvent, ExecutionEvent, ExecutionReport,
92        data::DataCommand,
93        execution::{GenerateOrderStatusReports, GeneratePositionStatusReports, TradingCommand},
94    },
95    msgbus::{self, BusMessage},
96    timer::TimeEventHandler,
97};
98use nautilus_core::{
99    UUID4, UnixNanos,
100    datetime::{NANOSECONDS_IN_MILLISECOND, mins_to_secs, secs_to_nanos_unchecked},
101};
102use nautilus_model::{
103    events::OrderEventAny,
104    identifiers::{ClientOrderId, TraderId, Venue},
105    orders::Order,
106    reports::{OrderStatusReport, PositionStatusReport},
107};
108use nautilus_system::{config::NautilusKernelConfig, kernel::NautilusKernel};
109use nautilus_trading::{
110    ExecutionAlgorithm, ExecutionAlgorithmNative,
111    strategy::{Strategy, StrategyNative},
112};
113use tabled::{builder::Builder, settings::Style};
114
115use crate::{
116    execution::{
117        client::LiveExecutionClient,
118        manager::{
119            ExecutionManager, ExecutionManagerConfig, OpenOrderReportCheck, PositionReportCheck,
120        },
121    },
122    runner::{AsyncRunner, AsyncRunnerChannels},
123};
124
125pub mod builder;
126pub mod config;
127#[cfg(feature = "plugin")]
128pub mod plugin;
129mod state;
130
131use builder::ExternalMessageBusIngress;
132pub use builder::LiveNodeBuilder;
133use config::{LiveNodeConfig, PluginConfig};
134use state::EngineConnectionStatus;
135pub use state::{LiveNodeHandle, NodeState};
136
137/// High-level abstraction for a live Nautilus system node.
138///
139/// Provides a simplified interface for running live systems
140/// with automatic client management and lifecycle handling.
141#[derive(Debug)]
142#[cfg_attr(
143    feature = "python",
144    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.live", unsendable)
145)]
146#[cfg_attr(
147    feature = "python",
148    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
149)]
150pub struct LiveNode {
151    kernel: NautilusKernel,
152    runner: Option<AsyncRunner>,
153    config: LiveNodeConfig,
154    handle: LiveNodeHandle,
155    exec_manager: ExecutionManager,
156    exec_clients: Vec<LiveExecutionClient>,
157    external_msgbus: Option<ExternalMessageBusIngress>,
158    shutdown_deadline: Option<dst::time::Instant>,
159    #[cfg(feature = "plugin")]
160    plugins: plugin::NodePlugins,
161    #[cfg(feature = "python")]
162    #[allow(dead_code)] // TODO: Under development
163    python_actors: Vec<pyo3::Py<pyo3::PyAny>>,
164}
165
166impl LiveNode {
167    /// Creates a new `LiveNode` from builder components.
168    ///
169    /// This is an internal constructor used by `LiveNodeBuilder`.
170    #[must_use]
171    pub(crate) fn new_from_builder(
172        kernel: NautilusKernel,
173        runner: AsyncRunner,
174        config: LiveNodeConfig,
175        exec_manager: ExecutionManager,
176        exec_clients: Vec<LiveExecutionClient>,
177        external_msgbus: Option<ExternalMessageBusIngress>,
178    ) -> Self {
179        Self {
180            kernel,
181            runner: Some(runner),
182            config,
183            handle: LiveNodeHandle::new(),
184            exec_manager,
185            exec_clients,
186            external_msgbus,
187            shutdown_deadline: None,
188            #[cfg(feature = "plugin")]
189            plugins: plugin::NodePlugins,
190            #[cfg(feature = "python")]
191            python_actors: Vec::new(),
192        }
193    }
194
195    /// Creates a new [`LiveNodeBuilder`] for fluent configuration.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the environment is invalid for live trading.
200    pub fn builder(
201        trader_id: TraderId,
202        environment: Environment,
203    ) -> anyhow::Result<LiveNodeBuilder> {
204        LiveNodeBuilder::new(trader_id, environment)
205    }
206
207    /// Creates a new [`LiveNode`] directly from a kernel name and optional configuration.
208    ///
209    /// This is a convenience method for creating a live node with a pre-configured
210    /// kernel configuration, bypassing the builder pattern. If no config is provided,
211    /// a default configuration will be used.
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if kernel construction fails.
216    pub fn build(name: String, config: Option<LiveNodeConfig>) -> anyhow::Result<Self> {
217        let mut config = config.unwrap_or_default();
218        config.environment = Environment::Live;
219
220        match config.environment() {
221            Environment::Sandbox | Environment::Live => {}
222            Environment::Backtest => {
223                anyhow::bail!("LiveNode cannot be used with Backtest environment");
224            }
225        }
226
227        config.validate_runtime_support()?;
228
229        if config.event_store.is_some() {
230            anyhow::bail!(
231                "LiveNodeConfig.event_store is set but LiveNode::build cannot install a factory; \
232                 use LiveNodeBuilder::with_event_store(...) instead"
233            );
234        }
235
236        let runner = AsyncRunner::new();
237        runner.bind_senders();
238
239        let kernel = NautilusKernel::new(name, config.clone())?;
240
241        let exec_manager_config =
242            ExecutionManagerConfig::from(&config.exec_engine).with_trader_id(config.trader_id);
243        let exec_manager = ExecutionManager::new(
244            kernel.clock.clone(),
245            kernel.cache.clone(),
246            exec_manager_config,
247        );
248
249        let node = Self {
250            kernel,
251            runner: Some(runner),
252            config,
253            handle: LiveNodeHandle::new(),
254            exec_manager,
255            exec_clients: Vec::new(),
256            external_msgbus: None,
257            shutdown_deadline: None,
258            #[cfg(feature = "plugin")]
259            plugins: plugin::NodePlugins,
260            #[cfg(feature = "python")]
261            python_actors: Vec::new(),
262        };
263        node.load_configured_plugins()?;
264
265        log::info!("LiveNode built successfully with kernel config");
266
267        Ok(node)
268    }
269
270    /// Loads and registers plug-ins declared on the node config.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error when plug-ins are configured without host-side support.
275    #[cfg(feature = "plugin")]
276    pub(crate) fn load_configured_plugins(&self) -> anyhow::Result<()> {
277        if self.config.plugins.is_empty() {
278            return Ok(());
279        }
280
281        anyhow::bail!(
282            "LiveNodeConfig.plugins requires host-side plug-in support; nautilus-plugin is the guest SDK only"
283        )
284    }
285
286    /// Loads and registers plug-ins declared on the node config.
287    ///
288    /// # Errors
289    ///
290    /// Returns an error when plug-ins are configured without host-side support.
291    #[cfg(not(feature = "plugin"))]
292    pub(crate) fn load_configured_plugins(&self) -> anyhow::Result<()> {
293        if self.config.plugins.is_empty() {
294            return Ok(());
295        }
296
297        anyhow::bail!(
298            "LiveNodeConfig.plugins requires host-side plug-in support; nautilus-plugin is the guest SDK only"
299        )
300    }
301
302    /// Loads and registers one plug-in instance.
303    ///
304    /// # Errors
305    ///
306    /// Returns an error because dynamic plug-in hosting lives in the host-side integration.
307    #[cfg(feature = "plugin")]
308    #[expect(
309        clippy::needless_pass_by_value,
310        reason = "signature mirrors the host-enabled API"
311    )]
312    pub fn add_plugin(&mut self, config: PluginConfig) -> anyhow::Result<()> {
313        config.validate_runtime_support(self.config.plugins.len())?;
314
315        anyhow::bail!(
316            "LiveNode::add_plugin requires host-side plug-in support; nautilus-plugin is the guest SDK only"
317        )
318    }
319
320    /// Rejects plug-in registration when host support is not linked.
321    ///
322    /// # Errors
323    ///
324    /// Always returns an error explaining that host-side support is required.
325    #[cfg(not(feature = "plugin"))]
326    #[expect(
327        clippy::needless_pass_by_value,
328        reason = "signature mirrors the plugin-enabled API"
329    )]
330    pub fn add_plugin(&mut self, config: PluginConfig) -> anyhow::Result<()> {
331        let _ = config;
332        anyhow::bail!(
333            "LiveNode::add_plugin requires host-side plug-in support; nautilus-plugin is the guest SDK only"
334        )
335    }
336
337    /// Returns a thread-safe handle to control this node.
338    #[must_use]
339    pub fn handle(&self) -> LiveNodeHandle {
340        self.handle.clone()
341    }
342
343    /// Starts the live node without entering a select loop.
344    ///
345    /// Connects clients, runs reconciliation, and starts the trader, but does
346    /// not consume the runner or drive channel receivers. Channel traffic that
347    /// arrives after startup is not serviced until the caller provides a loop.
348    ///
349    /// For a self-contained entry point that owns the event loop, use [`run`](Self::run).
350    ///
351    /// # Errors
352    ///
353    /// Returns an error if startup fails.
354    pub async fn start(&mut self) -> anyhow::Result<()> {
355        if self.state().is_running() {
356            anyhow::bail!("Already running");
357        }
358
359        if let Some(runner) = self.runner.as_ref() {
360            runner.bind_senders();
361        }
362
363        self.handle.set_state(NodeState::Starting);
364
365        self.kernel.reset_shutdown_flag();
366        self.kernel.start_async().await;
367
368        if self.kernel.is_event_store_replay() {
369            log::info!(
370                "Event-store replay loaded; skipping live client connection and reconciliation",
371            );
372            self.handle.set_state(NodeState::Running);
373            return Ok(());
374        }
375
376        if self.kernel.is_event_store_replay_configured() {
377            self.abort_startup("Event-store replay did not start")
378                .await?;
379            return Ok(());
380        }
381
382        // Connect data clients first and flush instrument events into cache
383        self.kernel.connect_data_clients().await;
384
385        if let Some(runner) = self.runner.as_mut() {
386            runner.flush_pending_data();
387        }
388
389        self.kernel.connect_exec_clients().await;
390
391        if let Some(reason) = self.startup_abort_reason() {
392            self.abort_startup(reason).await?;
393            return Ok(());
394        }
395
396        match self.await_engines_connected().await {
397            EngineConnectionStatus::Connected => {}
398            EngineConnectionStatus::TimedOut => {
399                log::error!("Cannot start trader: engine client(s) not connected");
400                self.handle.set_state(NodeState::Running);
401                return Ok(());
402            }
403            EngineConnectionStatus::StopRequested => {
404                self.abort_startup("Stop signal received during startup")
405                    .await?;
406                return Ok(());
407            }
408            EngineConnectionStatus::ShutdownRequested => {
409                self.abort_startup("Shutdown signal received during startup")
410                    .await?;
411                return Ok(());
412            }
413        }
414
415        self.perform_startup_reconciliation().await?;
416
417        self.kernel.start_trader();
418        #[cfg(feature = "plugin")]
419        if let Err(e) = self.plugins.start_controllers() {
420            return self.abort_after_trader_start_failure(e).await;
421        }
422
423        self.handle.set_state(NodeState::Running);
424
425        Ok(())
426    }
427
428    /// Stop the live node.
429    ///
430    /// This method stops the trader, waits for the configured grace period to allow
431    /// residual events to be processed, then finalizes the shutdown sequence.
432    ///
433    /// # Errors
434    ///
435    /// Returns an error if shutdown fails.
436    pub async fn stop(&mut self) -> anyhow::Result<()> {
437        if !self.state().is_running() {
438            anyhow::bail!("Not running");
439        }
440
441        self.handle.set_state(NodeState::ShuttingDown);
442
443        #[cfg(feature = "plugin")]
444        let controller_stop_result = self.plugins.stop_controllers();
445        #[cfg(not(feature = "plugin"))]
446        let controller_stop_result: anyhow::Result<()> = Ok(());
447
448        self.kernel.stop_trader();
449        let delay = self.kernel.delay_post_stop();
450        log::info!("Awaiting residual events ({delay:?})...");
451
452        dst::time::sleep(delay).await;
453        let stop_result = self.finalize_stop().await;
454        match (controller_stop_result, stop_result) {
455            (Ok(()), Ok(())) => Ok(()),
456            (Err(controller_err), Ok(())) => Err(controller_err),
457            (Ok(()), Err(stop_err)) => Err(stop_err),
458            (Err(controller_err), Err(stop_err)) => {
459                log::error!("Error stopping plug-in controllers: {controller_err}");
460                Err(stop_err)
461            }
462        }
463    }
464
465    /// Awaits engine clients to connect with timeout.
466    ///
467    /// Returns the final connection wait status.
468    async fn await_engines_connected(&self) -> EngineConnectionStatus {
469        log::info!(
470            "Awaiting engine connections ({:?} timeout)...",
471            self.config.timeout_connection
472        );
473
474        let start = dst::time::Instant::now();
475        let timeout = self.config.timeout_connection;
476        let interval = Duration::from_millis(100);
477
478        while start.elapsed() < timeout {
479            if self.handle.should_stop() {
480                log::warn!("Stop signal received, aborting connection wait");
481                return EngineConnectionStatus::StopRequested;
482            }
483
484            if self.kernel.is_shutdown_requested() {
485                log::warn!("Shutdown signal received, aborting connection wait");
486                return EngineConnectionStatus::ShutdownRequested;
487            }
488
489            if self.kernel.check_engines_connected() {
490                log::info!("All engine clients connected");
491                return EngineConnectionStatus::Connected;
492            }
493            dst::time::sleep(interval).await;
494        }
495
496        self.log_connection_status();
497        EngineConnectionStatus::TimedOut
498    }
499
500    /// Awaits engine clients to disconnect with timeout.
501    ///
502    /// Logs an error with client status on timeout but does not fail.
503    async fn await_engines_disconnected(&self) {
504        log::info!(
505            "Awaiting engine disconnections ({:?} timeout)...",
506            self.config.timeout_disconnection
507        );
508
509        let start = dst::time::Instant::now();
510        let timeout = self.config.timeout_disconnection;
511        let interval = Duration::from_millis(100);
512
513        while start.elapsed() < timeout {
514            if self.kernel.check_engines_disconnected() {
515                log::info!("All engine clients disconnected");
516                return;
517            }
518            dst::time::sleep(interval).await;
519        }
520
521        log::error!(
522            "Timed out ({:?}) waiting for engines to disconnect\n\
523             DataEngine.check_disconnected() == {}\n\
524             ExecEngine.check_disconnected() == {}",
525            timeout,
526            self.kernel.data_engine().check_disconnected(),
527            self.kernel.exec_engine().borrow().check_disconnected(),
528        );
529    }
530
531    fn log_connection_status(&self) {
532        let data_status = self.kernel.data_client_connection_status();
533        let exec_status = self.kernel.exec_client_connection_status();
534
535        let mut rows: Vec<ClientStatus> = Vec::new();
536
537        for (client_id, connected) in data_status {
538            rows.push(ClientStatus {
539                client: client_id.to_string(),
540                client_type: "Data",
541                connected,
542            });
543        }
544
545        for (client_id, connected) in exec_status {
546            rows.push(ClientStatus {
547                client: client_id.to_string(),
548                client_type: "Execution",
549                connected,
550            });
551        }
552
553        let table = render_client_statuses(rows);
554
555        log::warn!(
556            "Timed out ({:?}) waiting for engines to connect\n\n{table}\n\n\
557             DataEngine.check_connected() == {}\n\
558             ExecEngine.check_connected() == {}",
559            self.config.timeout_connection,
560            self.kernel.data_engine().check_connected(),
561            self.kernel.exec_engine().borrow().check_connected(),
562        );
563    }
564
565    /// Performs startup reconciliation to align internal state with venue state.
566    ///
567    /// This method queries each execution client for mass status (orders, fills, positions)
568    /// and reconciles any discrepancies with the local cache state.
569    ///
570    /// # Errors
571    ///
572    /// Returns an error if reconciliation fails or times out.
573    #[expect(clippy::await_holding_refcell_ref)] // Single-threaded runtime, intentional design
574    async fn perform_startup_reconciliation(&mut self) -> anyhow::Result<()> {
575        if !self.config.exec_engine.reconciliation {
576            log::info!("Startup reconciliation disabled");
577            return Ok(());
578        }
579
580        log_info!(
581            "Starting execution state reconciliation...",
582            color = LogColor::Blue
583        );
584
585        let lookback_mins = self
586            .config
587            .exec_engine
588            .reconciliation_lookback_mins
589            .map(u64::from);
590
591        let timeout = self.config.timeout_reconciliation;
592        let start = dst::time::Instant::now();
593        let client_ids = self.kernel.exec_engine.borrow().client_ids();
594
595        for client_id in client_ids {
596            if start.elapsed() > timeout {
597                log::warn!("Reconciliation timeout reached, stopping early");
598                break;
599            }
600
601            log_info!(
602                "Requesting mass status from {}...",
603                client_id,
604                color = LogColor::Blue
605            );
606
607            let mass_status_result = self
608                .kernel
609                .exec_engine
610                .borrow_mut()
611                .generate_mass_status(&client_id, lookback_mins)
612                .await;
613
614            match mass_status_result {
615                Ok(Some(mass_status)) => {
616                    log_info!(
617                        "Reconciling ExecutionMassStatus for {}",
618                        client_id,
619                        color = LogColor::Blue
620                    );
621
622                    let exec_engine_rc = self.kernel.exec_engine.clone();
623
624                    let result = self
625                        .exec_manager
626                        .reconcile_execution_mass_status(mass_status, exec_engine_rc)
627                        .await;
628
629                    if result.events.is_empty() {
630                        log_info!(
631                            "Reconciliation for {} succeeded",
632                            client_id,
633                            color = LogColor::Blue
634                        );
635                    } else {
636                        log::info!(
637                            color = LogColor::Blue as u8;
638                            "Reconciliation for {} processed {} events",
639                            client_id,
640                            result.events.len()
641                        );
642                    }
643
644                    // Register external orders with execution clients for tracking
645                    if !result.external_orders.is_empty() {
646                        let exec_engine = self.kernel.exec_engine.borrow();
647                        for external in result.external_orders {
648                            exec_engine.register_external_order(
649                                external.client_order_id,
650                                external.venue_order_id,
651                                external.instrument_id,
652                                external.strategy_id,
653                                external.ts_init,
654                            );
655                        }
656                    }
657                }
658                Ok(None) => {
659                    log::warn!(
660                        "No mass status available from {client_id} \
661                         (likely adapter error when generating reports)"
662                    );
663                }
664                Err(e) => {
665                    log::warn!("Failed to get mass status from {client_id}: {e}");
666                }
667            }
668        }
669
670        self.kernel.portfolio.borrow_mut().initialize_orders();
671        self.kernel.portfolio.borrow_mut().initialize_positions();
672
673        let elapsed_secs = start.elapsed().as_secs_f64();
674        log_info!(
675            "Startup reconciliation completed in {:.2}s",
676            elapsed_secs,
677            color = LogColor::Blue
678        );
679
680        Ok(())
681    }
682
683    /// Run the live node with automatic shutdown handling.
684    ///
685    /// This method starts the node, runs indefinitely, and handles graceful shutdown
686    /// on interrupt signals.
687    ///
688    /// # Thread Safety
689    ///
690    /// The event loop runs directly on the current thread (not spawned) because the
691    /// msgbus uses thread-local storage. Endpoints registered by the kernel are only
692    /// accessible from the same thread.
693    ///
694    /// # Shutdown Sequence
695    ///
696    /// 1. Signal received (SIGINT, SIGTERM, or handle stop).
697    /// 2. Trader components stopped (triggers order cancellations, etc.).
698    /// 3. Event loop continues processing residual events for the configured grace period.
699    /// 4. Kernel finalized, clients disconnected, remaining events drained.
700    ///
701    /// # Errors
702    ///
703    /// Returns an error if the node fails to start or encounters a runtime error.
704    pub async fn run(&mut self) -> anyhow::Result<()> {
705        if self.state().is_running() {
706            anyhow::bail!("Already running");
707        }
708
709        let Some(runner) = self.runner.take() else {
710            anyhow::bail!("Runner already consumed - run() called twice");
711        };
712        runner.bind_senders();
713
714        let AsyncRunnerChannels {
715            mut time_evt_rx,
716            mut exec_evt_rx,
717            mut exec_cmd_rx,
718            mut data_evt_rx,
719            mut data_cmd_rx,
720        } = runner.take_channels();
721
722        log::info!("Event loop starting");
723
724        self.handle.set_state(NodeState::Starting);
725        self.kernel.reset_shutdown_flag();
726        self.kernel.start_async().await;
727
728        if self.kernel.is_event_store_replay() {
729            log::info!(
730                "Event-store replay loaded; skipping live client connection and reconciliation",
731            );
732            self.handle.set_state(NodeState::Running);
733            return Ok(());
734        }
735
736        if self.kernel.is_event_store_replay_configured() {
737            self.abort_startup("Event-store replay did not start")
738                .await?;
739            return Ok(());
740        }
741
742        let mut external_msgbus_rx = match self.take_external_ingress_receiver() {
743            Ok(rx) => rx,
744            Err(e) => {
745                let result = self
746                    .abort_startup("External message bus ingress failed to start")
747                    .await;
748                Self::drain_channels(
749                    &mut time_evt_rx,
750                    &mut data_evt_rx,
751                    &mut data_cmd_rx,
752                    &mut exec_evt_rx,
753                    &mut exec_cmd_rx,
754                );
755                log::info!("Event loop stopped");
756
757                if let Err(finalize_err) = result {
758                    anyhow::bail!(
759                        "failed to start external message bus ingress: {e}; failed to finalize startup abort: {finalize_err}"
760                    );
761                }
762
763                return Err(e);
764            }
765        };
766
767        let stop_handle = self.handle.clone();
768        let mut pending = PendingEvents::default();
769
770        // Startup phase 1: Connect data clients and drain instrument events into cache.
771        // This ensures the cache is populated before execution clients connect.
772        // TODO: Add ctrl_c, stop_handle, and shutdown monitoring here to
773        // allow aborting a hanging connect future.
774        drive_with_event_buffering(
775            self.kernel.connect_data_clients(),
776            &mut pending,
777            &mut time_evt_rx,
778            &mut data_evt_rx,
779            &mut data_cmd_rx,
780            &mut exec_evt_rx,
781            &mut exec_cmd_rx,
782        )
783        .await;
784
785        // Flush any data events still queued in the channel receivers that the
786        // select loop did not capture before the connect future resolved, then
787        // drain everything into cache.
788        flush_pending_data(&mut pending, &mut data_evt_rx, &mut data_cmd_rx);
789        debug_assert!(
790            pending.data_evts.is_empty() && pending.data_cmds.is_empty(),
791            "data must be drained into cache before exec clients connect",
792        );
793
794        // Startup phase 2: Connect execution clients (instruments now in cache)
795        let engine_connection_status = drive_with_event_buffering(
796            self.connect_exec_phase(),
797            &mut pending,
798            &mut time_evt_rx,
799            &mut data_evt_rx,
800            &mut data_cmd_rx,
801            &mut exec_evt_rx,
802            &mut exec_cmd_rx,
803        )
804        .await?;
805
806        // Flush channel receivers and drain all remaining pending events
807        flush_all_pending(
808            &mut pending,
809            &mut time_evt_rx,
810            &mut data_evt_rx,
811            &mut data_cmd_rx,
812            &mut exec_evt_rx,
813            &mut exec_cmd_rx,
814        );
815        debug_assert!(
816            pending.is_empty(),
817            "all startup events must be processed before reconciliation",
818        );
819
820        if let Some(reason) = engine_connection_status
821            .abort_reason()
822            .or_else(|| self.startup_abort_reason())
823        {
824            self.abort_startup(reason).await?;
825            Self::drain_channels(
826                &mut time_evt_rx,
827                &mut data_evt_rx,
828                &mut data_cmd_rx,
829                &mut exec_evt_rx,
830                &mut exec_cmd_rx,
831            );
832            log::info!("Event loop stopped");
833            return Ok(());
834        }
835
836        if engine_connection_status == EngineConnectionStatus::Connected {
837            // Run reconciliation now that instruments are in cache and start trader
838            self.perform_startup_reconciliation().await?;
839            self.kernel.start_trader();
840            #[cfg(feature = "plugin")]
841            if let Err(e) = self.plugins.start_controllers() {
842                let result = self.abort_after_trader_start_failure(e).await;
843                Self::drain_channels(
844                    &mut time_evt_rx,
845                    &mut data_evt_rx,
846                    &mut data_cmd_rx,
847                    &mut exec_evt_rx,
848                    &mut exec_cmd_rx,
849                );
850                log::info!("Event loop stopped");
851                return result;
852            }
853        } else {
854            log::error!("Not starting trader: engine client(s) not connected");
855        }
856
857        self.handle.set_state(NodeState::Running);
858
859        let exec_config = &self.config.exec_engine;
860        let inflight_interval_ns =
861            u64::from(exec_config.inflight_check_interval_ms) * NANOSECONDS_IN_MILLISECOND;
862        let open_interval_ns = exec_config
863            .open_check_interval_secs
864            .filter(|&s| s > 0.0)
865            .map_or(0, secs_to_nanos_unchecked);
866        let position_interval_ns = exec_config
867            .position_check_interval_secs
868            .filter(|&s| s > 0.0)
869            .map_or(0, secs_to_nanos_unchecked);
870        let has_clients = !self
871            .kernel
872            .exec_engine
873            .borrow()
874            .get_all_clients()
875            .is_empty();
876        let recon_enabled = has_clients
877            && (inflight_interval_ns > 0 || open_interval_ns > 0 || position_interval_ns > 0);
878
879        let recon_min_interval = if recon_enabled {
880            let mut intervals = Vec::new();
881
882            if exec_config.inflight_check_interval_ms > 0 {
883                intervals.push(Duration::from_millis(u64::from(
884                    exec_config.inflight_check_interval_ms,
885                )));
886            }
887
888            if let Some(s) = exec_config.open_check_interval_secs.filter(|&s| s > 0.0) {
889                intervals.push(Duration::from_secs_f64(s));
890            }
891
892            if let Some(s) = exec_config
893                .position_check_interval_secs
894                .filter(|&s| s > 0.0)
895            {
896                intervals.push(Duration::from_secs_f64(s));
897            }
898
899            intervals
900                .into_iter()
901                .min()
902                .unwrap_or(Duration::from_secs(1))
903        } else {
904            Duration::from_secs(1) // Unused, timer won't fire
905        };
906
907        // `reconciliation_startup_delay_secs` is a post-reconciliation grace period:
908        // startup reconciliation has already completed above, and this delay offsets
909        // the first periodic tick to let the system stabilize before continuous checks
910        // begin.
911        let startup_delay = if self.config.exec_engine.reconciliation {
912            Duration::from_secs_f64(exec_config.reconciliation_startup_delay_secs)
913        } else {
914            Duration::ZERO
915        };
916
917        let recon_start = dst::time::Instant::now() + startup_delay;
918
919        let mut ts_last_inflight = self.exec_manager.generate_timestamp_ns();
920        let mut ts_last_open = ts_last_inflight;
921        let mut ts_last_position = ts_last_inflight;
922
923        // Per-task `(interval, next_fire)` schedules dispatched by the
924        // shared `maintenance_timer` below. See module docs for rationale.
925        let far_future = Duration::from_hours(24 * 365 * 100);
926
927        let make_schedule = |opt_dur: Option<Duration>| -> (Duration, dst::time::Instant) {
928            let dur = opt_dur.unwrap_or(far_future);
929            (dur, recon_start + dur)
930        };
931
932        let (recon_interval, mut recon_next) = make_schedule(if recon_enabled {
933            Some(recon_min_interval)
934        } else {
935            None
936        });
937
938        let (purge_orders_interval, mut purge_orders_next) = make_schedule(
939            exec_config
940                .purge_closed_orders_interval_mins
941                .filter(|&m| m > 0)
942                .map(|m| Duration::from_secs(mins_to_secs(u64::from(m)))),
943        );
944
945        let (purge_positions_interval, mut purge_positions_next) = make_schedule(
946            exec_config
947                .purge_closed_positions_interval_mins
948                .filter(|&m| m > 0)
949                .map(|m| Duration::from_secs(mins_to_secs(u64::from(m)))),
950        );
951
952        let (purge_account_interval, mut purge_account_next) = make_schedule(
953            exec_config
954                .purge_account_events_interval_mins
955                .filter(|&m| m > 0)
956                .map(|m| Duration::from_secs(mins_to_secs(u64::from(m)))),
957        );
958
959        let (own_books_interval, mut own_books_next) = make_schedule(
960            exec_config
961                .own_books_audit_interval_secs
962                .filter(|&s| s > 0.0)
963                .map(Duration::from_secs_f64),
964        );
965
966        let (prune_fills_interval, mut prune_fills_next) =
967            make_schedule(Some(Duration::from_mins(1)));
968
969        let mut maintenance_timer = dst::time::interval(Duration::from_millis(100));
970        maintenance_timer.set_missed_tick_behavior(dst::time::MissedTickBehavior::Skip);
971
972        // Stop-check timer is not subject to the reconciliation startup delay,
973        // so shutdown signals remain responsive from the moment the node reaches
974        // `Running`. Set `MissedTickBehavior::Skip` so backlog ticks do not fire
975        // a burst after the select arm was suspended by other branches.
976        let mut stop_check_timer = dst::time::interval(Duration::from_millis(100));
977        stop_check_timer.set_missed_tick_behavior(dst::time::MissedTickBehavior::Skip);
978
979        // Running phase: runs until shutdown deadline expires
980        let mut residual_events = 0usize;
981        let mut open_order_report_task: Option<OpenOrderReportTask> = None;
982        let mut position_report_task: Option<PositionReportTask> = None;
983        let ctrl_c = dst::signal::ctrl_c();
984        let terminate = dst::signal::terminate();
985        tokio::pin!(ctrl_c);
986        tokio::pin!(terminate);
987
988        loop {
989            let shutdown_deadline = self.shutdown_deadline;
990            let is_shutting_down = self.state() == NodeState::ShuttingDown;
991            let is_running = self.state() == NodeState::Running;
992
993            tokio::select! {
994                biased;
995
996                // Signal branches first so they are always checked
997                result = &mut ctrl_c, if is_running => {
998                    match result {
999                        Ok(()) => log::info!("Received SIGINT, shutting down"),
1000                        Err(e) => log::error!("Failed to listen for SIGINT: {e}"),
1001                    }
1002                    self.initiate_shutdown();
1003                }
1004                result = &mut terminate, if is_running => {
1005                    match result {
1006                        Ok(()) => log::info!("Received SIGTERM, shutting down"),
1007                        Err(e) => log::error!("Failed to listen for SIGTERM: {e}"),
1008                    }
1009                    self.initiate_shutdown();
1010                }
1011                _ = stop_check_timer.tick(), if is_running => {
1012                    if stop_handle.should_stop() {
1013                        log::info!("Received stop signal from handle");
1014                        self.initiate_shutdown();
1015                    } else if self.kernel.is_shutdown_requested() {
1016                        log::info!("Received ShutdownSystem command, shutting down");
1017                        self.initiate_shutdown();
1018                    }
1019                }
1020                () = async {
1021                    match shutdown_deadline {
1022                        Some(deadline) => dst::time::sleep_until(deadline).await,
1023                        None => std::future::pending::<()>().await,
1024                    }
1025                }, if self.state() == NodeState::ShuttingDown => {
1026                    break;
1027                }
1028                result = async {
1029                    match open_order_report_task.as_mut() {
1030                        Some(task) => task.future.as_mut().await,
1031                        None => std::future::pending::<OpenOrderReportResult>().await,
1032                    }
1033                }, if open_order_report_task.is_some() => {
1034                    open_order_report_task = None;
1035                    let events = self
1036                        .exec_manager
1037                        .reconcile_open_order_reports(&result.check, result.reports);
1038                    self.process_reconciliation_events(&events);
1039                }
1040                result = async {
1041                    match position_report_task.as_mut() {
1042                        Some(task) => task.future.as_mut().await,
1043                        None => std::future::pending::<PositionReportResult>().await,
1044                    }
1045                }, if position_report_task.is_some() => {
1046                    position_report_task = None;
1047                    let events = self.exec_manager.reconcile_position_reports(
1048                        &result.check,
1049                        result.reports,
1050                        &result.failed_venues,
1051                    );
1052                    self.process_reconciliation_events(&events);
1053                }
1054
1055                // Maintenance dispatcher (before event processing to avoid
1056                // starvation). See module docs for design rationale.
1057                _ = maintenance_timer.tick(), if is_running => {
1058                    let mut now = dst::time::Instant::now();
1059
1060                    if recon_enabled && now >= recon_next {
1061                        let recon_intervals = ReconciliationCheckIntervals {
1062                            inflight: inflight_interval_ns,
1063                            open: open_interval_ns,
1064                            position: position_interval_ns,
1065                        };
1066                        let mut recon_state = ReconciliationCheckState {
1067                            ts_last_inflight: &mut ts_last_inflight,
1068                            ts_last_open: &mut ts_last_open,
1069                            ts_last_position: &mut ts_last_position,
1070                            open_order_report_task: &mut open_order_report_task,
1071                            position_report_task: &mut position_report_task,
1072                        };
1073
1074                        self.run_reconciliation_checks(
1075                            recon_intervals,
1076                            &mut recon_state,
1077                        );
1078
1079                        now = dst::time::Instant::now();
1080                        recon_next = now + recon_interval;
1081                    }
1082
1083                    if now >= purge_orders_next {
1084                        self.exec_manager.purge_closed_orders();
1085                        purge_orders_next = now + purge_orders_interval;
1086                    }
1087
1088                    if now >= purge_positions_next {
1089                        self.exec_manager.purge_closed_positions();
1090                        purge_positions_next = now + purge_positions_interval;
1091                    }
1092
1093                    if now >= purge_account_next {
1094                        self.exec_manager.purge_account_events();
1095                        purge_account_next = now + purge_account_interval;
1096                    }
1097
1098                    if now >= own_books_next {
1099                        self.kernel.cache().borrow_mut().audit_own_order_books();
1100                        own_books_next = now + own_books_interval;
1101                    }
1102
1103                    if now >= prune_fills_next {
1104                        self.exec_manager.prune_recent_fills_cache(60.0);
1105                        prune_fills_next = now + prune_fills_interval;
1106                    }
1107                }
1108
1109                // Event processing branches. Exec commands and events are
1110                // ordered ahead of data events so a strategy action (cancel,
1111                // submit, etc.) is not delayed behind a market data backlog
1112                // when the biased select polls receivers each iteration.
1113                Some(handler) = time_evt_rx.recv() => {
1114                    AsyncRunner::handle_time_event(handler);
1115
1116                    if is_shutting_down {
1117                        log::debug!("Residual time event");
1118                        residual_events += 1;
1119                    }
1120                }
1121                Some(evt) = exec_evt_rx.recv() => {
1122                    if is_shutting_down {
1123                        log::debug!("Residual exec event: {evt:?}");
1124                        residual_events += 1;
1125                    }
1126
1127                    let mut close_ids: Vec<ClientOrderId> = Vec::new();
1128
1129                    match &evt {
1130                        ExecutionEvent::Order(order_evt) => {
1131                            self.exec_manager.record_local_activity(order_evt.client_order_id());
1132                            match order_evt {
1133                                OrderEventAny::Filled(fill) => {
1134                                    self.exec_manager.record_position_activity(
1135                                        fill.instrument_id,
1136                                        fill.account_id,
1137                                        fill.ts_event,
1138                                    );
1139                                    self.exec_manager.mark_fill_processed(fill.trade_id);
1140                                }
1141                                OrderEventAny::Accepted(_)
1142                                | OrderEventAny::Rejected(_)
1143                                | OrderEventAny::Canceled(_)
1144                                | OrderEventAny::Expired(_)
1145                                | OrderEventAny::Denied(_)
1146                                | OrderEventAny::Updated(_)
1147                                | OrderEventAny::ModifyRejected(_)
1148                                | OrderEventAny::CancelRejected(_) => {
1149                                    self.exec_manager.clear_recon_tracking(
1150                                        &order_evt.client_order_id(), true,
1151                                    );
1152                                }
1153                                _ => {}
1154                            }
1155                            close_ids.push(order_evt.client_order_id());
1156                        }
1157                        ExecutionEvent::OrderSubmittedBatch(batch) => {
1158                            for submitted in &batch.events {
1159                                self.exec_manager.record_local_activity(submitted.client_order_id);
1160                            }
1161                        }
1162                        ExecutionEvent::OrderAcceptedBatch(batch) => {
1163                            for accepted in &batch.events {
1164                                self.exec_manager.record_local_activity(accepted.client_order_id);
1165                                self.exec_manager.clear_recon_tracking(
1166                                    &accepted.client_order_id, true,
1167                                );
1168                            }
1169                        }
1170                        ExecutionEvent::OrderCanceledBatch(batch) => {
1171                            for canceled in &batch.events {
1172                                self.exec_manager.record_local_activity(canceled.client_order_id);
1173                                self.exec_manager.clear_recon_tracking(
1174                                    &canceled.client_order_id, true,
1175                                );
1176                                close_ids.push(canceled.client_order_id);
1177                            }
1178                        }
1179                        ExecutionEvent::Report(report) => {
1180                            if let ExecutionReport::Fill(fill_report) = report
1181                                && self.exec_manager.is_fill_recently_processed(&fill_report.trade_id) {
1182                                    log::debug!(
1183                                        "Skipping recently processed fill report: {}",
1184                                        fill_report.trade_id,
1185                                    );
1186                                    continue;
1187                            }
1188                            self.exec_manager.observe_execution_report(report);
1189                        }
1190                        ExecutionEvent::Account(_) => {}
1191                    }
1192
1193                    AsyncRunner::handle_exec_event(evt);
1194
1195                    // Post-dispatch: clear tracking when order closes
1196                    for coid in &close_ids {
1197                        let is_closed = self.kernel.cache().borrow()
1198                            .order(coid).is_some_and(|o| o.is_closed());
1199                        if is_closed {
1200                            self.exec_manager.clear_recon_tracking(coid, true);
1201                        }
1202                    }
1203                }
1204                Some(cmd) = exec_cmd_rx.recv() => {
1205                    if is_shutting_down {
1206                        log::debug!("Residual exec command: {cmd:?}");
1207                        residual_events += 1;
1208                    }
1209
1210                    match &cmd {
1211                        TradingCommand::SubmitOrder(submit) => {
1212                            self.exec_manager.register_inflight(submit.client_order_id);
1213                        }
1214                        TradingCommand::SubmitOrderList(submit) => {
1215                            for order_init in &submit.order_inits {
1216                                self.exec_manager.register_inflight(order_init.client_order_id);
1217                            }
1218                        }
1219                        TradingCommand::ModifyOrder(modify) => {
1220                            self.exec_manager.register_inflight(modify.client_order_id);
1221                        }
1222                        TradingCommand::ModifyOrders(modify) => {
1223                            for child in &modify.modifies {
1224                                self.exec_manager.register_inflight(child.client_order_id);
1225                            }
1226                        }
1227                        TradingCommand::CancelOrder(cancel) => {
1228                            self.exec_manager.register_inflight(cancel.client_order_id);
1229                        }
1230                        _ => {}
1231                    }
1232                    AsyncRunner::handle_exec_command(cmd);
1233                }
1234                message = recv_external_msgbus_message(&mut external_msgbus_rx) => {
1235                    match message {
1236                        Some(message) => {
1237                            if is_shutting_down {
1238                                log::debug!("Residual external message bus message: {message}");
1239                                residual_events += 1;
1240                            }
1241                            Self::republish_external_msgbus_message(&message);
1242                        }
1243                        None => {
1244                            log::info!("External message bus ingress closed");
1245                            external_msgbus_rx = None;
1246                            self.close_external_ingress();
1247                        }
1248                    }
1249                }
1250                Some(evt) = data_evt_rx.recv() => {
1251                    if is_shutting_down {
1252                        log::debug!("Residual data event: {evt:?}");
1253                        residual_events += 1;
1254                    }
1255                    AsyncRunner::handle_data_event(evt);
1256                }
1257                Some(cmd) = data_cmd_rx.recv() => {
1258                    if is_shutting_down {
1259                        log::debug!("Residual data command: {cmd:?}");
1260                        residual_events += 1;
1261                    }
1262                    AsyncRunner::handle_data_command(cmd);
1263                }
1264            }
1265        }
1266
1267        if residual_events > 0 {
1268            log::debug!("Processed {residual_events} residual events during shutdown");
1269        }
1270
1271        drop(open_order_report_task.take());
1272        drop(position_report_task.take());
1273        drop(external_msgbus_rx.take());
1274        let _ = self.kernel.cache().borrow().check_residuals();
1275
1276        self.finalize_stop().await?;
1277
1278        // Handle events that arrived during finalize_stop
1279        Self::drain_channels(
1280            &mut time_evt_rx,
1281            &mut data_evt_rx,
1282            &mut data_cmd_rx,
1283            &mut exec_evt_rx,
1284            &mut exec_cmd_rx,
1285        );
1286
1287        log::info!("Event loop stopped");
1288
1289        Ok(())
1290    }
1291
1292    fn take_external_ingress_receiver(
1293        &mut self,
1294    ) -> anyhow::Result<Option<tokio::sync::mpsc::Receiver<BusMessage>>> {
1295        let Some(external_ingress) = self.external_msgbus.as_mut() else {
1296            return Ok(None);
1297        };
1298
1299        let receiver = external_ingress.take_receiver()?;
1300        log::info!("External message bus ingress started");
1301        Ok(Some(receiver))
1302    }
1303
1304    fn republish_external_msgbus_message(message: &BusMessage) {
1305        if let Err(e) = msgbus::republish_external_message(message) {
1306            log::error!("Failed to republish external message bus message: {e}");
1307        }
1308    }
1309
1310    fn close_external_ingress(&mut self) {
1311        if let Some(external_ingress) = self.external_msgbus.as_mut()
1312            && !external_ingress.is_closed()
1313        {
1314            external_ingress.close();
1315        }
1316    }
1317
1318    fn process_reconciliation_events(&mut self, events: &[OrderEventAny]) {
1319        if events.is_empty() {
1320            return;
1321        }
1322
1323        log::info!(
1324            "Processing {} reconciliation event{}",
1325            events.len(),
1326            if events.len() == 1 { "" } else { "s" }
1327        );
1328
1329        for event in events {
1330            self.exec_manager
1331                .record_local_activity(event.client_order_id());
1332            if let OrderEventAny::Filled(fill) = event {
1333                self.exec_manager.record_position_activity(
1334                    fill.instrument_id,
1335                    fill.account_id,
1336                    fill.ts_event,
1337                );
1338                self.exec_manager.mark_fill_processed(fill.trade_id);
1339            }
1340            self.kernel.exec_engine.borrow_mut().process(event);
1341        }
1342    }
1343
1344    /// Connects execution clients and checks all engines are connected.
1345    ///
1346    /// Returns the final connection wait status.
1347    /// Must be called after data clients are connected and instrument events drained.
1348    async fn connect_exec_phase(&mut self) -> anyhow::Result<EngineConnectionStatus> {
1349        self.kernel.connect_exec_clients().await;
1350        Ok(self.await_engines_connected().await)
1351    }
1352
1353    fn startup_abort_reason(&self) -> Option<&'static str> {
1354        if self.handle.should_stop() {
1355            Some("Stop signal received during startup")
1356        } else if self.kernel.is_shutdown_requested() {
1357            Some("Shutdown signal received during startup")
1358        } else {
1359            None
1360        }
1361    }
1362
1363    async fn abort_startup(&mut self, reason: &str) -> anyhow::Result<()> {
1364        log::info!("{reason}, aborting startup");
1365        self.handle.set_state(NodeState::ShuttingDown);
1366        self.finalize_stop().await
1367    }
1368
1369    #[cfg(feature = "plugin")]
1370    async fn abort_after_trader_start_failure(
1371        &mut self,
1372        start_err: anyhow::Error,
1373    ) -> anyhow::Result<()> {
1374        log::info!("Plug-in controller startup failed, aborting startup");
1375        self.handle.set_state(NodeState::ShuttingDown);
1376        self.kernel.stop_trader();
1377
1378        if let Err(finalize_err) = self.finalize_stop().await {
1379            anyhow::bail!(
1380                "failed to start plug-in controller: {start_err}; failed to finalize startup abort: {finalize_err}"
1381            );
1382        }
1383        Err(start_err)
1384    }
1385
1386    fn initiate_shutdown(&mut self) {
1387        #[cfg(feature = "plugin")]
1388        if let Err(e) = self.plugins.stop_controllers() {
1389            log::error!("Error stopping plug-in controllers: {e}");
1390        }
1391        self.kernel.stop_trader();
1392        let delay = self.kernel.delay_post_stop();
1393        log::info!("Awaiting residual events ({delay:?})...");
1394
1395        self.shutdown_deadline = Some(dst::time::Instant::now() + delay);
1396        self.handle.set_state(NodeState::ShuttingDown);
1397    }
1398
1399    async fn finalize_stop(&mut self) -> anyhow::Result<()> {
1400        self.close_external_ingress();
1401
1402        let disconnect_result = self.kernel.disconnect_clients().await;
1403        if let Err(ref e) = disconnect_result {
1404            log::error!("Error disconnecting clients: {e}");
1405        }
1406
1407        self.await_engines_disconnected().await;
1408        self.kernel.finalize_stop().await;
1409
1410        self.handle.set_state(NodeState::Stopped);
1411
1412        disconnect_result
1413    }
1414
1415    fn drain_channels(
1416        time_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TimeEventHandler>,
1417        data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
1418        data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataCommand>,
1419        exec_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1420        exec_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TradingCommand>,
1421    ) {
1422        let mut drained = 0;
1423
1424        while let Ok(handler) = time_evt_rx.try_recv() {
1425            AsyncRunner::handle_time_event(handler);
1426            drained += 1;
1427        }
1428
1429        while let Ok(cmd) = data_cmd_rx.try_recv() {
1430            AsyncRunner::handle_data_command(cmd);
1431            drained += 1;
1432        }
1433
1434        while let Ok(evt) = data_evt_rx.try_recv() {
1435            AsyncRunner::handle_data_event(evt);
1436            drained += 1;
1437        }
1438
1439        while let Ok(cmd) = exec_cmd_rx.try_recv() {
1440            AsyncRunner::handle_exec_command(cmd);
1441            drained += 1;
1442        }
1443
1444        while let Ok(evt) = exec_evt_rx.try_recv() {
1445            AsyncRunner::handle_exec_event(evt);
1446            drained += 1;
1447        }
1448
1449        if drained > 0 {
1450            log::info!("Drained {drained} remaining events during shutdown");
1451        }
1452    }
1453
1454    /// Gets the node's environment.
1455    #[must_use]
1456    pub fn environment(&self) -> Environment {
1457        self.kernel.environment()
1458    }
1459
1460    /// Gets a reference to the underlying kernel.
1461    #[must_use]
1462    pub const fn kernel(&self) -> &NautilusKernel {
1463        &self.kernel
1464    }
1465
1466    /// Gets an exclusive reference to the underlying kernel.
1467    #[must_use]
1468    pub const fn kernel_mut(&mut self) -> &mut NautilusKernel {
1469        &mut self.kernel
1470    }
1471
1472    /// Gets the node's trader ID.
1473    #[must_use]
1474    pub fn trader_id(&self) -> TraderId {
1475        self.kernel.trader_id()
1476    }
1477
1478    /// Gets the node's instance ID.
1479    #[must_use]
1480    pub const fn instance_id(&self) -> UUID4 {
1481        self.kernel.instance_id()
1482    }
1483
1484    /// Returns the current node state.
1485    #[must_use]
1486    pub fn state(&self) -> NodeState {
1487        self.handle.state()
1488    }
1489
1490    /// Checks if the live node is currently running.
1491    #[must_use]
1492    pub fn is_running(&self) -> bool {
1493        self.state().is_running()
1494    }
1495
1496    /// Sets the cache database adapter for persistence.
1497    ///
1498    /// This allows setting a database adapter (e.g., PostgreSQL, Redis) after the node
1499    /// is built but before it starts running. The database adapter is used to persist
1500    /// cache data for recovery and state management.
1501    ///
1502    /// # Errors
1503    ///
1504    /// Returns an error if the node is already running.
1505    pub fn set_cache_database(
1506        &mut self,
1507        database: Box<dyn CacheDatabaseAdapter>,
1508    ) -> anyhow::Result<()> {
1509        if self.state() != NodeState::Idle {
1510            anyhow::bail!(
1511                "Cannot set cache database while node is running, set it before calling start()"
1512            );
1513        }
1514
1515        self.kernel.cache().borrow_mut().set_database(database);
1516        Ok(())
1517    }
1518
1519    /// Returns the execution manager.
1520    #[must_use]
1521    pub fn exec_manager(&self) -> &ExecutionManager {
1522        &self.exec_manager
1523    }
1524
1525    /// Returns a mutable reference to the execution manager.
1526    #[must_use]
1527    pub fn exec_manager_mut(&mut self) -> &mut ExecutionManager {
1528        &mut self.exec_manager
1529    }
1530
1531    /// Adds an actor to the trader.
1532    ///
1533    /// This method provides a high-level interface for adding actors to the underlying
1534    /// trader without requiring direct access to the kernel. Actors should be added
1535    /// after the node is built but before starting the node.
1536    ///
1537    /// # Errors
1538    ///
1539    /// Returns an error if:
1540    /// - The trader is not in a valid state for adding components.
1541    /// - An actor with the same ID is already registered.
1542    /// - The node is currently running.
1543    pub fn add_actor<T>(&mut self, actor: T) -> anyhow::Result<()>
1544    where
1545        T: DataActor + DataActorNative + Component + Actor + 'static,
1546    {
1547        if self.state() != NodeState::Idle {
1548            anyhow::bail!(
1549                "Cannot add actor while node is running, add actors before calling start()"
1550            );
1551        }
1552
1553        self.kernel.trader.borrow_mut().add_actor(actor)
1554    }
1555
1556    /// Adds an actor to the live node using a factory function.
1557    ///
1558    /// The factory function is called at registration time to create the actor,
1559    /// avoiding cloning issues with non-cloneable actor types.
1560    ///
1561    /// # Errors
1562    ///
1563    /// Returns an error if:
1564    /// - The node is currently running.
1565    /// - The factory function fails to create the actor.
1566    /// - The underlying trader registration fails.
1567    pub fn add_actor_from_factory<F, T>(&mut self, factory: F) -> anyhow::Result<()>
1568    where
1569        F: FnOnce() -> anyhow::Result<T>,
1570        T: DataActor + DataActorNative + Component + Actor + 'static,
1571    {
1572        if self.state() != NodeState::Idle {
1573            anyhow::bail!(
1574                "Cannot add actor while node is running, add actors before calling start()"
1575            );
1576        }
1577
1578        self.kernel
1579            .trader
1580            .borrow_mut()
1581            .add_actor_from_factory(factory)
1582    }
1583
1584    /// Adds a strategy to the trader.
1585    ///
1586    /// Strategies are registered in both the component registry (for lifecycle management)
1587    /// and the actor registry (for data callbacks via msgbus).
1588    ///
1589    /// # Errors
1590    ///
1591    /// Returns an error if:
1592    /// - The node is currently running.
1593    /// - A strategy with the same ID is already registered.
1594    pub fn add_strategy<T>(&mut self, mut strategy: T) -> anyhow::Result<()>
1595    where
1596        T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
1597    {
1598        if self.state() != NodeState::Idle {
1599            anyhow::bail!(
1600                "Cannot add strategy while node is running, add strategies before calling start()"
1601            );
1602        }
1603
1604        // Capture strategy-owned values before adding the strategy, which moves it
1605        let strategy_id = self
1606            .kernel
1607            .trader
1608            .borrow()
1609            .prepare_strategy_for_registration(&mut strategy)?;
1610        let oms_type = StrategyNative::strategy_core(&strategy).config.oms_type;
1611        if let Some(claims) = strategy.external_order_claims() {
1612            for instrument_id in &claims {
1613                self.exec_manager
1614                    .claim_external_orders(*instrument_id, strategy_id)?;
1615            }
1616            log_info!(
1617                "Registered external order claims for {}: {:?}",
1618                strategy_id,
1619                claims,
1620                color = LogColor::Blue
1621            );
1622        }
1623
1624        self.kernel.trader.borrow_mut().add_strategy(strategy)?;
1625
1626        if let Some(oms_type) = oms_type {
1627            self.kernel
1628                .exec_engine
1629                .borrow_mut()
1630                .register_oms_type(strategy_id, oms_type);
1631        }
1632
1633        Ok(())
1634    }
1635
1636    /// Adds an execution algorithm to the trader.
1637    ///
1638    /// Execution algorithms are registered in both the component registry (for lifecycle
1639    /// management) and the actor registry (for data callbacks via msgbus).
1640    ///
1641    /// # Errors
1642    ///
1643    /// Returns an error if:
1644    /// - The node is currently running.
1645    /// - An execution algorithm with the same ID is already registered.
1646    pub fn add_exec_algorithm<T>(&mut self, exec_algorithm: T) -> anyhow::Result<()>
1647    where
1648        T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
1649    {
1650        if self.state() != NodeState::Idle {
1651            anyhow::bail!(
1652                "Cannot add exec algorithm while node is running, add exec algorithms before calling start()"
1653            );
1654        }
1655
1656        self.kernel
1657            .trader
1658            .borrow_mut()
1659            .add_exec_algorithm(exec_algorithm)
1660    }
1661
1662    // Runs reconciliation sub-checks, each gated by its own interval.
1663    // Continuous checks only schedule venue work; they do not await venue I/O
1664    // in the event loop.
1665    fn run_reconciliation_checks(
1666        &mut self,
1667        intervals: ReconciliationCheckIntervals,
1668        state: &mut ReconciliationCheckState<'_>,
1669    ) {
1670        let ts_now = self.exec_manager.generate_timestamp_ns();
1671
1672        if reconciliation_check_due(ts_now, *state.ts_last_inflight, intervals.inflight) {
1673            if self.state() == NodeState::ShuttingDown {
1674                return;
1675            }
1676            let result = self.exec_manager.check_inflight_orders();
1677            self.process_reconciliation_events(&result.events);
1678            for cmd in result.queries {
1679                AsyncRunner::handle_exec_command(cmd);
1680            }
1681            *state.ts_last_inflight = ts_now;
1682        }
1683
1684        let open_due = reconciliation_check_due(ts_now, *state.ts_last_open, intervals.open);
1685        let position_due =
1686            reconciliation_check_due(ts_now, *state.ts_last_position, intervals.position);
1687
1688        if (open_due || position_due) && self.state() == NodeState::ShuttingDown {
1689            return;
1690        }
1691
1692        if state.open_order_report_task.is_some() {
1693            if open_due {
1694                log::debug!("Open-order reconciliation already in progress");
1695                *state.ts_last_open = ts_now;
1696            }
1697
1698            if position_due {
1699                log::debug!(
1700                    "Position reconciliation delayed: open-order reconciliation in progress"
1701                );
1702            }
1703
1704            return;
1705        }
1706
1707        if state.position_report_task.is_some() {
1708            if position_due {
1709                log::debug!("Position reconciliation already in progress");
1710                *state.ts_last_position = ts_now;
1711            }
1712
1713            if open_due {
1714                log::debug!(
1715                    "Open-order reconciliation delayed: position reconciliation in progress"
1716                );
1717            }
1718
1719            return;
1720        }
1721
1722        if position_due && (!open_due || *state.ts_last_position < *state.ts_last_open) {
1723            *state.position_report_task = self.start_position_report_check();
1724            *state.ts_last_position = ts_now;
1725        } else if open_due {
1726            *state.open_order_report_task = self.start_open_order_report_check();
1727            *state.ts_last_open = ts_now;
1728        }
1729    }
1730
1731    fn start_open_order_report_check(&self) -> Option<OpenOrderReportTask> {
1732        if self.exec_clients.is_empty() {
1733            log::debug!("No execution clients to check orders consistency");
1734            return None;
1735        }
1736
1737        let check = self
1738            .exec_manager
1739            .prepare_open_order_report_check(UUID4::new());
1740        let command = check.command.clone();
1741        let clients = self.exec_clients.clone();
1742
1743        Some(OpenOrderReportTask {
1744            future: Box::pin(async move {
1745                let reports = request_open_order_reports(clients, command).await;
1746                OpenOrderReportResult { check, reports }
1747            }),
1748        })
1749    }
1750
1751    fn start_position_report_check(&self) -> Option<PositionReportTask> {
1752        if self.exec_clients.is_empty() {
1753            log::debug!("No execution clients to check positions consistency");
1754            return None;
1755        }
1756
1757        let check = self
1758            .exec_manager
1759            .prepare_position_report_check(UUID4::new());
1760        let command = check.command.clone();
1761        let clients = self.exec_clients.clone();
1762
1763        Some(PositionReportTask {
1764            future: Box::pin(async move {
1765                let result = request_position_reports(clients, command).await;
1766                PositionReportResult {
1767                    check,
1768                    reports: result.reports,
1769                    failed_venues: result.failed_venues,
1770                }
1771            }),
1772        })
1773    }
1774}
1775
1776async fn recv_external_msgbus_message(
1777    rx: &mut Option<tokio::sync::mpsc::Receiver<BusMessage>>,
1778) -> Option<BusMessage> {
1779    match rx {
1780        Some(rx) => rx.recv().await,
1781        None => std::future::pending::<Option<BusMessage>>().await,
1782    }
1783}
1784
1785async fn request_open_order_reports(
1786    clients: Vec<LiveExecutionClient>,
1787    command: GenerateOrderStatusReports,
1788) -> Vec<OrderStatusReport> {
1789    let mut all_reports = Vec::new();
1790
1791    for client in clients {
1792        match client.generate_order_status_reports(&command).await {
1793            Ok(reports) => {
1794                all_reports.extend(reports);
1795            }
1796            Err(e) => {
1797                log::warn!(
1798                    "Failed to generate order status reports from {}: {e}",
1799                    client.client_id()
1800                );
1801            }
1802        }
1803    }
1804
1805    all_reports
1806}
1807
1808async fn request_position_reports(
1809    clients: Vec<LiveExecutionClient>,
1810    command: GeneratePositionStatusReports,
1811) -> PositionReportQueryResult {
1812    let mut all_reports = Vec::new();
1813    let mut failed_venues = IndexSet::new();
1814
1815    for client in clients {
1816        let venue = client.venue();
1817        match client.generate_position_status_reports(&command).await {
1818            Ok(reports) => {
1819                all_reports.extend(reports);
1820            }
1821            Err(e) => {
1822                failed_venues.insert(venue);
1823                log::warn!(
1824                    "Failed to generate position status reports from {}: {e}",
1825                    client.client_id()
1826                );
1827            }
1828        }
1829    }
1830
1831    PositionReportQueryResult {
1832        reports: all_reports,
1833        failed_venues,
1834    }
1835}
1836
1837fn reconciliation_check_due(ts_now: UnixNanos, ts_last: UnixNanos, interval_ns: u64) -> bool {
1838    interval_ns > 0
1839        && ts_now
1840            .duration_since(&ts_last)
1841            .is_some_and(|elapsed_ns| elapsed_ns >= interval_ns)
1842}
1843
1844#[derive(Clone, Copy)]
1845struct ReconciliationCheckIntervals {
1846    inflight: u64,
1847    open: u64,
1848    position: u64,
1849}
1850
1851struct ReconciliationCheckState<'a> {
1852    ts_last_inflight: &'a mut UnixNanos,
1853    ts_last_open: &'a mut UnixNanos,
1854    ts_last_position: &'a mut UnixNanos,
1855    open_order_report_task: &'a mut Option<OpenOrderReportTask>,
1856    position_report_task: &'a mut Option<PositionReportTask>,
1857}
1858
1859type OpenOrderReportFuture = Pin<Box<dyn Future<Output = OpenOrderReportResult>>>;
1860
1861struct OpenOrderReportTask {
1862    future: OpenOrderReportFuture,
1863}
1864
1865struct OpenOrderReportResult {
1866    check: OpenOrderReportCheck,
1867    reports: Vec<OrderStatusReport>,
1868}
1869
1870type PositionReportFuture = Pin<Box<dyn Future<Output = PositionReportResult>>>;
1871
1872struct PositionReportTask {
1873    future: PositionReportFuture,
1874}
1875
1876struct PositionReportResult {
1877    check: PositionReportCheck,
1878    reports: Vec<PositionStatusReport>,
1879    failed_venues: IndexSet<Venue>,
1880}
1881
1882struct PositionReportQueryResult {
1883    reports: Vec<PositionStatusReport>,
1884    failed_venues: IndexSet<Venue>,
1885}
1886
1887/// Flushes data events and commands from both `pending` and the channel receivers
1888/// into the cache, looping until no progress is made.
1889///
1890/// This closes the gap where `drive_with_event_buffering` exits as soon as its
1891/// driven future resolves (biased select), leaving items in the channel receivers
1892/// that were not captured into `pending`.
1893fn flush_pending_data(
1894    pending: &mut PendingEvents,
1895    data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
1896    data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataCommand>,
1897) {
1898    loop {
1899        let mut progressed = pending.drain_data();
1900
1901        while let Ok(evt) = data_evt_rx.try_recv() {
1902            AsyncRunner::handle_data_event(evt);
1903            progressed = true;
1904        }
1905
1906        while let Ok(cmd) = data_cmd_rx.try_recv() {
1907            AsyncRunner::handle_data_command(cmd);
1908            progressed = true;
1909        }
1910
1911        if !progressed {
1912            break;
1913        }
1914    }
1915}
1916
1917/// Flushes all channel receivers into `pending`, then drains everything.
1918///
1919/// Unlike [`flush_pending_data`] this is a single pass, not a drain-until-quiet
1920/// loop. Sufficient for phase 2 where the goal is to capture items the biased
1921/// select did not poll before the connect future resolved.
1922fn flush_all_pending(
1923    pending: &mut PendingEvents,
1924    time_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TimeEventHandler>,
1925    data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
1926    data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataCommand>,
1927    exec_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1928    exec_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TradingCommand>,
1929) {
1930    // Flush channel receivers into pending
1931    while let Ok(handler) = time_evt_rx.try_recv() {
1932        AsyncRunner::handle_time_event(handler);
1933    }
1934
1935    while let Ok(evt) = data_evt_rx.try_recv() {
1936        pending.data_evts.push(evt);
1937    }
1938
1939    while let Ok(cmd) = data_cmd_rx.try_recv() {
1940        pending.data_cmds.push(cmd);
1941    }
1942
1943    while let Ok(evt) = exec_evt_rx.try_recv() {
1944        match evt {
1945            ExecutionEvent::Account(_) => {
1946                AsyncRunner::handle_exec_event(evt);
1947            }
1948            ExecutionEvent::Report(report) => {
1949                pending.exec_reports.push(report);
1950            }
1951            ExecutionEvent::Order(order_evt) => {
1952                pending.order_evts.push(order_evt);
1953            }
1954            ExecutionEvent::OrderSubmittedBatch(batch) => {
1955                for submitted in batch {
1956                    pending.order_evts.push(OrderEventAny::Submitted(submitted));
1957                }
1958            }
1959            ExecutionEvent::OrderAcceptedBatch(batch) => {
1960                for accepted in batch {
1961                    pending.order_evts.push(OrderEventAny::Accepted(accepted));
1962                }
1963            }
1964            ExecutionEvent::OrderCanceledBatch(batch) => {
1965                for canceled in batch {
1966                    pending.order_evts.push(OrderEventAny::Canceled(canceled));
1967                }
1968            }
1969        }
1970    }
1971
1972    while let Ok(cmd) = exec_cmd_rx.try_recv() {
1973        pending.exec_cmds.push(cmd);
1974    }
1975
1976    pending.drain();
1977}
1978
1979/// Drives a future to completion while buffering channel events.
1980///
1981/// Time events are handled immediately. Account events are forwarded directly.
1982/// All other events are buffered in `pending` for later processing.
1983async fn drive_with_event_buffering<F: std::future::Future>(
1984    future: F,
1985    pending: &mut PendingEvents,
1986    time_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TimeEventHandler>,
1987    data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
1988    data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataCommand>,
1989    exec_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1990    exec_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TradingCommand>,
1991) -> F::Output {
1992    tokio::pin!(future);
1993
1994    loop {
1995        tokio::select! {
1996            biased;
1997
1998            result = &mut future => {
1999                break result;
2000            }
2001            Some(handler) = time_evt_rx.recv() => {
2002                AsyncRunner::handle_time_event(handler);
2003            }
2004            Some(evt) = exec_evt_rx.recv() => {
2005                // Account events are safe to process immediately. Report and
2006                // Order events need ExecEngine borrow_mut which may conflict
2007                // with the borrow held by the driven future.
2008                match evt {
2009                    ExecutionEvent::Account(_) => {
2010                        AsyncRunner::handle_exec_event(evt);
2011                    }
2012                    ExecutionEvent::Report(report) => {
2013                        pending.exec_reports.push(report);
2014                    }
2015                    ExecutionEvent::Order(order_evt) => {
2016                        pending.order_evts.push(order_evt);
2017                    }
2018                    ExecutionEvent::OrderSubmittedBatch(batch) => {
2019                        for submitted in batch {
2020                            pending.order_evts.push(OrderEventAny::Submitted(submitted));
2021                        }
2022                    }
2023                    ExecutionEvent::OrderAcceptedBatch(batch) => {
2024                        for accepted in batch {
2025                            pending.order_evts.push(OrderEventAny::Accepted(accepted));
2026                        }
2027                    }
2028                    ExecutionEvent::OrderCanceledBatch(batch) => {
2029                        for canceled in batch {
2030                            pending.order_evts.push(OrderEventAny::Canceled(canceled));
2031                        }
2032                    }
2033                }
2034            }
2035            Some(cmd) = exec_cmd_rx.recv() => {
2036                pending.exec_cmds.push(cmd);
2037            }
2038            Some(evt) = data_evt_rx.recv() => {
2039                pending.data_evts.push(evt);
2040            }
2041            Some(cmd) = data_cmd_rx.recv() => {
2042                pending.data_cmds.push(cmd);
2043            }
2044        }
2045    }
2046}
2047
2048#[derive(Default)]
2049struct PendingEvents {
2050    data_cmds: Vec<DataCommand>,
2051    data_evts: Vec<DataEvent>,
2052    exec_cmds: Vec<TradingCommand>,
2053    exec_reports: Vec<ExecutionReport>,
2054    order_evts: Vec<OrderEventAny>,
2055}
2056
2057impl PendingEvents {
2058    fn is_empty(&self) -> bool {
2059        self.data_evts.is_empty()
2060            && self.data_cmds.is_empty()
2061            && self.exec_cmds.is_empty()
2062            && self.exec_reports.is_empty()
2063            && self.order_evts.is_empty()
2064    }
2065
2066    /// Drains only data events and commands into the cache.
2067    ///
2068    /// Returns `true` if any events or commands were drained.
2069    fn drain_data(&mut self) -> bool {
2070        let total = self.data_evts.len() + self.data_cmds.len();
2071
2072        if total > 0 {
2073            log::debug!(
2074                "Draining {total} data events/commands into cache \
2075                 (data_evts={}, data_cmds={})",
2076                self.data_evts.len(),
2077                self.data_cmds.len(),
2078            );
2079        }
2080
2081        for evt in self.data_evts.drain(..) {
2082            AsyncRunner::handle_data_event(evt);
2083        }
2084
2085        for cmd in self.data_cmds.drain(..) {
2086            AsyncRunner::handle_data_command(cmd);
2087        }
2088
2089        total > 0
2090    }
2091
2092    /// Drains all remaining pending events.
2093    fn drain(&mut self) {
2094        let total = self.data_evts.len()
2095            + self.data_cmds.len()
2096            + self.exec_cmds.len()
2097            + self.exec_reports.len()
2098            + self.order_evts.len();
2099
2100        if total > 0 {
2101            log::debug!(
2102                "Processing {total} events/commands queued during startup \
2103                 (data_evts={}, data_cmds={}, exec_cmds={}, exec_reports={}, order_evts={})",
2104                self.data_evts.len(),
2105                self.data_cmds.len(),
2106                self.exec_cmds.len(),
2107                self.exec_reports.len(),
2108                self.order_evts.len()
2109            );
2110        }
2111
2112        for evt in self.data_evts.drain(..) {
2113            AsyncRunner::handle_data_event(evt);
2114        }
2115
2116        for cmd in self.data_cmds.drain(..) {
2117            AsyncRunner::handle_data_command(cmd);
2118        }
2119
2120        for report in self.exec_reports.drain(..) {
2121            AsyncRunner::handle_exec_event(ExecutionEvent::Report(report));
2122        }
2123
2124        for cmd in self.exec_cmds.drain(..) {
2125            AsyncRunner::handle_exec_command(cmd);
2126        }
2127
2128        for evt in self.order_evts.drain(..) {
2129            AsyncRunner::handle_exec_event(ExecutionEvent::Order(evt));
2130        }
2131    }
2132}
2133
2134struct ClientStatus {
2135    client: String,
2136    client_type: &'static str,
2137    connected: bool,
2138}
2139
2140fn render_client_statuses(rows: Vec<ClientStatus>) -> String {
2141    let mut builder = Builder::with_capacity(rows.len() + 1, 3);
2142    builder.push_record(["Client", "Type", "Connected"]);
2143
2144    for row in rows {
2145        builder.push_record([
2146            row.client,
2147            row.client_type.to_string(),
2148            row.connected.to_string(),
2149        ]);
2150    }
2151
2152    builder.build().with(Style::rounded()).to_string()
2153}
2154
2155#[cfg(test)]
2156mod tests {
2157    use std::{
2158        cell::{Cell, RefCell},
2159        fmt::Debug,
2160        rc::Rc,
2161        sync::{
2162            Arc, Mutex,
2163            atomic::{AtomicBool, Ordering},
2164        },
2165    };
2166
2167    use bytes::Bytes;
2168    #[cfg(feature = "python")]
2169    use nautilus_common::runner::{
2170        SyncDataCommandSender, SyncTradingCommandSender, replace_data_cmd_sender,
2171        replace_exec_cmd_sender,
2172    };
2173    use nautilus_common::{
2174        actor::DataActor,
2175        cache::Cache,
2176        clock::Clock,
2177        enums::SerializationEncoding,
2178        messages::execution::{SubmitOrder, TradingCommand},
2179        msgbus::{
2180            self, BusMessage, BusPayloadType, MessageBusBacking, MessageBusBackingFactory,
2181            MessageBusConfig, MessageBusExternalEgress, MessageBusExternalIngress,
2182            MessagingSwitchboard, TypedHandler, TypedIntoHandler,
2183        },
2184    };
2185    use nautilus_core::{UUID4, UnixNanos};
2186    use nautilus_execution::engine::{
2187        ExecutionEngine, SnapshotAnchorer, stubs::StubExecutionClient,
2188    };
2189    use nautilus_model::{
2190        data::QuoteTick,
2191        enums::{OmsType, OrderStatus, OrderType},
2192        identifiers::{
2193            AccountId, ClientId, InstrumentId, PositionId, StrategyId, TraderId, VenueOrderId,
2194        },
2195        instruments::{Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt},
2196        orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
2197        types::{Price, Quantity},
2198    };
2199    use nautilus_system::{KernelEventStore, RegisteredComponents, event_store::EventStoreConfig};
2200    use nautilus_trading::{
2201        nautilus_strategy,
2202        strategy::{config::StrategyConfig, core::StrategyCore},
2203    };
2204    use rstest::*;
2205
2206    use super::*;
2207
2208    #[rstest]
2209    fn test_render_client_statuses() {
2210        let rows = vec![
2211            ClientStatus {
2212                client: "BINANCE".to_string(),
2213                client_type: "Data",
2214                connected: true,
2215            },
2216            ClientStatus {
2217                client: "SIM".to_string(),
2218                client_type: "Execution",
2219                connected: false,
2220            },
2221        ];
2222
2223        let output = render_client_statuses(rows);
2224        let expected = "╭─────────┬───────────┬───────────╮\n\
2225│ Client  │ Type      │ Connected │\n\
2226├─────────┼───────────┼───────────┤\n\
2227│ BINANCE │ Data      │ true      │\n\
2228│ SIM     │ Execution │ false     │\n\
2229╰─────────┴───────────┴───────────╯";
2230
2231        assert_eq!(output, expected);
2232    }
2233
2234    #[derive(Debug)]
2235    struct ReplayKernelEventStore {
2236        fail_restore: bool,
2237    }
2238
2239    impl KernelEventStore for ReplayKernelEventStore {
2240        fn restore_parent_cache(
2241            &mut self,
2242            _instance_id: UUID4,
2243            _cache: &mut Cache,
2244        ) -> anyhow::Result<()> {
2245            if self.fail_restore {
2246                anyhow::bail!("replay restore failed");
2247            }
2248
2249            Ok(())
2250        }
2251
2252        fn open(
2253            &mut self,
2254            _instance_id: UUID4,
2255            _components: &RegisteredComponents,
2256            _environment: Environment,
2257        ) -> anyhow::Result<()> {
2258            Ok(())
2259        }
2260
2261        fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
2262            None
2263        }
2264
2265        fn seal(&mut self, _ts_init: UnixNanos) {}
2266
2267        fn run_id(&self) -> Option<&str> {
2268            Some("replay-child")
2269        }
2270
2271        fn parent_run_id(&self) -> Option<&str> {
2272            Some("seed-run")
2273        }
2274
2275        fn is_event_store_replay_configured(&self) -> bool {
2276            true
2277        }
2278
2279        fn is_halted(&self) -> bool {
2280            false
2281        }
2282    }
2283
2284    #[derive(Debug)]
2285    struct TestStrategy {
2286        core: StrategyCore,
2287    }
2288
2289    impl TestStrategy {
2290        fn new(config: StrategyConfig) -> Self {
2291            Self {
2292                core: StrategyCore::new(config),
2293            }
2294        }
2295    }
2296
2297    impl DataActor for TestStrategy {}
2298
2299    nautilus_strategy!(TestStrategy);
2300
2301    fn live_node_with_replay_store(fail_restore: bool) -> LiveNode {
2302        // load_state must be true: the kernel rejects event-store replay otherwise,
2303        // and LiveNodeConfig defaults it to false.
2304        let builder = LiveNodeBuilder::new(TraderId::default(), Environment::Live)
2305            .unwrap()
2306            .with_exec_engine_config(crate::config::LiveExecEngineConfig {
2307                reconciliation: false,
2308                ..Default::default()
2309            })
2310            .with_load_state(true)
2311            .with_name("TestKernel")
2312            .with_event_store(move |_instance_id: UUID4, _clock: Rc<RefCell<dyn Clock>>| {
2313                Ok(Box::new(ReplayKernelEventStore { fail_restore }) as Box<dyn KernelEventStore>)
2314            });
2315
2316        builder.build().unwrap()
2317    }
2318
2319    #[rstest]
2320    fn test_add_strategy_registers_configured_hedging_oms_type() {
2321        let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
2322            .unwrap()
2323            .with_reconciliation(false)
2324            .with_delay_post_stop_secs(0)
2325            .with_timeout_connection(1)
2326            .build()
2327            .unwrap();
2328        let strategy_id = StrategyId::from("FUNDING_ARBITRAGE-001");
2329
2330        node.add_strategy(TestStrategy::new(StrategyConfig {
2331            strategy_id: Some(strategy_id),
2332            oms_type: Some(OmsType::Hedging),
2333            ..Default::default()
2334        }))
2335        .unwrap();
2336
2337        let instrument = crypto_perpetual_ethusdt();
2338        let instrument_id = instrument.id();
2339        let client_id = ClientId::from("STUB");
2340
2341        node.kernel
2342            .cache
2343            .borrow_mut()
2344            .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
2345            .unwrap();
2346        node.kernel
2347            .exec_engine
2348            .borrow_mut()
2349            .register_client(Box::new(StubExecutionClient::new(
2350                client_id,
2351                AccountId::from("TEST-ACCOUNT"),
2352                instrument_id.venue,
2353                OmsType::Netting,
2354                None,
2355            )))
2356            .unwrap();
2357
2358        let order = OrderTestBuilder::new(OrderType::Market)
2359            .trader_id(node.trader_id())
2360            .strategy_id(strategy_id)
2361            .instrument_id(instrument_id)
2362            .quantity(Quantity::from("1.000"))
2363            .build();
2364        let position_id = PositionId::new("CUSTOM-POSITION-001");
2365
2366        node.kernel
2367            .cache
2368            .borrow_mut()
2369            .add_order(order.clone(), Some(position_id), Some(client_id), true)
2370            .unwrap();
2371
2372        let submit_order = SubmitOrder::new(
2373            order.trader_id(),
2374            Some(client_id),
2375            strategy_id,
2376            instrument_id,
2377            order.client_order_id(),
2378            order.init_event().clone(),
2379            order.exec_algorithm_id(),
2380            Some(position_id),
2381            None,
2382            UUID4::new(),
2383            UnixNanos::default(),
2384            None,
2385        );
2386
2387        node.kernel
2388            .exec_engine
2389            .borrow()
2390            .execute(TradingCommand::SubmitOrder(submit_order));
2391
2392        let exec_engine = node.kernel.exec_engine.borrow();
2393        let cache = exec_engine.cache().borrow();
2394        let cached_order = cache
2395            .order(&order.client_order_id())
2396            .expect("Order should be cached");
2397
2398        assert_eq!(cached_order.status(), OrderStatus::Initialized);
2399    }
2400
2401    #[rstest]
2402    fn test_run_reconciliation_checks_does_not_publish_open_order_queries() {
2403        let config = LiveNodeConfig {
2404            exec_engine: crate::config::LiveExecEngineConfig {
2405                reconciliation: true,
2406                open_check_interval_secs: Some(1.0),
2407                position_check_interval_secs: Some(1.0),
2408                max_single_order_queries_per_cycle: 5,
2409                ..Default::default()
2410            },
2411            ..Default::default()
2412        };
2413        let mut node =
2414            LiveNode::build("ReconciliationFallbackNode".to_string(), Some(config)).unwrap();
2415        let client_id = ClientId::from("TEST-QUERY");
2416        let account_id = AccountId::from("TEST-QUERY-001");
2417
2418        let trading_commands = Rc::new(RefCell::new(Vec::new()));
2419        msgbus::register_trading_command_endpoint(
2420            MessagingSwitchboard::exec_engine_execute(),
2421            TypedIntoHandler::from({
2422                let trading_commands = trading_commands.clone();
2423                move |command: TradingCommand| {
2424                    trading_commands.borrow_mut().push(command);
2425                }
2426            }),
2427        );
2428
2429        let venue_order_id = VenueOrderId::from("V-NODE-QUERY-001");
2430        let instrument = crypto_perpetual_ethusdt();
2431        let instrument_id = instrument.id();
2432        let client_order_id = ClientOrderId::from("O-NODE-QUERY-001");
2433
2434        node.kernel
2435            .cache
2436            .borrow_mut()
2437            .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
2438            .unwrap();
2439        insert_accepted_limit_order_in_node(
2440            &node,
2441            account_id,
2442            client_id,
2443            instrument_id,
2444            client_order_id,
2445            venue_order_id,
2446        );
2447
2448        let mut ts_last_inflight = UnixNanos::default();
2449        let mut ts_last_open = UnixNanos::default();
2450        let mut ts_last_position = UnixNanos::default();
2451        let mut open_order_report_task = None;
2452        let mut position_report_task = None;
2453
2454        node.run_reconciliation_checks(
2455            ReconciliationCheckIntervals {
2456                inflight: 0,
2457                open: 1,
2458                position: 0,
2459            },
2460            &mut ReconciliationCheckState {
2461                ts_last_inflight: &mut ts_last_inflight,
2462                ts_last_open: &mut ts_last_open,
2463                ts_last_position: &mut ts_last_position,
2464                open_order_report_task: &mut open_order_report_task,
2465                position_report_task: &mut position_report_task,
2466            },
2467        );
2468
2469        let commands = trading_commands.borrow();
2470
2471        assert!(commands.is_empty());
2472        assert!(open_order_report_task.is_none());
2473        assert!(position_report_task.is_none());
2474
2475        ExecutionEngine::register_msgbus_handlers(&node.kernel.exec_engine);
2476    }
2477
2478    fn insert_accepted_limit_order_in_node(
2479        node: &LiveNode,
2480        account_id: AccountId,
2481        client_id: ClientId,
2482        instrument_id: InstrumentId,
2483        client_order_id: ClientOrderId,
2484        venue_order_id: VenueOrderId,
2485    ) {
2486        let order = OrderTestBuilder::new(OrderType::Limit)
2487            .client_order_id(client_order_id)
2488            .instrument_id(instrument_id)
2489            .quantity(Quantity::from("10.0"))
2490            .price(Price::from("100.0"))
2491            .build();
2492        let submitted = TestOrderEventStubs::submitted(&order, account_id);
2493        node.kernel
2494            .cache
2495            .borrow_mut()
2496            .add_order(order, None, Some(client_id), false)
2497            .unwrap();
2498        let order = node
2499            .kernel
2500            .cache
2501            .borrow_mut()
2502            .update_order(&submitted)
2503            .unwrap();
2504        let accepted = TestOrderEventStubs::accepted(&order, account_id, venue_order_id);
2505        node.kernel
2506            .cache
2507            .borrow_mut()
2508            .update_order(&accepted)
2509            .unwrap();
2510    }
2511
2512    #[rstest]
2513    #[case(0, NodeState::Idle)]
2514    #[case(1, NodeState::Starting)]
2515    #[case(2, NodeState::Running)]
2516    #[case(3, NodeState::ShuttingDown)]
2517    #[case(4, NodeState::Stopped)]
2518    fn test_node_state_from_u8_valid(#[case] value: u8, #[case] expected: NodeState) {
2519        assert_eq!(NodeState::from_u8(value), expected);
2520    }
2521
2522    #[rstest]
2523    #[case(5)]
2524    #[case(255)]
2525    #[should_panic(expected = "Invalid NodeState value")]
2526    fn test_node_state_from_u8_invalid_panics(#[case] value: u8) {
2527        let _ = NodeState::from_u8(value);
2528    }
2529
2530    #[rstest]
2531    fn test_node_state_roundtrip() {
2532        for state in [
2533            NodeState::Idle,
2534            NodeState::Starting,
2535            NodeState::Running,
2536            NodeState::ShuttingDown,
2537            NodeState::Stopped,
2538        ] {
2539            assert_eq!(NodeState::from_u8(state.as_u8()), state);
2540        }
2541    }
2542
2543    #[rstest]
2544    fn test_node_state_is_running_only_for_running() {
2545        assert!(!NodeState::Idle.is_running());
2546        assert!(!NodeState::Starting.is_running());
2547        assert!(NodeState::Running.is_running());
2548        assert!(!NodeState::ShuttingDown.is_running());
2549        assert!(!NodeState::Stopped.is_running());
2550    }
2551
2552    #[rstest]
2553    #[tokio::test]
2554    async fn test_await_engines_connected_returns_stop_requested() {
2555        let node = LiveNode::build("TestNode".to_string(), None).unwrap();
2556        let handle = node.handle();
2557
2558        handle.stop();
2559
2560        let status = node.await_engines_connected().await;
2561
2562        assert_eq!(status, EngineConnectionStatus::StopRequested);
2563        assert!(handle.should_stop());
2564    }
2565
2566    #[rstest]
2567    #[tokio::test]
2568    async fn test_await_engines_connected_returns_shutdown_requested() {
2569        let node = LiveNode::build("TestNode".to_string(), None).unwrap();
2570
2571        node.kernel().shutdown_flag().set(true);
2572
2573        let status = node.await_engines_connected().await;
2574
2575        assert_eq!(status, EngineConnectionStatus::ShutdownRequested);
2576    }
2577
2578    #[rstest]
2579    #[tokio::test]
2580    async fn test_start_stop_request_aborts_startup_without_running() {
2581        let config = LiveNodeConfig {
2582            exec_engine: crate::config::LiveExecEngineConfig {
2583                reconciliation: false,
2584                ..Default::default()
2585            },
2586            timeout_disconnection: Duration::from_millis(50),
2587            ..Default::default()
2588        };
2589        let mut node = LiveNode::build("TestNode".to_string(), Some(config)).unwrap();
2590        let handle = node.handle();
2591
2592        handle.stop();
2593        node.start().await.unwrap();
2594
2595        assert_eq!(handle.state(), NodeState::Stopped);
2596        assert!(handle.should_stop());
2597        assert!(!handle.is_running());
2598    }
2599
2600    #[rstest]
2601    #[tokio::test]
2602    async fn test_start_event_store_replay_skips_live_connections() {
2603        let mut node = live_node_with_replay_store(false);
2604        let handle = node.handle();
2605
2606        node.start().await.unwrap();
2607
2608        assert_eq!(handle.state(), NodeState::Running);
2609        assert!(handle.is_running());
2610        assert!(node.kernel.is_event_store_replay());
2611        assert!(node.runner.is_some());
2612    }
2613
2614    #[rstest]
2615    #[tokio::test]
2616    async fn test_start_event_store_replay_config_failure_aborts_startup() {
2617        let mut node = live_node_with_replay_store(true);
2618        let handle = node.handle();
2619
2620        node.start().await.unwrap();
2621
2622        assert_eq!(handle.state(), NodeState::Stopped);
2623        assert!(!handle.is_running());
2624        assert!(node.kernel.is_event_store_replay_configured());
2625        assert!(!node.kernel.is_event_store_replay());
2626        assert!(node.runner.is_some());
2627    }
2628
2629    #[rstest]
2630    #[tokio::test]
2631    async fn test_run_event_store_replay_consumes_runner_and_stops_before_connections() {
2632        let mut node = live_node_with_replay_store(false);
2633        let handle = node.handle();
2634
2635        node.run().await.unwrap();
2636
2637        assert_eq!(handle.state(), NodeState::Running);
2638        assert!(handle.is_running());
2639        assert!(node.kernel.is_event_store_replay());
2640        assert!(node.runner.is_none());
2641    }
2642
2643    #[rstest]
2644    #[tokio::test]
2645    async fn test_run_event_store_replay_config_failure_aborts_startup() {
2646        let mut node = live_node_with_replay_store(true);
2647        let handle = node.handle();
2648
2649        node.run().await.unwrap();
2650
2651        assert_eq!(handle.state(), NodeState::Stopped);
2652        assert!(!handle.is_running());
2653        assert!(node.kernel.is_event_store_replay_configured());
2654        assert!(!node.kernel.is_event_store_replay());
2655        assert!(node.runner.is_none());
2656    }
2657
2658    #[rstest]
2659    fn test_build_rejects_event_store_config_without_factory() {
2660        let config = LiveNodeConfig {
2661            event_store: Some(EventStoreConfig::default()),
2662            exec_engine: crate::config::LiveExecEngineConfig {
2663                reconciliation: false,
2664                ..Default::default()
2665            },
2666            ..Default::default()
2667        };
2668
2669        let err = LiveNodeBuilder::from_config(config)
2670            .expect("builder")
2671            .build()
2672            .expect_err("should reject event_store config without factory");
2673
2674        assert!(
2675            err.to_string().contains("with_event_store"),
2676            "error message should mention with_event_store, was: {err}"
2677        );
2678    }
2679
2680    #[rstest]
2681    fn test_direct_build_rejects_event_store_config() {
2682        let config = LiveNodeConfig {
2683            event_store: Some(EventStoreConfig::default()),
2684            exec_engine: crate::config::LiveExecEngineConfig {
2685                reconciliation: false,
2686                ..Default::default()
2687            },
2688            ..Default::default()
2689        };
2690
2691        let err = LiveNode::build("TestNode".to_string(), Some(config))
2692            .expect_err("LiveNode::build should reject event_store config");
2693
2694        assert!(
2695            err.to_string().contains("with_event_store"),
2696            "error message should mention with_event_store, was: {err}"
2697        );
2698    }
2699
2700    #[rstest]
2701    fn test_handle_initial_state() {
2702        let handle = LiveNodeHandle::new();
2703
2704        assert_eq!(handle.state(), NodeState::Idle);
2705        assert!(!handle.should_stop());
2706        assert!(!handle.is_running());
2707    }
2708
2709    #[rstest]
2710    fn test_handle_stop_sets_flag() {
2711        let handle = LiveNodeHandle::new();
2712
2713        handle.stop();
2714
2715        assert!(handle.should_stop());
2716    }
2717
2718    #[rstest]
2719    fn test_handle_set_state_running_clears_stop_flag() {
2720        let handle = LiveNodeHandle::new();
2721        handle.stop();
2722        assert!(handle.should_stop());
2723
2724        handle.set_state(NodeState::Running);
2725
2726        assert!(!handle.should_stop());
2727        assert!(handle.is_running());
2728        assert_eq!(handle.state(), NodeState::Running);
2729    }
2730
2731    #[rstest]
2732    fn test_handle_node_state_transitions() {
2733        let handle = LiveNodeHandle::new();
2734        assert_eq!(handle.state(), NodeState::Idle);
2735
2736        handle.set_state(NodeState::Starting);
2737        assert_eq!(handle.state(), NodeState::Starting);
2738        assert!(!handle.is_running());
2739
2740        handle.set_state(NodeState::Running);
2741        assert_eq!(handle.state(), NodeState::Running);
2742        assert!(handle.is_running());
2743
2744        handle.set_state(NodeState::ShuttingDown);
2745        assert_eq!(handle.state(), NodeState::ShuttingDown);
2746        assert!(!handle.is_running());
2747
2748        handle.set_state(NodeState::Stopped);
2749        assert_eq!(handle.state(), NodeState::Stopped);
2750        assert!(!handle.is_running());
2751    }
2752
2753    #[rstest]
2754    fn test_handle_clone_shares_state_bidirectionally() {
2755        let handle1 = LiveNodeHandle::new();
2756        let handle2 = handle1.clone();
2757
2758        // Mutation from handle1 visible in handle2
2759        handle1.stop();
2760        assert!(handle2.should_stop());
2761
2762        // Mutation from handle2 visible in handle1
2763        handle2.set_state(NodeState::Running);
2764        assert_eq!(handle1.state(), NodeState::Running);
2765    }
2766
2767    #[rstest]
2768    fn test_handle_stop_flag_independent_of_state() {
2769        let handle = LiveNodeHandle::new();
2770
2771        // Stop flag can be set regardless of state
2772        handle.set_state(NodeState::Starting);
2773        handle.stop();
2774        assert!(handle.should_stop());
2775        assert_eq!(handle.state(), NodeState::Starting);
2776
2777        // Only Running state clears the stop flag
2778        handle.set_state(NodeState::ShuttingDown);
2779        assert!(handle.should_stop()); // Still set
2780
2781        handle.set_state(NodeState::Running);
2782        assert!(!handle.should_stop()); // Cleared
2783    }
2784
2785    #[rstest]
2786    fn test_builder_creation() {
2787        let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox);
2788
2789        assert!(result.is_ok());
2790    }
2791
2792    #[rstest]
2793    fn test_builder_rejects_backtest() {
2794        let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Backtest);
2795
2796        assert!(result.is_err());
2797        assert!(result.unwrap_err().to_string().contains("Backtest"));
2798    }
2799
2800    #[rstest]
2801    fn test_builder_accepts_live_environment() {
2802        let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Live);
2803
2804        assert!(result.is_ok());
2805    }
2806
2807    #[rstest]
2808    fn test_builder_accepts_sandbox_environment() {
2809        let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox);
2810
2811        assert!(result.is_ok());
2812    }
2813
2814    #[rstest]
2815    fn test_builder_fluent_api_chaining() {
2816        let builder = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Live)
2817            .unwrap()
2818            .with_name("TestNode")
2819            .with_instance_id(UUID4::new())
2820            .with_load_state(false)
2821            .with_save_state(true)
2822            .with_timeout_connection(30)
2823            .with_timeout_reconciliation(60)
2824            .with_reconciliation(true)
2825            .with_reconciliation_lookback_mins(120)
2826            .with_timeout_portfolio(10)
2827            .with_timeout_disconnection_secs(5)
2828            .with_delay_post_stop_secs(3)
2829            .with_delay_shutdown_secs(10);
2830
2831        assert_eq!(builder.name(), "TestNode");
2832    }
2833
2834    #[rstest]
2835    fn test_builder_with_external_msgbus_egress_uses_configured_encoding() {
2836        let (external_egress, publications, closed) = CapturingExternalEgress::new();
2837        let msgbus_config = MessageBusConfig {
2838            encoding: SerializationEncoding::Json,
2839            ..Default::default()
2840        };
2841        let node = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
2842            .unwrap()
2843            .with_msgbus_config(msgbus_config)
2844            .with_external_msgbus_egress(Box::new(external_egress))
2845            .build()
2846            .expect("node builds with external message bus egress");
2847        let quote = QuoteTick::default();
2848
2849        msgbus::publish_quote("data.quotes.TEST".into(), &quote);
2850
2851        let publications = publications.borrow();
2852        assert_eq!(publications.len(), 1);
2853        assert_eq!(publications[0].topic, "data.quotes.TEST");
2854        assert_eq!(
2855            serde_json::from_slice::<QuoteTick>(&publications[0].payload)
2856                .expect("JSON payload must decode as QuoteTick"),
2857            quote
2858        );
2859        drop(publications);
2860
2861        msgbus::get_message_bus().borrow_mut().dispose();
2862        assert!(closed.get());
2863        drop(node);
2864    }
2865
2866    #[rstest]
2867    #[tokio::test(flavor = "current_thread")]
2868    async fn test_builder_with_external_msgbus_factory_installs_egress_and_ingress() {
2869        let quote = QuoteTick::default();
2870        let (tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
2871        let publications = Arc::new(Mutex::new(Vec::new()));
2872        let closed = Arc::new(AtomicBool::new(false));
2873        let factory = CapturingBackingFactory::new(publications.clone(), closed.clone(), Some(rx));
2874        let msgbus_config = MessageBusConfig {
2875            external_streams: Some(vec!["stream".to_string()]),
2876            ..Default::default()
2877        };
2878        let config = LiveNodeConfig {
2879            environment: Environment::Sandbox,
2880            msgbus: Some(msgbus_config),
2881            exec_engine: crate::config::LiveExecEngineConfig {
2882                reconciliation: false,
2883                ..Default::default()
2884            },
2885            delay_post_stop: Duration::ZERO,
2886            timeout_connection: Duration::from_millis(500),
2887            timeout_disconnection: Duration::from_millis(500),
2888            ..Default::default()
2889        };
2890        let mut node = LiveNodeBuilder::from_config(config)
2891            .unwrap()
2892            .with_external_msgbus_factory(Box::new(factory))
2893            .build()
2894            .expect("node builds with external message bus factory");
2895
2896        msgbus::publish_quote("data.quotes.TEST".into(), &quote);
2897        {
2898            let publications = publications.lock().unwrap();
2899            assert_eq!(publications.len(), 1);
2900            assert_eq!(publications[0].topic, "data.quotes.TEST");
2901            assert_eq!(
2902                serde_json::from_slice::<QuoteTick>(&publications[0].payload)
2903                    .expect("JSON payload must decode as QuoteTick"),
2904                quote
2905            );
2906        }
2907
2908        let received = Rc::new(RefCell::new(Vec::<QuoteTick>::new()));
2909        let handle = node.handle();
2910        let handler = TypedHandler::from({
2911            let received = received.clone();
2912            let handle = handle.clone();
2913            move |quote: &QuoteTick| {
2914                received.borrow_mut().push(*quote);
2915                handle.stop();
2916            }
2917        });
2918        msgbus::subscribe_quotes("data.quotes.*".into(), handler, None);
2919        msgbus::get_message_bus()
2920            .borrow_mut()
2921            .add_streaming_type(BusPayloadType::QuoteTick);
2922
2923        let payload =
2924            Bytes::from(serde_json::to_vec(&quote).expect("QuoteTick should serialize as JSON"));
2925        let message = BusMessage::with_str_topic(
2926            "data.quotes.TEST",
2927            BusPayloadType::QuoteTick,
2928            payload,
2929            SerializationEncoding::Json,
2930        );
2931
2932        tokio::time::timeout(Duration::from_secs(5), async {
2933            let run = node.run();
2934            tokio::pin!(run);
2935
2936            let drive = async {
2937                for _ in 0..100 {
2938                    if handle.is_running() {
2939                        break;
2940                    }
2941                    tokio::time::sleep(Duration::from_millis(10)).await;
2942                }
2943                assert!(handle.is_running(), "node should reach running state");
2944
2945                tx.send(message)
2946                    .await
2947                    .expect("external ingress receiver should be open");
2948
2949                for _ in 0..100 {
2950                    if received.borrow().len() == 1 {
2951                        break;
2952                    }
2953                    tokio::time::sleep(Duration::from_millis(10)).await;
2954                }
2955                assert_eq!(*received.borrow(), vec![quote]);
2956            };
2957
2958            tokio::select! {
2959                biased;
2960
2961                () = drive => {}
2962                result = &mut run => {
2963                    panic!("node stopped before factory ingress was republished: {result:?}");
2964                }
2965            }
2966
2967            run.await.expect("node should stop cleanly");
2968        })
2969        .await
2970        .expect("live node should republish factory ingress and stop before timeout");
2971
2972        assert_eq!(handle.state(), NodeState::Stopped);
2973        assert!(closed.load(Ordering::Relaxed));
2974        msgbus::get_message_bus().borrow_mut().dispose();
2975    }
2976
2977    #[rstest]
2978    #[tokio::test(flavor = "current_thread")]
2979    async fn test_builder_with_external_msgbus_factory_without_streams_runs_without_ingress() {
2980        let quote = QuoteTick::default();
2981        let publications = Arc::new(Mutex::new(Vec::new()));
2982        let closed = Arc::new(AtomicBool::new(false));
2983        let factory = CapturingBackingFactory::new(publications.clone(), closed.clone(), None);
2984        let config = LiveNodeConfig {
2985            environment: Environment::Sandbox,
2986            msgbus: Some(MessageBusConfig::default()),
2987            exec_engine: crate::config::LiveExecEngineConfig {
2988                reconciliation: false,
2989                ..Default::default()
2990            },
2991            delay_post_stop: Duration::ZERO,
2992            timeout_connection: Duration::from_millis(500),
2993            timeout_disconnection: Duration::from_millis(500),
2994            ..Default::default()
2995        };
2996        let mut node = LiveNodeBuilder::from_config(config)
2997            .unwrap()
2998            .with_external_msgbus_factory(Box::new(factory))
2999            .build()
3000            .expect("node builds with egress-only message bus factory");
3001        let handle = node.handle();
3002
3003        msgbus::publish_quote("data.quotes.TEST".into(), &quote);
3004        {
3005            let publications = publications.lock().unwrap();
3006            assert_eq!(publications.len(), 1);
3007            assert_eq!(publications[0].topic, "data.quotes.TEST");
3008        }
3009
3010        tokio::time::timeout(Duration::from_secs(5), async {
3011            let run = node.run();
3012            tokio::pin!(run);
3013
3014            let drive = async {
3015                for _ in 0..100 {
3016                    if handle.is_running() {
3017                        break;
3018                    }
3019                    tokio::time::sleep(Duration::from_millis(10)).await;
3020                }
3021                assert!(handle.is_running(), "node should reach running state");
3022                handle.stop();
3023            };
3024
3025            tokio::select! {
3026                biased;
3027
3028                () = drive => {}
3029                result = &mut run => {
3030                    panic!("node stopped before egress-only factory run was observed: {result:?}");
3031                }
3032            }
3033
3034            run.await.expect("node should stop cleanly");
3035        })
3036        .await
3037        .expect("live node should run without external ingress before timeout");
3038
3039        assert_eq!(handle.state(), NodeState::Stopped);
3040        msgbus::get_message_bus().borrow_mut().dispose();
3041        assert!(closed.load(Ordering::Relaxed));
3042    }
3043
3044    #[rstest]
3045    fn test_builder_with_external_msgbus_factory_rejects_injected_surfaces() {
3046        let (external_egress, _publications, _closed) = CapturingExternalEgress::new();
3047        let egress_factory = CapturingBackingFactory::new(
3048            Arc::new(Mutex::new(Vec::new())),
3049            Arc::new(AtomicBool::new(false)),
3050            None,
3051        );
3052        let egress_error = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3053            .unwrap()
3054            .with_external_msgbus_factory(Box::new(egress_factory))
3055            .with_external_msgbus_egress(Box::new(external_egress))
3056            .build()
3057            .expect_err("builder should reject factory plus injected egress");
3058
3059        assert!(
3060            egress_error
3061                .to_string()
3062                .contains("cannot be combined with injected egress or ingress")
3063        );
3064
3065        let (_tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
3066        let ingress_factory = CapturingBackingFactory::new(
3067            Arc::new(Mutex::new(Vec::new())),
3068            Arc::new(AtomicBool::new(false)),
3069            None,
3070        );
3071        let ingress = CapturingExternalIngress::new(rx, Rc::new(Cell::new(false)));
3072        let ingress_error = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3073            .unwrap()
3074            .with_external_msgbus_factory(Box::new(ingress_factory))
3075            .with_external_ingress(Box::new(ingress))
3076            .build()
3077            .expect_err("builder should reject factory plus injected ingress");
3078
3079        assert!(
3080            ingress_error
3081                .to_string()
3082                .contains("cannot be combined with injected egress or ingress")
3083        );
3084    }
3085
3086    #[rstest]
3087    #[tokio::test(flavor = "current_thread")]
3088    async fn test_run_republishes_external_ingress_on_local_msgbus() {
3089        let quote = QuoteTick::default();
3090        let received = Rc::new(RefCell::new(Vec::<QuoteTick>::new()));
3091        let payload =
3092            Bytes::from(serde_json::to_vec(&quote).expect("QuoteTick should serialize as JSON"));
3093        let message = BusMessage::with_str_topic(
3094            "data.quotes.TEST",
3095            BusPayloadType::QuoteTick,
3096            payload,
3097            SerializationEncoding::Json,
3098        );
3099        let (tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
3100        let closed = Rc::new(Cell::new(false));
3101        let ingress = CapturingExternalIngress::new(rx, closed.clone());
3102        let config = LiveNodeConfig {
3103            environment: Environment::Sandbox,
3104            exec_engine: crate::config::LiveExecEngineConfig {
3105                reconciliation: false,
3106                ..Default::default()
3107            },
3108            delay_post_stop: Duration::ZERO,
3109            timeout_connection: Duration::from_millis(500),
3110            timeout_disconnection: Duration::from_millis(500),
3111            ..Default::default()
3112        };
3113        let mut node = LiveNodeBuilder::from_config(config)
3114            .unwrap()
3115            .with_external_ingress(Box::new(ingress))
3116            .build()
3117            .expect("node builds with external message bus ingress");
3118        let handle = node.handle();
3119        let handler = TypedHandler::from({
3120            let received = received.clone();
3121            move |quote: &QuoteTick| {
3122                received.borrow_mut().push(*quote);
3123            }
3124        });
3125        msgbus::subscribe_quotes("data.quotes.*".into(), handler, None);
3126        msgbus::get_message_bus()
3127            .borrow_mut()
3128            .add_streaming_type(BusPayloadType::QuoteTick);
3129
3130        tokio::time::timeout(Duration::from_secs(5), async {
3131            let run = node.run();
3132            tokio::pin!(run);
3133
3134            let drive = async {
3135                for _ in 0..100 {
3136                    if handle.is_running() {
3137                        break;
3138                    }
3139                    tokio::time::sleep(Duration::from_millis(10)).await;
3140                }
3141                assert!(handle.is_running(), "node should reach running state");
3142
3143                tx.send(message)
3144                    .await
3145                    .expect("external ingress receiver should be open");
3146
3147                for _ in 0..100 {
3148                    if received.borrow().len() == 1 {
3149                        break;
3150                    }
3151                    tokio::time::sleep(Duration::from_millis(10)).await;
3152                }
3153                assert_eq!(*received.borrow(), vec![quote]);
3154                handle.stop();
3155            };
3156
3157            tokio::select! {
3158                biased;
3159
3160                () = drive => {}
3161                result = &mut run => {
3162                    panic!("node stopped before external message was republished: {result:?}");
3163                }
3164            }
3165
3166            run.await.expect("node should stop cleanly");
3167        })
3168        .await
3169        .expect("live node should republish ingress and stop before timeout");
3170
3171        assert_eq!(handle.state(), NodeState::Stopped);
3172        assert!(closed.get());
3173        msgbus::get_message_bus().borrow_mut().dispose();
3174    }
3175
3176    #[rstest]
3177    #[tokio::test(flavor = "current_thread")]
3178    async fn test_run_closes_external_ingress_when_receiver_closes() {
3179        let (tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
3180        let closed = Rc::new(Cell::new(false));
3181        let ingress = CapturingExternalIngress::new(rx, closed.clone());
3182        let config = LiveNodeConfig {
3183            environment: Environment::Sandbox,
3184            exec_engine: crate::config::LiveExecEngineConfig {
3185                reconciliation: false,
3186                ..Default::default()
3187            },
3188            delay_post_stop: Duration::ZERO,
3189            timeout_connection: Duration::from_millis(500),
3190            timeout_disconnection: Duration::from_millis(500),
3191            ..Default::default()
3192        };
3193        let mut node = LiveNodeBuilder::from_config(config)
3194            .unwrap()
3195            .with_external_ingress(Box::new(ingress))
3196            .build()
3197            .expect("node builds with external message bus ingress");
3198        let handle = node.handle();
3199
3200        tokio::time::timeout(Duration::from_secs(5), async {
3201            let run = node.run();
3202            tokio::pin!(run);
3203
3204            let drive = async {
3205                for _ in 0..100 {
3206                    if handle.is_running() {
3207                        break;
3208                    }
3209                    tokio::time::sleep(Duration::from_millis(10)).await;
3210                }
3211                assert!(handle.is_running(), "node should reach running state");
3212
3213                drop(tx);
3214
3215                for _ in 0..100 {
3216                    if closed.get() {
3217                        break;
3218                    }
3219                    tokio::time::sleep(Duration::from_millis(10)).await;
3220                }
3221                assert!(closed.get(), "external ingress should close");
3222                assert!(
3223                    handle.is_running(),
3224                    "node should keep running after ingress closes"
3225                );
3226                handle.stop();
3227            };
3228
3229            tokio::select! {
3230                biased;
3231
3232                () = drive => {}
3233                result = &mut run => {
3234                    panic!("node stopped before ingress close was observed: {result:?}");
3235                }
3236            }
3237
3238            run.await.expect("node should stop cleanly");
3239        })
3240        .await
3241        .expect("live node should close ingress and stop before timeout");
3242
3243        assert_eq!(handle.state(), NodeState::Stopped);
3244    }
3245
3246    #[rstest]
3247    #[tokio::test(flavor = "current_thread")]
3248    async fn test_run_aborts_startup_when_external_ingress_receiver_unavailable() {
3249        let closed = Rc::new(Cell::new(false));
3250        let ingress = FailingExternalIngress::new(closed.clone());
3251        let config = LiveNodeConfig {
3252            environment: Environment::Sandbox,
3253            exec_engine: crate::config::LiveExecEngineConfig {
3254                reconciliation: false,
3255                ..Default::default()
3256            },
3257            delay_post_stop: Duration::ZERO,
3258            timeout_connection: Duration::from_millis(500),
3259            timeout_disconnection: Duration::from_millis(500),
3260            ..Default::default()
3261        };
3262        let mut node = LiveNodeBuilder::from_config(config)
3263            .unwrap()
3264            .with_external_ingress(Box::new(ingress))
3265            .build()
3266            .expect("node builds with external message bus ingress");
3267        let handle = node.handle();
3268
3269        let err = node.run().await.expect_err("run should fail");
3270
3271        assert!(
3272            err.to_string()
3273                .contains("external ingress receiver unavailable")
3274        );
3275        assert_eq!(handle.state(), NodeState::Stopped);
3276        assert!(closed.get());
3277    }
3278
3279    #[cfg(feature = "python")]
3280    #[rstest]
3281    fn test_node_build_and_initial_state() {
3282        let node = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3283            .unwrap()
3284            .with_name("TestNode")
3285            .build()
3286            .unwrap();
3287
3288        assert_eq!(node.state(), NodeState::Idle);
3289        assert!(!node.is_running());
3290        assert_eq!(node.environment(), Environment::Sandbox);
3291        assert_eq!(node.trader_id(), TraderId::from("TRADER-001"));
3292    }
3293
3294    #[cfg(feature = "python")]
3295    #[rstest]
3296    fn test_node_build_replaces_stale_runner_senders() {
3297        replace_data_cmd_sender(Arc::new(SyncDataCommandSender));
3298        replace_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
3299
3300        let first = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3301            .unwrap()
3302            .with_name("FirstNode")
3303            .build()
3304            .unwrap();
3305
3306        assert_eq!(first.state(), NodeState::Idle);
3307        drop(first);
3308
3309        let second = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3310            .unwrap()
3311            .with_name("SecondNode")
3312            .build()
3313            .unwrap();
3314
3315        assert_eq!(second.state(), NodeState::Idle);
3316        assert!(!second.is_running());
3317    }
3318
3319    #[cfg(feature = "python")]
3320    #[rstest]
3321    fn test_node_handle_reflects_node_state() {
3322        let node = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3323            .unwrap()
3324            .with_name("TestNode")
3325            .build()
3326            .unwrap();
3327
3328        let handle = node.handle();
3329
3330        assert_eq!(handle.state(), NodeState::Idle);
3331        assert!(!handle.is_running());
3332    }
3333
3334    #[rstest]
3335    fn test_pending_drain_data_returns_false_when_empty() {
3336        let mut pending = PendingEvents::default();
3337
3338        assert!(!pending.drain_data());
3339    }
3340
3341    #[rstest]
3342    fn test_pending_drain_data_returns_true_when_non_empty() {
3343        use nautilus_model::instruments::{InstrumentAny, stubs::crypto_perpetual_ethusdt};
3344
3345        let mut pending = PendingEvents::default();
3346        pending
3347            .data_evts
3348            .push(DataEvent::Instrument(InstrumentAny::CryptoPerpetual(
3349                crypto_perpetual_ethusdt(),
3350            )));
3351
3352        assert!(pending.drain_data());
3353        assert!(pending.data_evts.is_empty());
3354    }
3355
3356    fn stub_data_event() -> DataEvent {
3357        use nautilus_model::instruments::{InstrumentAny, stubs::crypto_perpetual_ethusdt};
3358
3359        DataEvent::Instrument(InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt()))
3360    }
3361
3362    fn stub_data_command() -> DataCommand {
3363        use nautilus_common::messages::data::{SubscribeCommand, subscribe::SubscribeInstruments};
3364        use nautilus_core::{UUID4, UnixNanos};
3365        use nautilus_model::identifiers::Venue;
3366
3367        DataCommand::Subscribe(SubscribeCommand::Instruments(SubscribeInstruments::new(
3368            None,
3369            Venue::from("TEST"),
3370            UUID4::new(),
3371            UnixNanos::default(),
3372            None,
3373            None,
3374        )))
3375    }
3376
3377    #[rstest]
3378    fn test_flush_pending_data_drains_events_and_commands() {
3379        let (evt_tx, mut evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3380        let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3381
3382        let mut pending = PendingEvents::default();
3383
3384        // Pre-load pending (items captured by the select loop)
3385        pending.data_evts.push(stub_data_event());
3386        pending.data_cmds.push(stub_data_command());
3387
3388        // Pre-load channels (items missed by the select loop)
3389        evt_tx.send(stub_data_event()).unwrap();
3390        cmd_tx.send(stub_data_command()).unwrap();
3391
3392        flush_pending_data(&mut pending, &mut evt_rx, &mut cmd_rx);
3393
3394        assert!(pending.data_evts.is_empty());
3395        assert!(pending.data_cmds.is_empty());
3396        assert!(evt_rx.try_recv().is_err());
3397        assert!(cmd_rx.try_recv().is_err());
3398    }
3399
3400    #[rstest]
3401    fn test_flush_pending_data_drains_mixed_sources() {
3402        let (evt_tx, mut evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3403        let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3404
3405        let mut pending = PendingEvents::default();
3406
3407        // First pass: pending has an event, channel has a command
3408        pending.data_evts.push(stub_data_event());
3409        cmd_tx.send(stub_data_command()).unwrap();
3410
3411        // Second pass: channel has items that simulate arrival during first drain
3412        evt_tx.send(stub_data_event()).unwrap();
3413        evt_tx.send(stub_data_event()).unwrap();
3414        cmd_tx.send(stub_data_command()).unwrap();
3415
3416        flush_pending_data(&mut pending, &mut evt_rx, &mut cmd_rx);
3417
3418        assert!(pending.data_evts.is_empty());
3419        assert!(pending.data_cmds.is_empty());
3420        assert!(evt_rx.try_recv().is_err());
3421        assert!(cmd_rx.try_recv().is_err());
3422    }
3423
3424    fn stub_time_event_handler() -> TimeEventHandler {
3425        use std::rc::Rc;
3426
3427        use nautilus_common::timer::{TimeEvent, TimeEventCallback, TimeEventHandler};
3428        use nautilus_core::{UUID4, UnixNanos};
3429        use ustr::Ustr;
3430
3431        TimeEventHandler::new(
3432            TimeEvent::new(
3433                Ustr::from("test-timer"),
3434                UUID4::new(),
3435                UnixNanos::default(),
3436                UnixNanos::default(),
3437            ),
3438            TimeEventCallback::RustLocal(Rc::new(|_| {})),
3439        )
3440    }
3441
3442    fn stub_trading_command() -> TradingCommand {
3443        use nautilus_common::messages::execution::query::QueryAccount;
3444        use nautilus_core::{UUID4, UnixNanos};
3445        use nautilus_model::identifiers::AccountId;
3446
3447        TradingCommand::QueryAccount(QueryAccount::new(
3448            TraderId::from("TESTER-001"),
3449            None,
3450            AccountId::from("TEST-001"),
3451            UUID4::new(),
3452            UnixNanos::default(),
3453            None,
3454            None, // correlation_id
3455        ))
3456    }
3457
3458    fn stub_exec_event() -> ExecutionEvent {
3459        use nautilus_model::{
3460            enums::{LiquiditySide, OrderSide},
3461            identifiers::{AccountId, InstrumentId, TradeId, VenueOrderId},
3462            reports::FillReport,
3463            types::{Money, Price, Quantity},
3464        };
3465
3466        ExecutionEvent::Report(ExecutionReport::Fill(Box::new(FillReport::new(
3467            AccountId::from("TEST-001"),
3468            InstrumentId::from("TEST.VENUE"),
3469            VenueOrderId::from("V-001"),
3470            TradeId::from("T-001"),
3471            OrderSide::Buy,
3472            Quantity::from("1.0"),
3473            Price::from("100.0"),
3474            Money::from("0.01 USD"),
3475            LiquiditySide::Maker,
3476            None,
3477            None,
3478            nautilus_core::UnixNanos::default(),
3479            nautilus_core::UnixNanos::default(),
3480            None,
3481        ))))
3482    }
3483
3484    #[rstest]
3485    fn test_flush_all_pending_drains_buffered_channels() {
3486        let (time_tx, mut time_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
3487        let (data_evt_tx, mut data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3488        let (data_cmd_tx, mut data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3489        let (exec_evt_tx, mut exec_evt_rx) =
3490            tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3491        let (exec_cmd_tx, mut exec_cmd_rx) =
3492            tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
3493
3494        let mut pending = PendingEvents::default();
3495
3496        // Pre-load pending with data items
3497        pending.data_evts.push(stub_data_event());
3498        pending.data_cmds.push(stub_data_command());
3499
3500        // Pre-load all channel types
3501        time_tx.send(stub_time_event_handler()).unwrap();
3502        data_evt_tx.send(stub_data_event()).unwrap();
3503        data_cmd_tx.send(stub_data_command()).unwrap();
3504        exec_evt_tx.send(stub_exec_event()).unwrap();
3505        exec_cmd_tx.send(stub_trading_command()).unwrap();
3506
3507        flush_all_pending(
3508            &mut pending,
3509            &mut time_rx,
3510            &mut data_evt_rx,
3511            &mut data_cmd_rx,
3512            &mut exec_evt_rx,
3513            &mut exec_cmd_rx,
3514        );
3515
3516        assert!(pending.data_evts.is_empty());
3517        assert!(pending.data_cmds.is_empty());
3518        assert!(pending.exec_reports.is_empty());
3519        assert!(pending.exec_cmds.is_empty());
3520        assert!(pending.order_evts.is_empty());
3521        assert!(time_rx.try_recv().is_err());
3522        assert!(data_evt_rx.try_recv().is_err());
3523        assert!(data_cmd_rx.try_recv().is_err());
3524        assert!(exec_evt_rx.try_recv().is_err());
3525        assert!(exec_cmd_rx.try_recv().is_err());
3526    }
3527
3528    fn stub_order_event() -> ExecutionEvent {
3529        use nautilus_model::events::order::spec::OrderSubmittedSpec;
3530
3531        ExecutionEvent::Order(OrderEventAny::Submitted(
3532            OrderSubmittedSpec::builder().build(),
3533        ))
3534    }
3535
3536    fn stub_account_event() -> ExecutionEvent {
3537        use nautilus_core::{UUID4, UnixNanos};
3538        use nautilus_model::{
3539            enums::AccountType, events::account::state::AccountState, identifiers::AccountId,
3540        };
3541
3542        ExecutionEvent::Account(AccountState::new(
3543            AccountId::from("TEST-001"),
3544            AccountType::Cash,
3545            vec![],
3546            vec![],
3547            true,
3548            UUID4::new(),
3549            UnixNanos::default(),
3550            UnixNanos::default(),
3551            None,
3552        ))
3553    }
3554
3555    #[rstest]
3556    fn test_flush_all_pending_routes_order_event_to_order_evts() {
3557        let (_time_tx, mut time_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
3558        let (_data_evt_tx, mut data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3559        let (_data_cmd_tx, mut data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3560        let (exec_evt_tx, mut exec_evt_rx) =
3561            tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3562        let (_exec_cmd_tx, mut exec_cmd_rx) =
3563            tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
3564
3565        let mut pending = PendingEvents::default();
3566
3567        exec_evt_tx.send(stub_order_event()).unwrap();
3568        exec_evt_tx.send(stub_exec_event()).unwrap();
3569
3570        flush_all_pending(
3571            &mut pending,
3572            &mut time_rx,
3573            &mut data_evt_rx,
3574            &mut data_cmd_rx,
3575            &mut exec_evt_rx,
3576            &mut exec_cmd_rx,
3577        );
3578
3579        // Both order and report events are drained by pending.drain()
3580        assert!(pending.order_evts.is_empty());
3581        assert!(pending.exec_reports.is_empty());
3582        assert!(exec_evt_rx.try_recv().is_err());
3583    }
3584
3585    #[rstest]
3586    fn test_flush_all_pending_routes_account_event_immediately() {
3587        let (_time_tx, mut time_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
3588        let (_data_evt_tx, mut data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3589        let (_data_cmd_tx, mut data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3590        let (exec_evt_tx, mut exec_evt_rx) =
3591            tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3592        let (_exec_cmd_tx, mut exec_cmd_rx) =
3593            tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
3594
3595        let mut pending = PendingEvents::default();
3596
3597        exec_evt_tx.send(stub_account_event()).unwrap();
3598
3599        flush_all_pending(
3600            &mut pending,
3601            &mut time_rx,
3602            &mut data_evt_rx,
3603            &mut data_cmd_rx,
3604            &mut exec_evt_rx,
3605            &mut exec_cmd_rx,
3606        );
3607
3608        // Account events are forwarded immediately, never buffered in pending
3609        assert!(pending.exec_reports.is_empty());
3610        assert!(pending.order_evts.is_empty());
3611        assert!(pending.exec_cmds.is_empty());
3612        assert!(exec_evt_rx.try_recv().is_err());
3613    }
3614
3615    #[rstest]
3616    fn test_pending_is_empty_when_default() {
3617        let pending = PendingEvents::default();
3618
3619        assert!(pending.is_empty());
3620    }
3621
3622    #[rstest]
3623    fn test_pending_is_empty_false_with_data_evt() {
3624        let mut pending = PendingEvents::default();
3625        pending.data_evts.push(stub_data_event());
3626
3627        assert!(!pending.is_empty());
3628    }
3629
3630    #[rstest]
3631    fn test_pending_is_empty_false_with_data_cmd() {
3632        let mut pending = PendingEvents::default();
3633        pending.data_cmds.push(stub_data_command());
3634
3635        assert!(!pending.is_empty());
3636    }
3637
3638    #[rstest]
3639    fn test_pending_is_empty_false_with_exec_cmd() {
3640        let mut pending = PendingEvents::default();
3641        pending.exec_cmds.push(stub_trading_command());
3642
3643        assert!(!pending.is_empty());
3644    }
3645
3646    #[rstest]
3647    fn test_pending_is_empty_false_with_exec_report() {
3648        let mut pending = PendingEvents::default();
3649
3650        if let ExecutionEvent::Report(report) = stub_exec_event() {
3651            pending.exec_reports.push(report);
3652        }
3653
3654        assert!(!pending.is_empty());
3655    }
3656
3657    #[rstest]
3658    fn test_pending_is_empty_false_with_order_evt() {
3659        let mut pending = PendingEvents::default();
3660
3661        if let ExecutionEvent::Order(order_evt) = stub_order_event() {
3662            pending.order_evts.push(order_evt);
3663        }
3664
3665        assert!(!pending.is_empty());
3666    }
3667
3668    fn stub_submitted_batch_event() -> ExecutionEvent {
3669        use nautilus_model::{
3670            events::{OrderSubmittedBatch, order::spec::OrderSubmittedSpec},
3671            identifiers::ClientOrderId,
3672        };
3673
3674        let events = vec![
3675            OrderSubmittedSpec::builder()
3676                .client_order_id(ClientOrderId::from("O-001"))
3677                .build(),
3678            OrderSubmittedSpec::builder()
3679                .client_order_id(ClientOrderId::from("O-002"))
3680                .build(),
3681        ];
3682
3683        ExecutionEvent::OrderSubmittedBatch(OrderSubmittedBatch::new(events))
3684    }
3685
3686    fn stub_canceled_batch_event() -> ExecutionEvent {
3687        use nautilus_model::{
3688            events::{OrderCanceledBatch, order::spec::OrderCanceledSpec},
3689            identifiers::ClientOrderId,
3690        };
3691
3692        let events = vec![
3693            OrderCanceledSpec::builder()
3694                .client_order_id(ClientOrderId::from("O-001"))
3695                .build(),
3696            OrderCanceledSpec::builder()
3697                .client_order_id(ClientOrderId::from("O-002"))
3698                .build(),
3699        ];
3700
3701        ExecutionEvent::OrderCanceledBatch(OrderCanceledBatch::new(events))
3702    }
3703
3704    #[rstest]
3705    fn test_flush_all_pending_buffers_submitted_batch_as_individual_events() {
3706        let (_time_tx, mut time_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
3707        let (_data_evt_tx, mut data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3708        let (_data_cmd_tx, mut data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3709        let (exec_evt_tx, mut exec_evt_rx) =
3710            tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3711        let (_exec_cmd_tx, mut exec_cmd_rx) =
3712            tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
3713
3714        let mut pending = PendingEvents::default();
3715
3716        exec_evt_tx.send(stub_submitted_batch_event()).unwrap();
3717
3718        flush_all_pending(
3719            &mut pending,
3720            &mut time_rx,
3721            &mut data_evt_rx,
3722            &mut data_cmd_rx,
3723            &mut exec_evt_rx,
3724            &mut exec_cmd_rx,
3725        );
3726
3727        // Batch should be unpacked into individual Submitted events then drained
3728        assert!(pending.order_evts.is_empty());
3729        assert!(exec_evt_rx.try_recv().is_err());
3730    }
3731
3732    #[rstest]
3733    fn test_flush_all_pending_buffers_canceled_batch_as_individual_events() {
3734        let (_time_tx, mut time_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
3735        let (_data_evt_tx, mut data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3736        let (_data_cmd_tx, mut data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3737        let (exec_evt_tx, mut exec_evt_rx) =
3738            tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3739        let (_exec_cmd_tx, mut exec_cmd_rx) =
3740            tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
3741
3742        let mut pending = PendingEvents::default();
3743
3744        exec_evt_tx.send(stub_canceled_batch_event()).unwrap();
3745
3746        flush_all_pending(
3747            &mut pending,
3748            &mut time_rx,
3749            &mut data_evt_rx,
3750            &mut data_cmd_rx,
3751            &mut exec_evt_rx,
3752            &mut exec_cmd_rx,
3753        );
3754
3755        // Batch should be unpacked into individual Canceled events then drained
3756        assert!(pending.order_evts.is_empty());
3757        assert!(exec_evt_rx.try_recv().is_err());
3758    }
3759
3760    #[rstest]
3761    fn test_flush_all_pending_expands_batch_into_order_evts_before_drain() {
3762        use nautilus_model::identifiers::ClientOrderId;
3763
3764        let (exec_evt_tx, mut exec_evt_rx) =
3765            tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3766
3767        exec_evt_tx.send(stub_canceled_batch_event()).unwrap();
3768
3769        let mut pending = PendingEvents::default();
3770
3771        // Manually replicate what flush_all_pending does before drain
3772        while let Ok(evt) = exec_evt_rx.try_recv() {
3773            match evt {
3774                ExecutionEvent::Account(_) => {
3775                    AsyncRunner::handle_exec_event(evt);
3776                }
3777                ExecutionEvent::Report(report) => {
3778                    pending.exec_reports.push(report);
3779                }
3780                ExecutionEvent::Order(order_evt) => {
3781                    pending.order_evts.push(order_evt);
3782                }
3783                ExecutionEvent::OrderSubmittedBatch(batch) => {
3784                    for submitted in batch {
3785                        pending.order_evts.push(OrderEventAny::Submitted(submitted));
3786                    }
3787                }
3788                ExecutionEvent::OrderAcceptedBatch(batch) => {
3789                    for accepted in batch {
3790                        pending.order_evts.push(OrderEventAny::Accepted(accepted));
3791                    }
3792                }
3793                ExecutionEvent::OrderCanceledBatch(batch) => {
3794                    for canceled in batch {
3795                        pending.order_evts.push(OrderEventAny::Canceled(canceled));
3796                    }
3797                }
3798            }
3799        }
3800
3801        assert_eq!(pending.order_evts.len(), 2);
3802        assert!(
3803            matches!(&pending.order_evts[0], OrderEventAny::Canceled(c) if c.client_order_id == ClientOrderId::from("O-001"))
3804        );
3805        assert!(
3806            matches!(&pending.order_evts[1], OrderEventAny::Canceled(c) if c.client_order_id == ClientOrderId::from("O-002"))
3807        );
3808    }
3809
3810    #[derive(Debug)]
3811    struct CapturedEgressMessage {
3812        topic: String,
3813        payload: Bytes,
3814    }
3815
3816    type CapturedEgressMessages = Rc<RefCell<Vec<CapturedEgressMessage>>>;
3817    type SharedClosed = Rc<Cell<bool>>;
3818
3819    #[derive(Debug)]
3820    struct CapturingExternalIngress {
3821        rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
3822        closed: SharedClosed,
3823    }
3824
3825    impl CapturingExternalIngress {
3826        fn new(rx: tokio::sync::mpsc::Receiver<BusMessage>, closed: SharedClosed) -> Self {
3827            Self {
3828                rx: Some(rx),
3829                closed,
3830            }
3831        }
3832    }
3833
3834    impl MessageBusExternalIngress for CapturingExternalIngress {
3835        fn is_closed(&self) -> bool {
3836            self.closed.get()
3837        }
3838
3839        fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
3840            self.rx
3841                .take()
3842                .ok_or_else(|| anyhow::anyhow!("external ingress receiver already taken"))
3843        }
3844
3845        fn close(&mut self) {
3846            self.closed.set(true);
3847        }
3848    }
3849
3850    #[derive(Debug)]
3851    struct FailingExternalIngress {
3852        closed: SharedClosed,
3853    }
3854
3855    impl FailingExternalIngress {
3856        fn new(closed: SharedClosed) -> Self {
3857            Self { closed }
3858        }
3859    }
3860
3861    impl MessageBusExternalIngress for FailingExternalIngress {
3862        fn is_closed(&self) -> bool {
3863            self.closed.get()
3864        }
3865
3866        fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
3867            anyhow::bail!("external ingress receiver unavailable")
3868        }
3869
3870        fn close(&mut self) {
3871            self.closed.set(true);
3872        }
3873    }
3874
3875    struct CapturingExternalEgress {
3876        publications: CapturedEgressMessages,
3877        closed: SharedClosed,
3878    }
3879
3880    impl CapturingExternalEgress {
3881        fn new() -> (Self, CapturedEgressMessages, SharedClosed) {
3882            let publications = Rc::new(RefCell::new(Vec::new()));
3883            let closed = Rc::new(Cell::new(false));
3884            (
3885                Self {
3886                    publications: publications.clone(),
3887                    closed: closed.clone(),
3888                },
3889                publications,
3890                closed,
3891            )
3892        }
3893    }
3894
3895    impl MessageBusExternalEgress for CapturingExternalEgress {
3896        fn is_closed(&self) -> bool {
3897            self.closed.get()
3898        }
3899
3900        fn publish(&self, message: BusMessage) {
3901            self.publications.borrow_mut().push(CapturedEgressMessage {
3902                topic: message.topic.to_string(),
3903                payload: message.payload,
3904            });
3905        }
3906
3907        fn close(&mut self) {
3908            self.closed.set(true);
3909        }
3910    }
3911
3912    struct CapturingBackingFactory {
3913        publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
3914        closed: Arc<AtomicBool>,
3915        rx: Mutex<Option<tokio::sync::mpsc::Receiver<BusMessage>>>,
3916    }
3917
3918    impl CapturingBackingFactory {
3919        fn new(
3920            publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
3921            closed: Arc<AtomicBool>,
3922            rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
3923        ) -> Self {
3924            Self {
3925                publications,
3926                closed,
3927                rx: Mutex::new(rx),
3928            }
3929        }
3930    }
3931
3932    impl Debug for CapturingBackingFactory {
3933        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3934            f.debug_struct(stringify!(CapturingBackingFactory))
3935                .finish_non_exhaustive()
3936        }
3937    }
3938
3939    impl MessageBusBackingFactory for CapturingBackingFactory {
3940        fn create(
3941            &self,
3942            _trader_id: TraderId,
3943            _instance_id: UUID4,
3944            _config: MessageBusConfig,
3945        ) -> anyhow::Result<Box<dyn MessageBusBacking>> {
3946            let rx = self.rx.lock().unwrap().take();
3947            Ok(Box::new(CapturingBacking {
3948                publications: self.publications.clone(),
3949                closed: self.closed.clone(),
3950                rx,
3951            }))
3952        }
3953    }
3954
3955    struct CapturingBacking {
3956        publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
3957        closed: Arc<AtomicBool>,
3958        rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
3959    }
3960
3961    impl MessageBusBacking for CapturingBacking {
3962        fn is_closed(&self) -> bool {
3963            self.closed.load(Ordering::Relaxed)
3964        }
3965
3966        fn publish(&self, message: BusMessage) {
3967            self.publications
3968                .lock()
3969                .unwrap()
3970                .push(CapturedEgressMessage {
3971                    topic: message.topic.to_string(),
3972                    payload: message.payload,
3973                });
3974        }
3975
3976        fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
3977            self.rx
3978                .take()
3979                .ok_or_else(|| anyhow::anyhow!("external ingress receiver unavailable"))
3980        }
3981
3982        fn close(&mut self) {
3983            self.closed.store(true, Ordering::Relaxed);
3984        }
3985    }
3986}