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