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