Skip to main content

nautilus_system/
kernel.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//! Kernel construction, component ownership, and run-lifecycle orchestration.
17//!
18//! # Architecture
19//!
20//! [`NautilusKernel`] owns the shared clock, cache, portfolio, trader, order emulator, and data,
21//! risk, and execution engines around an in-process message bus. These components use
22//! `Rc<RefCell<_>>`, so the kernel is not a cross-thread synchronization boundary.
23//!
24//! # Lifecycle
25//!
26//! Construction initializes logging, optional persistence, message-bus handlers, and shutdown
27//! routing. Normal startup starts the engines before initializing the trader. Live callers then
28//! connect data clients, let instrument events populate the cache, connect execution clients, and
29//! call [`NautilusKernel::start_trader`]. Event-store replay instead restores state and skips
30//! engines, clients, trader startup, and live reconciliation.
31//!
32//! Shutdown is split so [`NautilusKernel::stop_trader`] can emit residual events before
33//! [`NautilusKernel::finalize_stop`] saves state, stops engines, cancels timers, and seals the
34//! event-store run. [`NautilusKernel::reset`] retains the assembled system for reuse, while
35//! [`NautilusKernel::dispose`] releases its resources.
36
37#[cfg(feature = "streaming")]
38use std::collections::HashSet;
39use std::{
40    cell::{Cell, Ref, RefCell},
41    fmt::Debug,
42    rc::Rc,
43    time::Duration,
44};
45
46#[cfg(feature = "streaming")]
47use anyhow::Context;
48#[cfg(feature = "streaming")]
49use jiff::tz::TimeZone;
50use nautilus_common::{
51    cache::{Cache, CacheConfig, database::CacheDatabaseAdapter},
52    clock::Clock,
53    component::Component,
54    enums::{ComponentState, Environment},
55    logging::{
56        arm_shutdown_on_error, disarm_shutdown_on_error, headers, init_logging,
57        logger::{LogGuard, LoggerConfig},
58        try_drain_shutdown_on_error_trigger,
59    },
60    messages::system::ShutdownSystem,
61    msgbus::{
62        self, MessageBus, MessagingSwitchboard, ShareableMessageHandler, get_message_bus,
63        set_message_bus,
64    },
65};
66use nautilus_core::{UUID4, UnixNanos};
67use nautilus_data::engine::DataEngine;
68use nautilus_execution::{
69    engine::ExecutionEngine,
70    order_emulator::{adapter::OrderEmulatorAdapter, emulator::OrderEmulator},
71};
72use nautilus_model::identifiers::{ClientId, TraderId};
73#[cfg(feature = "streaming")]
74use nautilus_persistence::backend::feather::{
75    FeatherWriter, FeatherWriterSubscriptions, RotationConfig as WriterRotationConfig,
76};
77use nautilus_portfolio::portfolio::Portfolio;
78use nautilus_risk::engine::RiskEngine;
79use ustr::Ustr;
80
81use crate::{
82    builder::NautilusKernelBuilder,
83    clock_factory::ClockFactory,
84    config::NautilusKernelConfig,
85    event_store::{EventStoreFactory, KernelEventStore, RegisteredComponents},
86    trader::Trader,
87};
88
89/// Core Nautilus system kernel.
90///
91/// Orchestrates data and execution engines, cache, clock, and messaging across environments.
92pub struct NautilusKernel {
93    /// The kernel name (for logging and identification).
94    pub name: String,
95    /// The unique instance identifier for this kernel.
96    pub instance_id: UUID4,
97    /// The machine identifier (hostname or similar).
98    pub machine_id: String,
99    /// The kernel configuration.
100    pub config: Box<dyn NautilusKernelConfig>,
101    /// The shared in-memory cache.
102    pub cache: Rc<RefCell<Cache>>,
103    /// The clock driving the kernel.
104    pub clock: Rc<RefCell<dyn Clock>>,
105    /// The portfolio manager.
106    pub portfolio: Rc<RefCell<Portfolio>>,
107    /// Guard for the logging subsystem (keeps logger thread alive).
108    pub log_guard: LogGuard,
109    /// The data engine instance.
110    pub data_engine: Rc<RefCell<DataEngine>>,
111    /// The risk engine instance.
112    pub risk_engine: Rc<RefCell<RiskEngine>>,
113    /// The execution engine instance.
114    pub exec_engine: Rc<RefCell<ExecutionEngine>>,
115    /// The order emulator for handling emulated orders.
116    pub order_emulator: OrderEmulatorAdapter,
117    /// The trader component (shared for [`Controller`](crate::controller::Controller) access).
118    pub trader: Rc<RefCell<Trader>>,
119    /// The UNIX timestamp (nanoseconds) when the kernel was created.
120    pub ts_created: UnixNanos,
121    /// The UNIX timestamp (nanoseconds) when the kernel was last started.
122    pub ts_started: Option<UnixNanos>,
123    /// The UNIX timestamp (nanoseconds) when the kernel was last shutdown.
124    pub ts_shutdown: Option<UnixNanos>,
125    shutdown_requested: Rc<Cell<bool>>,
126    event_store: Option<Box<dyn KernelEventStore>>,
127    event_store_replay: bool,
128    state_save_armed: bool,
129    #[cfg(feature = "streaming")]
130    streaming_writer: Option<Rc<RefCell<FeatherWriter>>>,
131    #[cfg(feature = "streaming")]
132    streaming_subscriptions: Option<FeatherWriterSubscriptions>,
133}
134
135impl Debug for NautilusKernel {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        f.debug_struct(stringify!(NautilusKernel))
138            .field("name", &self.name)
139            .field("instance_id", &self.instance_id)
140            .field("machine_id", &self.machine_id)
141            .field("environment", &self.config.environment())
142            .finish_non_exhaustive()
143    }
144}
145
146/// Optional construction-time dependencies for [`NautilusKernel`].
147#[derive(Default)]
148pub struct NautilusKernelDependencies {
149    clock_factory: Option<ClockFactory>,
150    cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
151    event_store_factory: Option<EventStoreFactory>,
152}
153
154impl Debug for NautilusKernelDependencies {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        f.debug_struct(stringify!(NautilusKernelDependencies))
157            .field("clock_factory", &self.clock_factory.is_some())
158            .field("cache_database", &self.cache_database.is_some())
159            .field("event_store_factory", &self.event_store_factory.is_some())
160            .finish()
161    }
162}
163
164impl NautilusKernelDependencies {
165    /// Add a clock factory.
166    #[must_use]
167    pub fn with_clock_factory(mut self, clock_factory: Option<ClockFactory>) -> Self {
168        self.clock_factory = clock_factory;
169        self
170    }
171
172    /// Add a cache database adapter.
173    #[must_use]
174    pub fn with_cache_database(
175        mut self,
176        cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
177    ) -> Self {
178        self.cache_database = cache_database;
179        self
180    }
181
182    /// Add an event-store factory.
183    #[must_use]
184    pub fn with_event_store_factory(
185        mut self,
186        event_store_factory: Option<EventStoreFactory>,
187    ) -> Self {
188        self.event_store_factory = event_store_factory;
189        self
190    }
191}
192
193impl NautilusKernel {
194    /// Create a new [`NautilusKernelBuilder`] for fluent configuration.
195    #[must_use]
196    pub const fn builder(
197        name: String,
198        trader_id: TraderId,
199        environment: Environment,
200    ) -> NautilusKernelBuilder {
201        NautilusKernelBuilder::new(name, trader_id, environment)
202    }
203
204    /// Create a new [`NautilusKernel`] instance.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if the kernel fails to initialize.
209    pub fn new<T: NautilusKernelConfig + 'static>(name: String, config: T) -> anyhow::Result<Self> {
210        Self::new_with(name, config, None, None)
211    }
212
213    /// Create a new [`NautilusKernel`] instance with an injected cache database adapter.
214    ///
215    /// The adapter is passed straight to [`Cache::new`] so the kernel can restore
216    /// generic cache state (including snapshot blobs anchored by the event store) from
217    /// the durable backing store on startup, without an external caller pre-seeding the
218    /// in-memory cache.
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if the kernel fails to initialize.
223    pub fn new_with_cache_database<T: NautilusKernelConfig + 'static>(
224        name: String,
225        config: T,
226        cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
227    ) -> anyhow::Result<Self> {
228        Self::new_with(name, config, cache_database, None)
229    }
230
231    /// Create a new [`NautilusKernel`] instance with optional cache database and event store
232    /// injections.
233    ///
234    /// The cache adapter is passed to [`Cache::new`]; the event-store factory is invoked
235    /// with the kernel's clock so the resulting [`KernelEventStore`] implementation shares
236    /// the same time source the kernel uses to stamp `RunStarted`/`RunEnded` and any
237    /// drop-seal fallback timestamp.
238    ///
239    /// # Errors
240    ///
241    /// Returns an error if the kernel fails to initialize or the event-store factory fails.
242    pub fn new_with<T: NautilusKernelConfig + 'static>(
243        name: String,
244        config: T,
245        cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
246        event_store_factory: Option<EventStoreFactory>,
247    ) -> anyhow::Result<Self> {
248        Self::new_with_dependencies(
249            name,
250            config,
251            NautilusKernelDependencies::default()
252                .with_cache_database(cache_database)
253                .with_event_store_factory(event_store_factory),
254        )
255    }
256
257    /// Create a new [`NautilusKernel`] instance with construction-time dependencies.
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if the kernel fails to initialize or an injected factory fails.
262    #[expect(
263        clippy::too_many_lines,
264        reason = "kernel construction keeps initialization order and ownership visible in one place"
265    )]
266    pub fn new_with_dependencies<T: NautilusKernelConfig + 'static>(
267        name: String,
268        config: T,
269        dependencies: NautilusKernelDependencies,
270    ) -> anyhow::Result<Self> {
271        let NautilusKernelDependencies {
272            clock_factory,
273            cache_database,
274            event_store_factory,
275        } = dependencies;
276        let instance_id = config.instance_id().unwrap_or_default();
277        let machine_id = Self::determine_machine_id()?;
278
279        let logger_config = config.logging();
280        let log_guard = Self::initialize_logging(config.trader_id(), instance_id, logger_config)?;
281        headers::log_header(
282            config.trader_id(),
283            &machine_id,
284            instance_id,
285            Ustr::from(&name),
286        );
287
288        log::info!("Building system kernel");
289
290        let clock_factory =
291            clock_factory.unwrap_or_else(|| ClockFactory::for_environment(config.environment()));
292        let clock = clock_factory.clock();
293        let event_store = match event_store_factory {
294            Some(factory) => Some(factory(instance_id, clock.clone())?),
295            None => None,
296        };
297        let cache = Self::initialize_cache(config.cache(), cache_database);
298
299        let msgbus = Rc::new(RefCell::new(MessageBus::new(
300            config.trader_id(),
301            instance_id,
302            Some(name.clone()),
303            None,
304        )));
305        set_message_bus(msgbus);
306
307        if let Some(config) = config.msgbus()
308            && let Some(filter) = config.types_filter
309        {
310            get_message_bus().borrow_mut().set_types_filter(filter);
311        }
312
313        let portfolio = Rc::new(RefCell::new(Portfolio::new(
314            clock.clone(),
315            cache.clone(),
316            config.portfolio(),
317        )));
318
319        let risk_engine = RiskEngine::new(
320            config.risk_engine().unwrap_or_default(),
321            portfolio.borrow().clone_shallow(),
322            clock.clone(),
323            cache.clone(),
324        );
325        let risk_engine = Rc::new(RefCell::new(risk_engine));
326
327        let exec_engine = ExecutionEngine::new(clock.clone(), cache.clone(), config.exec_engine());
328        let exec_engine = Rc::new(RefCell::new(exec_engine));
329
330        let order_emulator = OrderEmulatorAdapter::new(clock.clone(), cache.clone());
331
332        let data_engine = DataEngine::new(clock.clone(), cache.clone(), config.data_engine());
333        #[cfg(feature = "streaming")]
334        let mut data_engine = data_engine;
335        #[cfg(feature = "streaming")]
336        {
337            let mut unnamed_index = 0;
338            let mut catalog_names = HashSet::new();
339
340            for catalog_config in config.catalogs() {
341                let name = catalog_config.name.clone().unwrap_or_else(|| {
342                    let name = format!("catalog_{unnamed_index}");
343                    unnamed_index += 1;
344                    name
345                });
346                anyhow::ensure!(
347                    catalog_names.insert(name.clone()),
348                    "Duplicate data catalog name '{name}'",
349                );
350                let catalog = catalog_config.create_catalog().with_context(|| {
351                    format!(
352                        "Failed to create data catalog from '{}'",
353                        catalog_config.path
354                    )
355                })?;
356                data_engine.register_catalog(catalog, Some(&name));
357            }
358        }
359        let data_engine = Rc::new(RefCell::new(data_engine));
360
361        DataEngine::register_msgbus_handlers(&data_engine);
362        RiskEngine::register_msgbus_handlers(&risk_engine);
363        ExecutionEngine::register_msgbus_handlers(&exec_engine);
364        OrderEmulator::register_msgbus_handlers(&order_emulator.emulator());
365
366        let shutdown_requested = Rc::new(Cell::new(false));
367        Self::register_shutdown_handler(config.trader_id(), shutdown_requested.clone());
368
369        let trader = Rc::new(RefCell::new(Trader::new(
370            config.trader_id(),
371            instance_id,
372            config.environment(),
373            clock_factory,
374            cache.clone(),
375            portfolio.clone(),
376        )));
377
378        let ts_created = clock.borrow().timestamp_ns();
379
380        #[cfg(feature = "streaming")]
381        let (streaming_writer, streaming_subscriptions) = match config.streaming() {
382            Some(streaming_config) => {
383                let environment = config.environment().to_string().to_ascii_lowercase();
384                let base_uri = match streaming_config.fs_protocol.as_str() {
385                    "file" => streaming_config
386                        .catalog_path
387                        .trim_end_matches('/')
388                        .to_string(),
389                    _ if streaming_config.catalog_path.contains("://") => streaming_config
390                        .catalog_path
391                        .trim_end_matches('/')
392                        .to_string(),
393                    protocol => format!(
394                        "{protocol}://{}",
395                        streaming_config.catalog_path.trim_end_matches('/'),
396                    ),
397                };
398                let uri = format!("{base_uri}/{environment}/{instance_id}");
399                let rotation_config = writer_rotation_config(&streaming_config.rotation_config);
400                let writer = Rc::new(RefCell::new(FeatherWriter::from_uri(
401                    &uri,
402                    None,
403                    clock.clone(),
404                    rotation_config,
405                    None,
406                    Some(streaming_config.flush_interval_ms),
407                    streaming_config.replace_existing,
408                )?));
409                let handler = FeatherWriter::subscribe_builtin_to_message_bus(writer.clone())
410                    .map_err(|e| anyhow::anyhow!(e.to_string()))?;
411                log::info!("Writing data and events to {uri}");
412                (Some(writer), Some(handler))
413            }
414            None => (None, None),
415        };
416
417        Ok(Self {
418            name,
419            instance_id,
420            machine_id,
421            event_store,
422            config: Box::new(config),
423            cache,
424            clock,
425            portfolio,
426            log_guard,
427            data_engine,
428            risk_engine,
429            exec_engine,
430            order_emulator,
431            trader,
432            ts_created,
433            ts_started: None,
434            ts_shutdown: None,
435            shutdown_requested,
436            event_store_replay: false,
437            state_save_armed: false,
438            #[cfg(feature = "streaming")]
439            streaming_writer,
440            #[cfg(feature = "streaming")]
441            streaming_subscriptions,
442        })
443    }
444
445    fn register_shutdown_handler(trader_id: TraderId, shutdown_requested: Rc<Cell<bool>>) {
446        let handler = ShareableMessageHandler::from_typed(move |cmd: &ShutdownSystem| {
447            if cmd.trader_id != trader_id {
448                log::warn!("Received {cmd} not for this trader {trader_id}, ignoring");
449                return;
450            }
451
452            if shutdown_requested.get() {
453                log::debug!("Shutdown already requested, ignoring {cmd}");
454                return;
455            }
456
457            log::info!("Received {cmd}, requesting shutdown");
458            shutdown_requested.set(true);
459        });
460        let topic = MessagingSwitchboard::shutdown_system_topic();
461        msgbus::subscribe_any(topic.into(), handler, None);
462    }
463
464    fn determine_machine_id() -> anyhow::Result<String> {
465        sysinfo::System::host_name().ok_or_else(|| anyhow::anyhow!("Failed to determine hostname"))
466    }
467
468    fn initialize_logging(
469        trader_id: TraderId,
470        instance_id: UUID4,
471        config: LoggerConfig,
472    ) -> anyhow::Result<LogGuard> {
473        #[cfg(feature = "tracing-bridge")]
474        let use_tracing = config.use_tracing;
475
476        let file_config = config.file_config.clone().unwrap_or_default();
477        let log_guard = match init_logging(trader_id, instance_id, config, file_config) {
478            Ok(guard) => guard,
479            Err(e) => {
480                // Only recover from SetLoggerError (logger already registered).
481                // This is common in tests where multiple kernels are created and
482                // the log crate's global logger persists after LogGuard teardown.
483                // Any other error (e.g. thread spawn failure) is propagated.
484                if e.downcast_ref::<log::SetLoggerError>().is_some() {
485                    if let Some(guard) = LogGuard::new() {
486                        guard
487                    } else {
488                        return Err(e.context(
489                            "A non-Nautilus logger is already registered; \
490                             cannot initialize Nautilus logging",
491                        ));
492                    }
493                } else {
494                    return Err(e);
495                }
496            }
497        };
498
499        // Initialize tracing subscriber if enabled (idempotent)
500        #[cfg(feature = "tracing-bridge")]
501        if use_tracing && !nautilus_common::logging::bridge::tracing_is_initialized() {
502            nautilus_common::logging::bridge::init_tracing()?;
503        }
504
505        Ok(log_guard)
506    }
507
508    fn initialize_cache(
509        cache_config: Option<CacheConfig>,
510        cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
511    ) -> Rc<RefCell<Cache>> {
512        let cache_config = cache_config.unwrap_or_default();
513        let cache = Cache::new(Some(cache_config), cache_database);
514
515        Rc::new(RefCell::new(cache))
516    }
517
518    fn cancel_timers(&self) {
519        self.clock.borrow_mut().cancel_timers();
520    }
521
522    #[must_use]
523    pub fn generate_timestamp_ns(&self) -> UnixNanos {
524        self.clock.borrow().timestamp_ns()
525    }
526
527    /// Returns the kernel's environment context (Backtest, Sandbox, Live).
528    #[must_use]
529    pub fn environment(&self) -> Environment {
530        self.config.environment()
531    }
532
533    /// Returns the kernel's name.
534    #[must_use]
535    pub const fn name(&self) -> &str {
536        self.name.as_str()
537    }
538
539    /// Returns the kernel's trader ID.
540    #[must_use]
541    pub fn trader_id(&self) -> TraderId {
542        self.config.trader_id()
543    }
544
545    /// Returns the kernel's machine ID.
546    #[must_use]
547    pub fn machine_id(&self) -> &str {
548        &self.machine_id
549    }
550
551    /// Returns the kernel's instance ID.
552    #[must_use]
553    pub const fn instance_id(&self) -> UUID4 {
554        self.instance_id
555    }
556
557    /// Returns the delay after stopping the node to await residual events before final shutdown.
558    #[must_use]
559    pub fn delay_post_stop(&self) -> Duration {
560        self.config.delay_post_stop()
561    }
562
563    /// Returns the UNIX timestamp (ns) when the kernel was created.
564    #[must_use]
565    pub const fn ts_created(&self) -> UnixNanos {
566        self.ts_created
567    }
568
569    /// Returns the UNIX timestamp (ns) when the kernel was last started.
570    #[must_use]
571    pub const fn ts_started(&self) -> Option<UnixNanos> {
572        self.ts_started
573    }
574
575    /// Returns the UNIX timestamp (ns) when the kernel was last shutdown.
576    #[must_use]
577    pub const fn ts_shutdown(&self) -> Option<UnixNanos> {
578        self.ts_shutdown
579    }
580
581    /// Returns `true` if shutdown has been requested.
582    ///
583    /// Drains pending shutdown-on-error logs before checking the kernel flag.
584    #[must_use]
585    pub fn is_shutdown_requested(&self) -> bool {
586        self.drain_shutdown_on_error_trigger();
587        self.shutdown_requested.get()
588    }
589
590    /// Clears the shutdown flag.
591    ///
592    /// Call this before starting a fresh run so a prior `ShutdownSystem`
593    /// command does not abort it.
594    pub fn reset_shutdown_flag(&self) {
595        self.shutdown_requested.set(false);
596    }
597
598    /// Returns a shared handle to the shutdown flag for async runtimes
599    /// that need to poll it outside the kernel's direct borrow.
600    #[must_use]
601    pub fn shutdown_flag(&self) -> Rc<Cell<bool>> {
602        self.shutdown_requested.clone()
603    }
604
605    fn drain_shutdown_on_error_trigger(&self) {
606        try_drain_shutdown_on_error_trigger(|trigger| {
607            let command = ShutdownSystem::new(
608                self.config.trader_id(),
609                trigger.component,
610                Some(format!(
611                    "Error log received from {}: {}",
612                    trigger.component, trigger.message
613                )),
614                UUID4::new(),
615                trigger.timestamp,
616                None,
617            );
618
619            msgbus::try_publish_any(
620                MessagingSwitchboard::shutdown_system_topic(),
621                command.as_any(),
622            )
623        });
624    }
625
626    /// Returns whether the kernel has been configured to load state.
627    #[must_use]
628    pub fn load_state(&self) -> bool {
629        self.config.load_state()
630    }
631
632    /// Returns whether the kernel has been configured to save state.
633    #[must_use]
634    pub fn save_state(&self) -> bool {
635        self.config.save_state()
636    }
637
638    /// Returns the kernel's clock.
639    #[must_use]
640    pub fn clock(&self) -> Rc<RefCell<dyn Clock>> {
641        self.clock.clone()
642    }
643
644    /// Returns the kernel's cache.
645    #[must_use]
646    pub fn cache(&self) -> Rc<RefCell<Cache>> {
647        self.cache.clone()
648    }
649
650    /// Returns the kernel's portfolio.
651    #[must_use]
652    pub fn portfolio(&self) -> Ref<'_, Portfolio> {
653        self.portfolio.borrow()
654    }
655
656    /// Returns the kernel's data engine.
657    #[must_use]
658    pub fn data_engine(&self) -> Ref<'_, DataEngine> {
659        self.data_engine.borrow()
660    }
661
662    /// Returns the kernel's risk engine.
663    #[must_use]
664    pub const fn risk_engine(&self) -> &Rc<RefCell<RiskEngine>> {
665        &self.risk_engine
666    }
667
668    /// Returns the kernel's execution engine.
669    #[must_use]
670    pub const fn exec_engine(&self) -> &Rc<RefCell<ExecutionEngine>> {
671        &self.exec_engine
672    }
673
674    /// Returns the kernel's trader (shared reference).
675    #[must_use]
676    pub fn trader(&self) -> &Rc<RefCell<Trader>> {
677        &self.trader
678    }
679
680    /// Starts the Nautilus system kernel synchronously (for backtest use).
681    pub fn start(&mut self) {
682        arm_shutdown_on_error(self.config.shutdown_on_error());
683        log::info!("Starting");
684
685        self.event_store_replay = false;
686
687        if let Some(event_store) = self.event_store.as_deref_mut() {
688            self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
689
690            let components = Self::collect_registered_components(&self.trader);
691            let environment = self.config.environment();
692            let event_store_replay_configured = event_store.is_event_store_replay_configured();
693
694            if event_store_replay_configured && !self.config.load_state() {
695                log::error!("Event-store replay requires load_state=true");
696                return;
697            }
698
699            if self.config.load_state()
700                && let Err(e) =
701                    event_store.restore_parent_cache(self.instance_id, &mut self.cache.borrow_mut())
702            {
703                log::error!("Failed to restore cache from event-store replay source: {e}");
704                return;
705            }
706
707            if let Err(e) = event_store.open(self.instance_id, &components, environment) {
708                log::error!("Failed to open event-store run: {e}");
709                return;
710            }
711
712            let anchorer = event_store.snapshot_anchorer();
713            self.exec_engine
714                .borrow_mut()
715                .set_snapshot_anchorer(anchorer);
716            self.event_store_replay = event_store_replay_configured;
717        }
718
719        if self.event_store_replay {
720            log::info!(
721                "Event-store replay loaded; skipping engines, clients, trader startup, and live reconciliation",
722            );
723            self.ts_started = Some(self.clock.borrow().timestamp_ns());
724            log::info!("Started");
725            return;
726        }
727
728        self.start_engines();
729
730        log::info!("Initializing trader");
731        if let Err(e) = self.trader.borrow_mut().initialize() {
732            log::error!("Error initializing trader: {e:?}");
733            return;
734        }
735
736        // Execution and data clients are started by their engines via `start_engines` above
737
738        self.ts_started = Some(self.clock.borrow().timestamp_ns());
739        log::info!("Started");
740    }
741
742    fn collect_registered_components(trader: &Rc<RefCell<Trader>>) -> RegisteredComponents {
743        let trader = trader.borrow();
744        let mut components = RegisteredComponents::default();
745        for actor_id in trader.actor_ids() {
746            components
747                .actors
748                .insert(actor_id.to_string(), String::new());
749        }
750
751        for strategy_id in trader.strategy_ids() {
752            components
753                .strategies
754                .insert(strategy_id.to_string(), String::new());
755        }
756
757        for algo_id in trader.exec_algorithm_ids() {
758            components
759                .algorithms
760                .insert(algo_id.to_string(), String::new());
761        }
762        components
763    }
764
765    /// Starts the Nautilus system kernel asynchronously.
766    #[expect(
767        clippy::unused_async,
768        reason = "keeps the public async kernel API shape stable"
769    )]
770    pub async fn start_async(&mut self) {
771        self.start();
772    }
773
774    /// Starts the trader (strategies and actors).
775    ///
776    /// This should be called after clients are connected and instruments are cached.
777    ///
778    /// # Errors
779    ///
780    /// Returns an error if the trader or a registered component fails to start. A failed partial
781    /// start is stopped immediately before the error is returned.
782    pub fn start_trader(&mut self) -> anyhow::Result<()> {
783        log::info!("Starting trader...");
784
785        let load_state = self.config.load_state();
786        let save_state = self.config.save_state();
787
788        if (load_state || save_state) && !self.cache.borrow().has_backing() {
789            log::warn!(
790                "Cache has no database backing, load_state={load_state} and save_state={save_state} will have no effect"
791            );
792        }
793
794        if load_state {
795            Trader::load_state(&self.trader)
796                .map_err(|e| anyhow::anyhow!("Failed to load actor and strategy state: {e:#}"))?;
797        }
798
799        self.state_save_armed = save_state;
800        self.order_emulator.start();
801
802        if let Err(start_err) = Trader::start_with_component_callbacks(&self.trader) {
803            let stop_result = self.stop_trader_after_start_failure();
804            self.order_emulator.stop();
805            let save_result = self.save_trader_state();
806
807            let mut errors = vec![format!("Failed to start trader: {start_err}")];
808            if let Err(e) = stop_result {
809                errors.push(format!("failed to stop partial trader start: {e}"));
810            }
811
812            if let Err(e) = save_result {
813                errors.push(format!("failed to save partial trader state: {e}"));
814            }
815            anyhow::bail!("{}", errors.join("; "));
816        }
817
818        log::info!("Trader started");
819        Ok(())
820    }
821
822    /// Stops the trader and its registered components.
823    ///
824    /// This method initiates a graceful shutdown of trading components (strategies, actors)
825    /// which may trigger residual events such as order cancellations. The caller should
826    /// continue processing events after calling this method to handle these residual events.
827    pub fn stop_trader(&mut self) {
828        disarm_shutdown_on_error();
829
830        if !self.trader.borrow().is_running() {
831            return;
832        }
833
834        log::info!("Stopping trader...");
835
836        if let Err(e) = self.trader.borrow_mut().stop() {
837            log::error!("Error stopping trader: {e}");
838        }
839    }
840
841    /// Stops a partially started trader without deferring managed strategy shutdown.
842    ///
843    /// # Errors
844    ///
845    /// Returns an error if any active trader component cannot be stopped.
846    pub fn stop_trader_after_start_failure(&mut self) -> anyhow::Result<()> {
847        disarm_shutdown_on_error();
848
849        if !matches!(
850            self.trader.borrow().state(),
851            ComponentState::Starting | ComponentState::Running
852        ) {
853            return Ok(());
854        }
855
856        log::info!("Stopping trader immediately...");
857        self.trader.borrow_mut().stop_after_start_failure()
858    }
859
860    /// Finalizes the kernel shutdown after the grace period.
861    ///
862    /// This method should be called after the residual events grace period has elapsed
863    /// and all remaining events have been processed. It disconnects clients and stops engines.
864    ///
865    /// # Errors
866    ///
867    /// Returns an error if actor or strategy state cannot be saved.
868    #[allow(unknown_lints)]
869    #[expect(
870        clippy::unused_async,
871        clippy::unused_async_trait_impl,
872        reason = "keeps the public async kernel API shape stable"
873    )]
874    pub async fn finalize_stop(&mut self) -> anyhow::Result<()> {
875        disarm_shutdown_on_error();
876
877        // Execution and data clients are stopped by their engines via `stop_engines` below
878
879        let save_result = self.save_trader_state();
880        self.portfolio.borrow_mut().finalize_equity_curve();
881        self.stop_engines();
882        self.cancel_timers();
883
884        let ts_shutdown = self.clock.borrow().timestamp_ns();
885
886        if let Some(event_store) = self.event_store.as_deref_mut() {
887            self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
888            event_store.seal(ts_shutdown);
889        }
890        self.ts_shutdown = Some(ts_shutdown);
891        log::info!("Stopped");
892        save_result?;
893        self.flush_streaming()
894    }
895
896    /// Saves actor and strategy state at most once for the current trader run.
897    ///
898    /// # Errors
899    ///
900    /// Returns an error if a component callback or cache persistence operation fails.
901    pub fn save_trader_state(&mut self) -> anyhow::Result<()> {
902        if !std::mem::take(&mut self.state_save_armed) {
903            return Ok(());
904        }
905
906        Trader::save_state(&self.trader)
907    }
908
909    /// Returns the kernel-managed event-store integration, when one was injected.
910    ///
911    /// Callers wire an implementation through
912    /// [`NautilusKernelBuilder::with_event_store`](crate::builder::NautilusKernelBuilder::with_event_store);
913    /// without an injected adapter this returns `None`.
914    #[must_use]
915    pub fn event_store(&self) -> Option<&dyn KernelEventStore> {
916        self.event_store.as_deref()
917    }
918
919    /// Returns whether the event-store integration is running an event-store replay start.
920    #[must_use]
921    pub fn is_event_store_replay(&self) -> bool {
922        self.event_store_replay
923    }
924
925    /// Returns whether the event-store integration is configured for an event-store replay start.
926    #[must_use]
927    pub fn is_event_store_replay_configured(&self) -> bool {
928        self.event_store
929            .as_deref()
930            .is_some_and(KernelEventStore::is_event_store_replay_configured)
931    }
932
933    /// Resets the Nautilus system kernel to its initial state.
934    pub fn reset(&mut self) {
935        disarm_shutdown_on_error();
936        log::info!("Resetting");
937
938        if let Err(e) = self.trader.borrow_mut().reset() {
939            log::error!("Error resetting trader: {e:?}");
940        }
941
942        self.data_engine.borrow_mut().reset();
943        self.exec_engine.borrow_mut().reset();
944        self.risk_engine.borrow_mut().reset();
945        self.order_emulator.reset();
946        self.portfolio.borrow_mut().reset();
947
948        self.ts_started = None;
949        self.ts_shutdown = None;
950        self.state_save_armed = false;
951
952        log::info!("Reset");
953    }
954
955    /// Disposes of the Nautilus system kernel, releasing resources.
956    pub fn dispose(&mut self) {
957        disarm_shutdown_on_error();
958        log::info!("Disposing");
959
960        let trader_state = self.trader.borrow().state();
961        match trader_state {
962            ComponentState::Running => self.stop_trader(),
963            ComponentState::Starting => {
964                if let Err(e) = self.stop_trader_after_start_failure() {
965                    log::error!("Error stopping partial trader start during disposal: {e:?}");
966                }
967            }
968            _ => {}
969        }
970
971        if let Err(e) = self.save_trader_state() {
972            log::error!("Error saving trader state during disposal: {e:?}");
973        }
974
975        {
976            let mut trader = self.trader.borrow_mut();
977            if trader.state() == ComponentState::PreInitialized
978                && let Err(e) = trader.initialize()
979            {
980                log::error!("Error initializing trader for disposal: {e:?}");
981            }
982
983            if !trader.is_disposed()
984                && let Err(e) = trader.dispose()
985            {
986                log::error!("Error disposing trader: {e:?}");
987            }
988        }
989
990        self.stop_engines();
991        self.portfolio.borrow_mut().reset();
992        self.cancel_timers();
993
994        // BacktestEngine::end() does not call finalize_stop, so dispose() seals the
995        // run for non-streaming backtests. finalize_stop (live) consumes the session
996        // first; this call is then a no-op. Callers that skip dispose entirely fall
997        // back to the event-store implementation's Drop.
998        if let Some(event_store) = self.event_store.as_deref_mut() {
999            self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
1000            let ts_dispose = self.clock.borrow().timestamp_ns();
1001            event_store.seal(ts_dispose);
1002        }
1003
1004        #[cfg(feature = "streaming")]
1005        {
1006            if let Some(subscriptions) = self.streaming_subscriptions.take() {
1007                FeatherWriter::unsubscribe_from_message_bus(&subscriptions);
1008            }
1009
1010            if let Some(writer) = self.streaming_writer.take()
1011                && let Err(e) =
1012                    nautilus_common::live::get_runtime().block_on(writer.borrow_mut().close())
1013            {
1014                log::error!("Error closing streaming writer: {e}");
1015            }
1016        }
1017
1018        self.data_engine.borrow_mut().dispose();
1019        self.exec_engine.borrow_mut().dispose();
1020        self.risk_engine.borrow_mut().dispose();
1021        self.order_emulator.dispose();
1022        self.cache.borrow_mut().dispose();
1023        get_message_bus().borrow_mut().dispose();
1024
1025        log::info!("Disposed");
1026    }
1027
1028    /// Flushes configured streaming output.
1029    ///
1030    /// # Errors
1031    ///
1032    /// Returns an error if buffered output cannot be written to the configured object store.
1033    pub fn flush_streaming(&mut self) -> anyhow::Result<()> {
1034        #[cfg(feature = "streaming")]
1035        if let Some(writer) = &self.streaming_writer {
1036            nautilus_common::live::get_runtime()
1037                .block_on(writer.borrow_mut().flush())
1038                .map_err(|e| anyhow::anyhow!(e.to_string()))?;
1039        }
1040        Ok(())
1041    }
1042
1043    /// Starts all engine components.
1044    fn start_engines(&self) {
1045        self.data_engine.borrow_mut().start();
1046        self.exec_engine.borrow_mut().start();
1047        self.risk_engine.borrow_mut().start();
1048    }
1049
1050    /// Stops all engine components.
1051    fn stop_engines(&self) {
1052        self.data_engine.borrow_mut().stop();
1053        self.exec_engine.borrow_mut().stop();
1054        self.risk_engine.borrow_mut().stop();
1055        self.order_emulator.stop();
1056    }
1057
1058    /// Connects data engine clients.
1059    ///
1060    /// Data clients are connected first so that instruments are published
1061    /// and can be drained into the cache before execution clients connect.
1062    #[expect(clippy::await_holding_refcell_ref)] // Single-threaded runtime, intentional design
1063    pub async fn connect_data_clients(&mut self) {
1064        log::info!("Connecting data clients...");
1065        self.data_engine.borrow_mut().connect().await;
1066    }
1067
1068    /// Connects execution engine clients.
1069    ///
1070    /// Must be called after data clients are connected and instrument events
1071    /// have been drained into the cache, so execution clients can load instruments.
1072    #[expect(clippy::await_holding_refcell_ref)] // Single-threaded runtime, intentional design
1073    pub async fn connect_exec_clients(&mut self) {
1074        log::info!("Connecting execution clients...");
1075        self.exec_engine.borrow_mut().connect().await;
1076    }
1077
1078    /// Disconnects all engine clients.
1079    ///
1080    /// # Errors
1081    ///
1082    /// Returns an error if any client fails to disconnect.
1083    #[expect(clippy::await_holding_refcell_ref)] // Single-threaded runtime, intentional design
1084    pub async fn disconnect_clients(&mut self) -> anyhow::Result<()> {
1085        log::info!("Disconnecting clients...");
1086        let mut data_engine = self.data_engine.borrow_mut();
1087        let mut exec_engine = self.exec_engine.borrow_mut();
1088        let (data_result, exec_result) =
1089            futures::join!(data_engine.disconnect(), exec_engine.disconnect());
1090
1091        match (data_result, exec_result) {
1092            (Ok(()), Ok(())) => Ok(()),
1093            (Err(data_err), Ok(())) => Err(data_err),
1094            (Ok(()), Err(exec_err)) => Err(exec_err),
1095            (Err(data_err), Err(exec_err)) => anyhow::bail!(
1096                "Failed to disconnect data clients: {data_err}; failed to disconnect execution \
1097                 clients: {exec_err}"
1098            ),
1099        }
1100    }
1101
1102    /// Returns `true` if all engine clients are connected.
1103    #[must_use]
1104    pub fn check_engines_connected(&self) -> bool {
1105        self.data_engine.borrow().check_connected() && self.exec_engine.borrow().check_connected()
1106    }
1107
1108    /// Returns `true` if all engine clients are disconnected.
1109    #[must_use]
1110    pub fn check_engines_disconnected(&self) -> bool {
1111        self.data_engine.borrow().check_disconnected()
1112            && self.exec_engine.borrow().check_disconnected()
1113    }
1114
1115    /// Returns connection status for all data clients.
1116    #[must_use]
1117    pub fn data_client_connection_status(&self) -> Vec<(ClientId, bool)> {
1118        self.data_engine.borrow().client_connection_status()
1119    }
1120
1121    /// Returns connection status for all execution clients.
1122    #[must_use]
1123    pub fn exec_client_connection_status(&self) -> Vec<(ClientId, bool)> {
1124        self.exec_engine.borrow().client_connection_status()
1125    }
1126}
1127
1128#[cfg(feature = "streaming")]
1129fn writer_rotation_config(config: &crate::config::RotationConfig) -> WriterRotationConfig {
1130    match config {
1131        crate::config::RotationConfig::Size { max_size } => WriterRotationConfig::Size {
1132            max_size: *max_size,
1133        },
1134        crate::config::RotationConfig::Interval { interval_ns } => WriterRotationConfig::Interval {
1135            interval_ns: *interval_ns,
1136        },
1137        crate::config::RotationConfig::ScheduledDates {
1138            interval_ns,
1139            schedule_ns,
1140        } => WriterRotationConfig::ScheduledDates {
1141            interval_ns: *interval_ns,
1142            rotation_time: *schedule_ns,
1143            rotation_timezone: TimeZone::UTC,
1144        },
1145        crate::config::RotationConfig::NoRotation => WriterRotationConfig::NoRotation,
1146    }
1147}
1148
1149#[cfg(all(test, feature = "python"))]
1150mod tests {
1151    use nautilus_common::messages::system::ShutdownSystem;
1152    use nautilus_core::UUID4;
1153    use rstest::*;
1154    use ustr::Ustr;
1155
1156    use super::*;
1157    use crate::builder::NautilusKernelBuilder;
1158
1159    #[rstest]
1160    fn test_shutdown_system_sets_kernel_flag() {
1161        let kernel = NautilusKernelBuilder::default().build().unwrap();
1162        assert!(!kernel.is_shutdown_requested());
1163
1164        let command = ShutdownSystem::new(
1165            kernel.trader_id(),
1166            Ustr::from("TestComponent"),
1167            Some("unit test".to_string()),
1168            UUID4::new(),
1169            kernel.generate_timestamp_ns(),
1170            None, // correlation_id
1171        );
1172
1173        msgbus::publish_any(
1174            MessagingSwitchboard::shutdown_system_topic(),
1175            command.as_any(),
1176        );
1177        assert!(kernel.is_shutdown_requested());
1178
1179        kernel.reset_shutdown_flag();
1180        assert!(!kernel.is_shutdown_requested());
1181    }
1182
1183    #[rstest]
1184    fn test_shutdown_system_idempotent() {
1185        let kernel = NautilusKernelBuilder::default().build().unwrap();
1186
1187        let make_cmd = || {
1188            ShutdownSystem::new(
1189                kernel.trader_id(),
1190                Ustr::from("TestComponent"),
1191                None,
1192                UUID4::new(),
1193                kernel.generate_timestamp_ns(),
1194                None, // correlation_id
1195            )
1196        };
1197
1198        let topic = MessagingSwitchboard::shutdown_system_topic();
1199        msgbus::publish_any(topic, make_cmd().as_any());
1200        assert!(kernel.is_shutdown_requested());
1201
1202        msgbus::publish_any(topic, make_cmd().as_any());
1203        assert!(kernel.is_shutdown_requested());
1204
1205        kernel.reset_shutdown_flag();
1206        assert!(!kernel.is_shutdown_requested());
1207
1208        msgbus::publish_any(topic, make_cmd().as_any());
1209        assert!(kernel.is_shutdown_requested());
1210    }
1211
1212    #[rstest]
1213    fn test_shutdown_system_ignores_other_trader() {
1214        let kernel = NautilusKernelBuilder::default().build().unwrap();
1215
1216        let command = ShutdownSystem::new(
1217            TraderId::from("OTHER-TRADER"),
1218            Ustr::from("TestComponent"),
1219            None,
1220            UUID4::new(),
1221            kernel.generate_timestamp_ns(),
1222            None, // correlation_id
1223        );
1224
1225        msgbus::publish_any(
1226            MessagingSwitchboard::shutdown_system_topic(),
1227            command.as_any(),
1228        );
1229        assert!(!kernel.is_shutdown_requested());
1230    }
1231}
1232
1233#[cfg(all(test, feature = "streaming"))]
1234mod streaming_tests {
1235    use std::{cell::RefCell, rc::Rc, sync::Arc};
1236
1237    use nautilus_common::{
1238        clock::TestClock,
1239        messages::data::{DataCommand, QuotesResponse, RequestCommand, RequestQuotes},
1240        msgbus::{self, MStr, ShareableMessageHandler},
1241    };
1242    use nautilus_model::{
1243        data::{CustomData, DataType, QuoteTick},
1244        identifiers::InstrumentId,
1245        types::{Price, Quantity},
1246    };
1247    use nautilus_persistence::{
1248        backend::catalog::ParquetDataCatalog, config::DataCatalogConfig,
1249        test_data::RustTestCustomData,
1250    };
1251    use nautilus_serialization::ensure_custom_data_registered;
1252    use rstest::rstest;
1253    use tempfile::tempdir;
1254
1255    use super::*;
1256    use crate::config::{KernelConfig, RotationConfig, StreamingConfig};
1257
1258    #[rstest]
1259    #[case(
1260        RotationConfig::Size { max_size: 17 },
1261        WriterRotationConfig::Size { max_size: 17 }
1262    )]
1263    #[case(
1264        RotationConfig::Interval { interval_ns: 23 },
1265        WriterRotationConfig::Interval { interval_ns: 23 }
1266    )]
1267    #[case(
1268        RotationConfig::ScheduledDates {
1269            interval_ns: 31,
1270            schedule_ns: UnixNanos::from(37),
1271        },
1272        WriterRotationConfig::ScheduledDates {
1273            interval_ns: 31,
1274            rotation_time: UnixNanos::from(37),
1275            rotation_timezone: TimeZone::UTC,
1276        }
1277    )]
1278    #[case(RotationConfig::NoRotation, WriterRotationConfig::NoRotation)]
1279    fn test_writer_rotation_config(
1280        #[case] config: RotationConfig,
1281        #[case] expected: WriterRotationConfig,
1282    ) {
1283        let actual = writer_rotation_config(&config);
1284
1285        match (actual, expected) {
1286            (
1287                WriterRotationConfig::Size { max_size: actual },
1288                WriterRotationConfig::Size { max_size: expected },
1289            )
1290            | (
1291                WriterRotationConfig::Interval {
1292                    interval_ns: actual,
1293                },
1294                WriterRotationConfig::Interval {
1295                    interval_ns: expected,
1296                },
1297            ) => assert_eq!(actual, expected),
1298            (
1299                WriterRotationConfig::ScheduledDates {
1300                    interval_ns: actual_interval,
1301                    rotation_time: actual_time,
1302                    rotation_timezone: actual_timezone,
1303                },
1304                WriterRotationConfig::ScheduledDates {
1305                    interval_ns: expected_interval,
1306                    rotation_time: expected_time,
1307                    rotation_timezone: expected_timezone,
1308                },
1309            ) => {
1310                assert_eq!(actual_interval, expected_interval);
1311                assert_eq!(actual_time, expected_time);
1312                assert_eq!(actual_timezone, expected_timezone);
1313            }
1314            (WriterRotationConfig::NoRotation, WriterRotationConfig::NoRotation) => {}
1315            (actual, expected) => panic!("rotation mismatch: {actual:?} != {expected:?}"),
1316        }
1317    }
1318
1319    #[rstest]
1320    fn test_configured_catalog_serves_builtin_quotes() {
1321        let directory = tempdir().unwrap();
1322        let instrument_id = InstrumentId::from("AUD/USD.SIM");
1323        let quotes = vec![
1324            QuoteTick::new(
1325                instrument_id,
1326                Price::from("1.00001"),
1327                Price::from("1.00003"),
1328                Quantity::from("100_000"),
1329                Quantity::from("200_000"),
1330                UnixNanos::from(1),
1331                UnixNanos::from(1),
1332            ),
1333            QuoteTick::new(
1334                instrument_id,
1335                Price::from("1.00002"),
1336                Price::from("1.00004"),
1337                Quantity::from("300_000"),
1338                Quantity::from("400_000"),
1339                UnixNanos::from(2),
1340                UnixNanos::from(2),
1341            ),
1342        ];
1343        let catalog = ParquetDataCatalog::new(directory.path(), None, None, None, None);
1344        catalog.write_to_parquet(&quotes, None, None, None).unwrap();
1345        let config = KernelConfig {
1346            catalogs: vec![DataCatalogConfig::new(
1347                directory.path().to_string_lossy().into_owned(),
1348                Some("file".to_string()),
1349                None,
1350                Some("history".to_string()),
1351            )],
1352            ..KernelConfig::default()
1353        };
1354        let mut kernel = NautilusKernel::new("CatalogQueryTest".to_string(), config).unwrap();
1355        kernel
1356            .clock
1357            .borrow_mut()
1358            .as_any_mut()
1359            .downcast_mut::<TestClock>()
1360            .unwrap()
1361            .set_time(UnixNanos::from(3));
1362        let request_id = UUID4::new();
1363        let received = Rc::new(RefCell::new(Vec::new()));
1364        let received_quotes = received.clone();
1365        msgbus::register_response_handler(
1366            &request_id,
1367            ShareableMessageHandler::from_typed(move |response: &QuotesResponse| {
1368                *received_quotes.borrow_mut() = response.data.clone();
1369            }),
1370        );
1371        let request = RequestQuotes::new(
1372            instrument_id,
1373            Some(UnixNanos::from(1).to_datetime_utc()),
1374            Some(UnixNanos::from(2).to_datetime_utc()),
1375            None,
1376            None,
1377            request_id,
1378            UnixNanos::from(3),
1379            None,
1380        );
1381
1382        kernel
1383            .data_engine
1384            .borrow_mut()
1385            .execute(DataCommand::Request(RequestCommand::Quotes(request)));
1386
1387        assert_eq!(*received.borrow(), quotes);
1388        assert_eq!(kernel.data_engine.borrow().request_count(), 1);
1389        assert_eq!(kernel.data_engine.borrow().response_count(), 1);
1390        kernel.dispose();
1391    }
1392
1393    #[rstest]
1394    fn test_configured_streaming_excludes_custom_data() {
1395        ensure_custom_data_registered::<RustTestCustomData>();
1396
1397        let directory = tempdir().unwrap();
1398        let instance_id = UUID4::new();
1399        let config = KernelConfig {
1400            instance_id: Some(instance_id),
1401            streaming: Some(StreamingConfig::new(
1402                directory.path().to_string_lossy().into_owned(),
1403                "file".to_string(),
1404                1_000,
1405                false,
1406                RotationConfig::NoRotation,
1407            )),
1408            ..KernelConfig::default()
1409        };
1410        let mut kernel = NautilusKernel::new("BuiltInStreamingTest".to_string(), config).unwrap();
1411        let instrument_id = InstrumentId::from("RUST.TEST");
1412        let custom = CustomData::new(
1413            Arc::new(RustTestCustomData {
1414                instrument_id,
1415                value: 1.23,
1416                flag: true,
1417                ts_event: UnixNanos::from(1_000),
1418                ts_init: UnixNanos::from(1_000),
1419            }),
1420            DataType::new("RustTestCustomData", None, Some(instrument_id.to_string())),
1421        );
1422
1423        msgbus::publish_any(MStr::from("data.custom"), &custom);
1424        kernel.flush_streaming().unwrap();
1425
1426        let custom_path = directory
1427            .path()
1428            .join("backtest")
1429            .join(instance_id.to_string())
1430            .join("data/custom");
1431        assert!(!custom_path.exists());
1432        kernel.dispose();
1433    }
1434}
1435
1436#[cfg(test)]
1437mod lifecycle_tests {
1438    use futures::FutureExt;
1439    use indexmap::IndexMap;
1440    use nautilus_common::{
1441        actor::registry::get_actor_unchecked,
1442        cache::Cache,
1443        messages::data::{DataCommand, SubscribeCommand, UnsubscribeCommand},
1444        msgbus::stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
1445    };
1446    use nautilus_execution::engine::SnapshotAnchorer;
1447    use nautilus_model::{
1448        enums::{OrderSide, OrderStatus, OrderType, TriggerType},
1449        identifiers::{ActorId, ClientOrderId, StrategyId},
1450        instruments::{
1451            CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
1452        },
1453        orders::{Order, OrderAny, OrderTestBuilder},
1454        types::{Price, Quantity},
1455    };
1456    use nautilus_testkit::{
1457        cache::TestCacheDatabaseControl,
1458        components::{StateActor, StateStrategy},
1459    };
1460    use rstest::rstest;
1461    use ustr::Ustr;
1462
1463    use super::*;
1464    use crate::{
1465        builder::NautilusKernelBuilder,
1466        event_store::{KernelEventStore, RegisteredComponents},
1467    };
1468
1469    #[derive(Debug)]
1470    struct RecordingEventStore {
1471        control: TestCacheDatabaseControl,
1472        opened: bool,
1473    }
1474
1475    impl KernelEventStore for RecordingEventStore {
1476        fn restore_parent_cache(
1477            &mut self,
1478            _instance_id: UUID4,
1479            _cache: &mut Cache,
1480        ) -> anyhow::Result<()> {
1481            self.control.record("event_store.restore");
1482            Ok(())
1483        }
1484
1485        fn open(
1486            &mut self,
1487            _instance_id: UUID4,
1488            _components: &RegisteredComponents,
1489            _environment: Environment,
1490        ) -> anyhow::Result<()> {
1491            self.control.record("event_store.open");
1492            self.opened = true;
1493            Ok(())
1494        }
1495
1496        fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
1497            None
1498        }
1499
1500        fn seal(&mut self, _ts_init: UnixNanos) {
1501            if self.opened {
1502                self.control.record("event_store.seal");
1503                self.opened = false;
1504            }
1505        }
1506
1507        fn run_id(&self) -> Option<&str> {
1508            None
1509        }
1510
1511        fn parent_run_id(&self) -> Option<&str> {
1512            None
1513        }
1514
1515        fn is_halted(&self) -> bool {
1516            false
1517        }
1518    }
1519
1520    fn state(key: &str, value: &[u8]) -> IndexMap<String, Vec<u8>> {
1521        IndexMap::from([(key.to_string(), value.to_vec())])
1522    }
1523
1524    fn finalize(kernel: &mut NautilusKernel) -> anyhow::Result<()> {
1525        kernel
1526            .finalize_stop()
1527            .now_or_never()
1528            .expect("kernel finalization must not yield")
1529    }
1530
1531    fn add_state_components(
1532        kernel: &NautilusKernel,
1533        control: &TestCacheDatabaseControl,
1534        actor: StateActor,
1535        strategy: StateStrategy,
1536    ) {
1537        kernel.trader.borrow_mut().add_actor(actor).unwrap();
1538        kernel.trader.borrow_mut().add_strategy(strategy).unwrap();
1539        control.record("components.registered");
1540    }
1541
1542    fn create_stop_market_order(instrument: &CryptoPerpetual, client_order_id: &str) -> OrderAny {
1543        OrderTestBuilder::new(OrderType::StopMarket)
1544            .instrument_id(instrument.id())
1545            .client_order_id(ClientOrderId::from(client_order_id))
1546            .side(OrderSide::Buy)
1547            .trigger_price(Price::from("5100.00"))
1548            .quantity(Quantity::from(1))
1549            .emulation_trigger(TriggerType::BidAsk)
1550            .build()
1551    }
1552
1553    fn register_data_command_handler(id: &str) -> TypedIntoMessageSavingHandler<DataCommand> {
1554        let (handler, saving_handler) =
1555            get_typed_into_message_saving_handler::<DataCommand>(Some(Ustr::from(id)));
1556        msgbus::register_data_command_endpoint(
1557            MessagingSwitchboard::data_engine_queue_execute(),
1558            handler,
1559        );
1560        saving_handler
1561    }
1562
1563    #[rstest]
1564    fn test_state_persistence_orders_restore_load_start_stop_save_seal_and_dispose() {
1565        let actor_id = ActorId::from("STATE-ACTOR");
1566        let strategy_id = StrategyId::from("STATE-STRATEGY-001");
1567        let actor_load = state("actor-loaded", b"actor-load-value");
1568        let strategy_load = state("strategy-loaded", b"strategy-load-value");
1569        let actor_save = state("actor-saved", b"actor-save-value");
1570        let strategy_save = state("strategy-saved", b"strategy-save-value");
1571        let (database, control) = TestCacheDatabaseControl::create();
1572        control.set_actor_state(actor_id, &actor_load);
1573        control.set_strategy_state(strategy_id, &strategy_load);
1574
1575        let event_store_control = control.clone();
1576        let mut kernel = NautilusKernelBuilder::default()
1577            .with_cache_database(Box::new(database))
1578            .with_event_store(move |_instance_id, _clock| {
1579                Ok(Box::new(RecordingEventStore {
1580                    control: event_store_control,
1581                    opened: false,
1582                }))
1583            })
1584            .build()
1585            .unwrap();
1586
1587        let actor = StateActor::new(actor_id, control.clone(), actor_save.clone());
1588        let strategy = StateStrategy::new(strategy_id, control.clone(), strategy_save.clone());
1589        add_state_components(&kernel, &control, actor, strategy);
1590
1591        kernel.start();
1592        kernel.start_trader().unwrap();
1593
1594        let actor_state = get_actor_unchecked::<StateActor>(&actor_id.inner())
1595            .state_load()
1596            .cloned();
1597        let strategy_state = get_actor_unchecked::<StateStrategy>(&strategy_id.inner())
1598            .state_load()
1599            .cloned();
1600        assert_eq!(actor_state, Some(actor_load));
1601        assert_eq!(strategy_state, Some(strategy_load));
1602
1603        kernel.stop_trader();
1604        kernel.stop_trader();
1605        finalize(&mut kernel).unwrap();
1606        finalize(&mut kernel).unwrap();
1607        kernel.dispose();
1608
1609        assert_eq!(
1610            control.events(),
1611            vec![
1612                "components.registered",
1613                "event_store.restore",
1614                "event_store.open",
1615                "actor.load:STATE-ACTOR",
1616                "actor.on_load",
1617                "strategy.load:STATE-STRATEGY-001",
1618                "strategy.on_load",
1619                "actor.on_start",
1620                "strategy.on_start",
1621                "actor.on_stop",
1622                "strategy.on_stop",
1623                "actor.on_save",
1624                "actor.update:STATE-ACTOR",
1625                "strategy.on_save",
1626                "strategy.update:STATE-STRATEGY-001",
1627                "event_store.seal",
1628                "database.close",
1629            ]
1630        );
1631        assert_eq!(control.actor_state(&actor_id), Some(actor_save));
1632        assert_eq!(control.strategy_state(&strategy_id), Some(strategy_save));
1633    }
1634
1635    #[rstest]
1636    fn test_state_persistence_skips_callbacks_without_cache_backing() {
1637        let actor_id = ActorId::from("NO-BACKING-ACTOR");
1638        let strategy_id = StrategyId::from("NO-BACKING-STRATEGY-001");
1639        let control = TestCacheDatabaseControl::default();
1640        let mut kernel = NautilusKernelBuilder::default().build().unwrap();
1641        let actor = StateActor::new(actor_id, control.clone(), state("actor", b"save"));
1642        let strategy = StateStrategy::new(strategy_id, control.clone(), state("strategy", b"save"));
1643        add_state_components(&kernel, &control, actor, strategy);
1644
1645        kernel.start();
1646        kernel.start_trader().unwrap();
1647        kernel.stop_trader();
1648        finalize(&mut kernel).unwrap();
1649        kernel.dispose();
1650
1651        assert_eq!(
1652            control.events(),
1653            vec![
1654                "components.registered",
1655                "actor.on_start",
1656                "strategy.on_start",
1657                "actor.on_stop",
1658                "strategy.on_stop",
1659            ]
1660        );
1661    }
1662
1663    #[rstest]
1664    fn test_state_persistence_skips_empty_load_and_persists_empty_save() {
1665        let actor_id = ActorId::from("EMPTY-STATE-ACTOR");
1666        let strategy_id = StrategyId::from("EMPTY-STATE-STRATEGY-001");
1667        let (database, control) = TestCacheDatabaseControl::create();
1668        let mut kernel = NautilusKernelBuilder::default()
1669            .with_cache_database(Box::new(database))
1670            .build()
1671            .unwrap();
1672        let actor = StateActor::new(actor_id, control.clone(), IndexMap::new());
1673        let strategy = StateStrategy::new(strategy_id, control.clone(), IndexMap::new());
1674        add_state_components(&kernel, &control, actor, strategy);
1675
1676        kernel.start();
1677        kernel.start_trader().unwrap();
1678        kernel.stop_trader();
1679        finalize(&mut kernel).unwrap();
1680
1681        assert_eq!(
1682            control.events(),
1683            vec![
1684                "components.registered",
1685                "actor.load:EMPTY-STATE-ACTOR",
1686                "strategy.load:EMPTY-STATE-STRATEGY-001",
1687                "actor.on_start",
1688                "strategy.on_start",
1689                "actor.on_stop",
1690                "strategy.on_stop",
1691                "actor.on_save",
1692                "actor.update:EMPTY-STATE-ACTOR",
1693                "strategy.on_save",
1694                "strategy.update:EMPTY-STATE-STRATEGY-001",
1695            ]
1696        );
1697        assert_eq!(control.actor_state(&actor_id), Some(IndexMap::new()));
1698        assert_eq!(control.strategy_state(&strategy_id), Some(IndexMap::new()));
1699        kernel.dispose();
1700    }
1701
1702    #[rstest]
1703    fn test_state_save_reports_all_callback_errors_and_continues_shutdown() {
1704        let actor_id = ActorId::from("FAIL-SAVE-ACTOR");
1705        let strategy_id = StrategyId::from("FAIL-SAVE-STRATEGY-001");
1706        let (database, control) = TestCacheDatabaseControl::create();
1707        let event_store_control = control.clone();
1708        let mut kernel = NautilusKernelBuilder::default()
1709            .with_cache_database(Box::new(database))
1710            .with_event_store(move |_instance_id, _clock| {
1711                Ok(Box::new(RecordingEventStore {
1712                    control: event_store_control,
1713                    opened: false,
1714                }))
1715            })
1716            .build()
1717            .unwrap();
1718        let actor = StateActor::new(actor_id, control.clone(), IndexMap::new()).with_fail_save();
1719        let strategy =
1720            StateStrategy::new(strategy_id, control.clone(), IndexMap::new()).with_fail_save();
1721        add_state_components(&kernel, &control, actor, strategy);
1722
1723        kernel.start();
1724        kernel.start_trader().unwrap();
1725        kernel.stop_trader();
1726        let expected_shutdown = kernel.clock.borrow().timestamp_ns();
1727        let error = finalize(&mut kernel).unwrap_err();
1728        kernel.dispose();
1729
1730        assert_eq!(
1731            error.to_string(),
1732            "Failed to save component state: actor FAIL-SAVE-ACTOR callback: test actor on_save \
1733             failure; strategy FAIL-SAVE-STRATEGY-001 callback: test strategy on_save failure"
1734        );
1735        assert_eq!(kernel.ts_shutdown, Some(expected_shutdown));
1736        assert_eq!(
1737            control.events(),
1738            vec![
1739                "components.registered",
1740                "event_store.restore",
1741                "event_store.open",
1742                "actor.load:FAIL-SAVE-ACTOR",
1743                "strategy.load:FAIL-SAVE-STRATEGY-001",
1744                "actor.on_start",
1745                "strategy.on_start",
1746                "actor.on_stop",
1747                "strategy.on_stop",
1748                "actor.on_save",
1749                "strategy.on_save",
1750                "event_store.seal",
1751                "database.close",
1752            ]
1753        );
1754    }
1755
1756    #[rstest]
1757    fn test_state_load_callback_failure_prevents_start_and_save() {
1758        let actor_id = ActorId::from("FAIL-LOAD-ACTOR");
1759        let strategy_id = StrategyId::from("FAIL-LOAD-STRATEGY-001");
1760        let (database, control) = TestCacheDatabaseControl::create();
1761        control.set_actor_state(actor_id, &state("actor", b"load"));
1762        control.set_strategy_state(strategy_id, &state("strategy", b"load"));
1763        let mut kernel = NautilusKernelBuilder::default()
1764            .with_cache_database(Box::new(database))
1765            .build()
1766            .unwrap();
1767        let actor = StateActor::new(actor_id, control.clone(), IndexMap::new()).with_fail_load();
1768        let strategy = StateStrategy::new(strategy_id, control.clone(), IndexMap::new());
1769        add_state_components(&kernel, &control, actor, strategy);
1770
1771        kernel.start();
1772        let error = kernel.start_trader().unwrap_err();
1773        kernel.dispose();
1774
1775        assert_eq!(
1776            error.to_string(),
1777            "Failed to load actor and strategy state: Failed to restore actor FAIL-LOAD-ACTOR \
1778             state: test actor on_load failure"
1779        );
1780        assert_eq!(
1781            control.events(),
1782            vec![
1783                "components.registered",
1784                "actor.load:FAIL-LOAD-ACTOR",
1785                "actor.on_load",
1786                "database.close",
1787            ]
1788        );
1789    }
1790
1791    #[rstest]
1792    fn test_state_save_reports_all_persistence_errors() {
1793        let actor_id = ActorId::from("FAIL-UPDATE-ACTOR");
1794        let strategy_id = StrategyId::from("FAIL-UPDATE-STRATEGY-001");
1795        let (database, control) = TestCacheDatabaseControl::create();
1796        control.set_fail_update_actor(true);
1797        control.set_fail_update_strategy(true);
1798        let mut kernel = NautilusKernelBuilder::default()
1799            .with_cache_database(Box::new(database))
1800            .build()
1801            .unwrap();
1802        let actor = StateActor::new(actor_id, control.clone(), state("actor", b"save"));
1803        let strategy = StateStrategy::new(strategy_id, control.clone(), state("strategy", b"save"));
1804        add_state_components(&kernel, &control, actor, strategy);
1805
1806        kernel.start();
1807        kernel.start_trader().unwrap();
1808        kernel.stop_trader();
1809        let error = finalize(&mut kernel).unwrap_err();
1810        kernel.dispose();
1811
1812        assert_eq!(
1813            error.to_string(),
1814            "Failed to save component state: actor FAIL-UPDATE-ACTOR persistence: test actor \
1815             update failure; strategy FAIL-UPDATE-STRATEGY-001 persistence: test strategy update \
1816             failure"
1817        );
1818        assert_eq!(
1819            control.events(),
1820            vec![
1821                "components.registered",
1822                "actor.load:FAIL-UPDATE-ACTOR",
1823                "strategy.load:FAIL-UPDATE-STRATEGY-001",
1824                "actor.on_start",
1825                "strategy.on_start",
1826                "actor.on_stop",
1827                "strategy.on_stop",
1828                "actor.on_save",
1829                "actor.update:FAIL-UPDATE-ACTOR",
1830                "strategy.on_save",
1831                "strategy.update:FAIL-UPDATE-STRATEGY-001",
1832                "database.close",
1833            ]
1834        );
1835    }
1836
1837    #[rstest]
1838    fn test_partial_startup_stops_and_saves_once() {
1839        let actor_id = ActorId::from("PARTIAL-ACTOR");
1840        let strategy_id = StrategyId::from("PARTIAL-STRATEGY-001");
1841        let (database, control) = TestCacheDatabaseControl::create();
1842        let mut kernel = NautilusKernelBuilder::default()
1843            .with_cache_database(Box::new(database))
1844            .build()
1845            .unwrap();
1846        let actor = StateActor::new(actor_id, control.clone(), state("actor", b"partial"));
1847        let strategy =
1848            StateStrategy::new(strategy_id, control.clone(), state("strategy", b"partial"))
1849                .with_fail_start();
1850        add_state_components(&kernel, &control, actor, strategy);
1851
1852        kernel.start();
1853        let error = kernel.start_trader().unwrap_err();
1854        kernel.dispose();
1855
1856        assert_eq!(
1857            error.to_string(),
1858            "Failed to start trader: test strategy on_start failure"
1859        );
1860        assert_eq!(
1861            control.events(),
1862            vec![
1863                "components.registered",
1864                "actor.load:PARTIAL-ACTOR",
1865                "strategy.load:PARTIAL-STRATEGY-001",
1866                "actor.on_start",
1867                "strategy.on_start",
1868                "actor.on_stop",
1869                "strategy.on_stop",
1870                "actor.on_save",
1871                "actor.update:PARTIAL-ACTOR",
1872                "strategy.on_save",
1873                "strategy.update:PARTIAL-STRATEGY-001",
1874                "database.close",
1875            ]
1876        );
1877        assert_eq!(
1878            control.actor_state(&actor_id),
1879            Some(state("actor", b"partial"))
1880        );
1881        assert_eq!(
1882            control.strategy_state(&strategy_id),
1883            Some(state("strategy", b"partial"))
1884        );
1885    }
1886
1887    #[rstest]
1888    fn test_forced_dispose_stops_and_saves_once() {
1889        let actor_id = ActorId::from("FORCED-ACTOR");
1890        let strategy_id = StrategyId::from("FORCED-STRATEGY-001");
1891        let (database, control) = TestCacheDatabaseControl::create();
1892        let mut kernel = NautilusKernelBuilder::default()
1893            .with_cache_database(Box::new(database))
1894            .build()
1895            .unwrap();
1896        let actor = StateActor::new(actor_id, control.clone(), state("actor", b"forced"));
1897        let strategy =
1898            StateStrategy::new(strategy_id, control.clone(), state("strategy", b"forced"));
1899        add_state_components(&kernel, &control, actor, strategy);
1900
1901        kernel.start();
1902        kernel.start_trader().unwrap();
1903        kernel.dispose();
1904
1905        assert_eq!(
1906            control.events(),
1907            vec![
1908                "components.registered",
1909                "actor.load:FORCED-ACTOR",
1910                "strategy.load:FORCED-STRATEGY-001",
1911                "actor.on_start",
1912                "strategy.on_start",
1913                "actor.on_stop",
1914                "strategy.on_stop",
1915                "actor.on_save",
1916                "actor.update:FORCED-ACTOR",
1917                "strategy.on_save",
1918                "strategy.update:FORCED-STRATEGY-001",
1919                "database.close",
1920            ]
1921        );
1922    }
1923
1924    #[rstest]
1925    fn test_start_trader_starts_order_emulator_for_cached_emulated_orders() {
1926        let mut kernel = NautilusKernelBuilder::default().build().unwrap();
1927        let data_commands = register_data_command_handler("DataEngine.queue_execute.kernel_start");
1928        let instrument = crypto_perpetual_ethusdt();
1929        let instrument_id = instrument.id();
1930        let first_order = create_stop_market_order(&instrument, "O-KERNEL-001");
1931        let second_order = create_stop_market_order(&instrument, "O-KERNEL-002");
1932        let first_client_order_id = first_order.client_order_id();
1933        let second_client_order_id = second_order.client_order_id();
1934        kernel
1935            .cache
1936            .borrow_mut()
1937            .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
1938            .unwrap();
1939        kernel
1940            .cache
1941            .borrow_mut()
1942            .add_order(first_order, None, None, false)
1943            .unwrap();
1944        kernel
1945            .cache
1946            .borrow_mut()
1947            .add_order(second_order, None, None, false)
1948            .unwrap();
1949
1950        kernel.start();
1951        assert!(
1952            kernel
1953                .order_emulator
1954                .get_emulator()
1955                .get_matching_core(&instrument_id)
1956                .is_none()
1957        );
1958        kernel.start_trader().unwrap();
1959
1960        let commands = data_commands.get_messages();
1961        let cache = kernel.cache.borrow();
1962        let first_status = cache.order(&first_client_order_id).unwrap().status();
1963        let second_status = cache.order(&second_client_order_id).unwrap().status();
1964        drop(cache);
1965        let emulator = kernel.order_emulator.get_emulator();
1966        assert!(emulator.get_matching_core(&instrument_id).is_some());
1967        assert_eq!(emulator.subscribed_quotes(), vec![instrument_id]);
1968        assert_eq!(first_status, OrderStatus::Emulated);
1969        assert_eq!(second_status, OrderStatus::Emulated);
1970        assert!(commands.iter().any(|command| matches!(
1971            command,
1972            DataCommand::Subscribe(SubscribeCommand::Quotes(command))
1973                if command.instrument_id == instrument_id
1974        )));
1975
1976        data_commands.clear();
1977        drop(emulator);
1978        kernel.stop_trader();
1979        kernel.dispose();
1980
1981        let commands = data_commands.get_messages();
1982        let emulator = kernel.order_emulator.get_emulator();
1983        assert!(emulator.subscribed_quotes().is_empty());
1984        assert!(emulator.get_matching_core(&instrument_id).is_none());
1985        assert!(commands.iter().any(|command| matches!(
1986            command,
1987            DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
1988                if command.instrument_id == instrument_id
1989        )));
1990    }
1991
1992    #[rstest]
1993    fn test_reset_resets_order_emulator_state() {
1994        let mut kernel = NautilusKernelBuilder::default().build().unwrap();
1995        let data_commands = register_data_command_handler("DataEngine.queue_execute.kernel_reset");
1996        let instrument = crypto_perpetual_ethusdt();
1997        let instrument_id = instrument.id();
1998        let order = create_stop_market_order(&instrument, "O-KERNEL-RESET-001");
1999        kernel
2000            .cache
2001            .borrow_mut()
2002            .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
2003            .unwrap();
2004        kernel
2005            .cache
2006            .borrow_mut()
2007            .add_order(order, None, None, false)
2008            .unwrap();
2009
2010        kernel.start();
2011        kernel.start_trader().unwrap();
2012        assert!(
2013            kernel
2014                .order_emulator
2015                .get_emulator()
2016                .get_matching_core(&instrument_id)
2017                .is_some()
2018        );
2019        kernel.stop_trader();
2020        data_commands.clear();
2021
2022        kernel.reset();
2023
2024        let commands = data_commands.get_messages();
2025        let emulator = kernel.order_emulator.get_emulator();
2026        assert!(emulator.subscribed_quotes().is_empty());
2027        assert!(emulator.get_matching_core(&instrument_id).is_none());
2028        assert!(commands.iter().any(|command| matches!(
2029            command,
2030            DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
2031                if command.instrument_id == instrument_id
2032        )));
2033
2034        drop(emulator);
2035        kernel.dispose();
2036    }
2037}