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::{DataCommand, QuotesResponse, RequestCommand, RequestQuotes},
1346        msgbus::{self, MStr, ShareableMessageHandler},
1347    };
1348    use nautilus_core::Params;
1349    use nautilus_model::{
1350        data::{CustomData, DataType, NautilusDataType, QuoteTick},
1351        identifiers::InstrumentId,
1352        types::{Price, Quantity},
1353    };
1354    use nautilus_persistence::{
1355        backend::parquet::catalog::ParquetDataCatalog, config::DataCatalogConfig,
1356        test_data::RustTestCustomData,
1357    };
1358    use nautilus_serialization::ensure_custom_data_registered;
1359    use rstest::rstest;
1360    use tempfile::tempdir;
1361
1362    use super::*;
1363    use crate::config::{KernelConfig, RotationConfig, StreamingConfig};
1364
1365    #[rstest]
1366    fn test_configured_catalog_serves_builtin_quotes() {
1367        let directory = tempdir().unwrap();
1368        let instrument_id = InstrumentId::from("AUD/USD.SIM");
1369        let quotes = vec![
1370            QuoteTick::new(
1371                instrument_id,
1372                Price::from("1.00001"),
1373                Price::from("1.00003"),
1374                Quantity::from("100_000"),
1375                Quantity::from("200_000"),
1376                UnixNanos::from(1),
1377                UnixNanos::from(1),
1378            ),
1379            QuoteTick::new(
1380                instrument_id,
1381                Price::from("1.00002"),
1382                Price::from("1.00004"),
1383                Quantity::from("300_000"),
1384                Quantity::from("400_000"),
1385                UnixNanos::from(2),
1386                UnixNanos::from(2),
1387            ),
1388        ];
1389        let catalog = ParquetDataCatalog::new(directory.path(), None, None, None, None);
1390        catalog.write_to_parquet(&quotes, None, None, None).unwrap();
1391        let config = KernelConfig {
1392            catalogs: vec![
1393                DataCatalogConfig::new(
1394                    directory.path().to_string_lossy().into_owned(),
1395                    Some("file".to_string()),
1396                    None,
1397                )
1398                .with_name(Some("history".to_string())),
1399            ],
1400            ..KernelConfig::default()
1401        };
1402        let mut kernel = NautilusKernel::new("CatalogQueryTest".to_string(), config).unwrap();
1403        kernel
1404            .clock
1405            .borrow_mut()
1406            .as_any_mut()
1407            .downcast_mut::<VirtualClock>()
1408            .unwrap()
1409            .set_time(UnixNanos::from(3));
1410        let request_id = UUID4::new();
1411        let received = Rc::new(RefCell::new(Vec::new()));
1412        let received_quotes = received.clone();
1413        msgbus::register_response_handler(
1414            &request_id,
1415            ShareableMessageHandler::from_typed(move |response: &QuotesResponse| {
1416                *received_quotes.borrow_mut() = response.data.clone();
1417            }),
1418        );
1419        let request = RequestQuotes::new(
1420            instrument_id,
1421            Some(UnixNanos::from(1).to_datetime_utc()),
1422            Some(UnixNanos::from(2).to_datetime_utc()),
1423            None,
1424            None,
1425            request_id,
1426            UnixNanos::from(3),
1427            None,
1428        );
1429
1430        kernel
1431            .data_engine
1432            .borrow_mut()
1433            .execute(DataCommand::Request(RequestCommand::Quotes(request)));
1434
1435        assert_eq!(*received.borrow(), quotes);
1436        assert_eq!(kernel.data_engine.borrow().request_count(), 1);
1437        assert_eq!(kernel.data_engine.borrow().response_count(), 1);
1438        kernel.dispose();
1439    }
1440
1441    #[rstest]
1442    fn test_configured_catalog_error_names_rejected_param() {
1443        let directory = tempdir().unwrap();
1444        let path = directory.path().to_string_lossy().into_owned();
1445        let mut params = Params::new();
1446        params.insert("batch_size".to_string(), serde_json::json!(1024));
1447
1448        let config = KernelConfig {
1449            catalogs: vec![
1450                DataCatalogConfig::new(path.clone(), Some("file".to_string()), None)
1451                    .with_params(Some(params)),
1452            ],
1453            ..KernelConfig::default()
1454        };
1455
1456        let error = NautilusKernel::new("CatalogParamTest".to_string(), config)
1457            .err()
1458            .unwrap();
1459
1460        assert_eq!(
1461            error.to_string(),
1462            format!(
1463                "Failed to create data catalog from '{path}': Unknown Parquet catalog param \
1464                 'batch_size': this catalog takes no params"
1465            )
1466        );
1467    }
1468
1469    #[rstest]
1470    fn test_configured_streaming_excludes_custom_data() {
1471        ensure_custom_data_registered::<RustTestCustomData>();
1472
1473        let directory = tempdir().unwrap();
1474        let instance_id = UUID4::new();
1475        let mut streaming = StreamingConfig::new(
1476            directory.path().to_string_lossy().into_owned(),
1477            None,
1478            1_000,
1479            false,
1480            RotationConfig::NoRotation,
1481        );
1482        streaming.data_types = Some(vec![NautilusDataType::QuoteTick]);
1483        let config = KernelConfig {
1484            instance_id: Some(instance_id),
1485            streaming: Some(streaming),
1486            ..KernelConfig::default()
1487        };
1488        let mut kernel = NautilusKernel::new("BuiltInStreamingTest".to_string(), config).unwrap();
1489        let instrument_id = InstrumentId::from("RUST.TEST");
1490        let custom = CustomData::new(
1491            Arc::new(RustTestCustomData {
1492                instrument_id,
1493                value: 1.23,
1494                flag: true,
1495                ts_event: UnixNanos::from(1_000),
1496                ts_init: UnixNanos::from(1_000),
1497            }),
1498            DataType::new("RustTestCustomData", None, Some(instrument_id.to_string())),
1499        );
1500
1501        msgbus::publish_any(MStr::from("data.custom"), &custom);
1502        kernel.close_streaming_writer().unwrap();
1503
1504        let custom_path = directory
1505            .path()
1506            .join("backtest")
1507            .join(instance_id.to_string())
1508            .join("data/custom");
1509        assert!(!custom_path.exists());
1510        kernel.dispose();
1511    }
1512
1513    #[rstest]
1514    #[case::promotion_without_catalog(
1515        None,
1516        "promotion_interval_ms requires catalog: promotion needs a catalog"
1517    )]
1518    #[case::catalog_param(
1519        Some(serde_json::json!({"params": {"batch_size": 1024}})),
1520        "Unknown Parquet catalog param 'batch_size': this catalog takes no params"
1521    )]
1522    #[case::zero_batch_size(
1523        Some(serde_json::json!({"batch_size": 0})),
1524        "invalid batch_size: must be a positive number of rows; omit the field for the \
1525         backend default"
1526    )]
1527    fn test_invalid_streaming_config_fails_before_replacing_files(
1528        #[case] catalog_fields: Option<serde_json::Value>,
1529        #[case] expected: &str,
1530    ) {
1531        let directory = tempdir().unwrap();
1532        let instance_id = UUID4::new();
1533        let run_directory = directory
1534            .path()
1535            .join("backtest")
1536            .join(instance_id.to_string());
1537        std::fs::create_dir_all(&run_directory).unwrap();
1538        let existing = run_directory.join("existing.feather");
1539        std::fs::write(&existing, b"preserve").unwrap();
1540
1541        let catalog_path = directory.path().join("catalog");
1542
1543        // Deserialized like a config file, which skips `DataCatalogConfig::validate`
1544        let catalog = catalog_fields.map(|mut fields| {
1545            fields["path"] = serde_json::json!(catalog_path.to_string_lossy());
1546            serde_json::from_value::<DataCatalogConfig>(fields).unwrap()
1547        });
1548
1549        let mut streaming = StreamingConfig::new(
1550            directory.path().to_string_lossy().into_owned(),
1551            catalog,
1552            1_000,
1553            true,
1554            RotationConfig::NoRotation,
1555        );
1556        streaming.promotion_interval_ms = Some(1_000);
1557        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(VirtualClock::new()));
1558
1559        let error = NautilusKernel::setup_streaming_writer(
1560            Environment::Backtest,
1561            instance_id,
1562            &streaming,
1563            &clock,
1564        )
1565        .err()
1566        .unwrap();
1567
1568        assert_eq!(error.to_string(), expected);
1569        assert_eq!(std::fs::read(&existing).unwrap(), b"preserve");
1570    }
1571
1572    #[rstest]
1573    fn test_streaming_replace_existing_creates_missing_catalog_directory() {
1574        let directory = tempdir().unwrap();
1575        let instance_id = UUID4::new();
1576        let run_directory = directory
1577            .path()
1578            .join("backtest")
1579            .join(instance_id.to_string());
1580        std::fs::create_dir_all(&run_directory).unwrap();
1581        let existing = run_directory.join("existing.feather");
1582        std::fs::write(&existing, b"replace").unwrap();
1583        let catalog_path = directory.path().join("catalog");
1584
1585        let streaming = StreamingConfig::new(
1586            directory.path().to_string_lossy().into_owned(),
1587            Some(DataCatalogConfig::new(
1588                catalog_path.to_string_lossy().into_owned(),
1589                None,
1590                None,
1591            )),
1592            1_000,
1593            true,
1594            RotationConfig::NoRotation,
1595        );
1596        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(VirtualClock::new()));
1597
1598        let (_, subscription) = NautilusKernel::setup_streaming_writer(
1599            Environment::Backtest,
1600            instance_id,
1601            &streaming,
1602            &clock,
1603        )
1604        .unwrap();
1605        subscription.close().unwrap();
1606
1607        assert!(!existing.exists());
1608        assert!(catalog_path.is_dir());
1609    }
1610
1611    #[rstest]
1612    fn test_configured_streaming_promotes_into_separate_catalog() {
1613        let writer_directory = tempdir().unwrap();
1614        let catalog_directory = tempdir().unwrap();
1615        let catalog_path = catalog_directory.path().join("catalog");
1616        let instance_id = UUID4::new();
1617
1618        let mut streaming = StreamingConfig::new(
1619            writer_directory.path().to_string_lossy().into_owned(),
1620            Some(DataCatalogConfig::new(
1621                catalog_path.to_string_lossy().into_owned(),
1622                None,
1623                None,
1624            )),
1625            1_000,
1626            false,
1627            RotationConfig::NoRotation,
1628        );
1629        streaming.data_types = Some(vec![NautilusDataType::QuoteTick]);
1630
1631        let config = KernelConfig {
1632            instance_id: Some(instance_id),
1633            streaming: Some(streaming),
1634            ..KernelConfig::default()
1635        };
1636
1637        let mut kernel =
1638            NautilusKernel::new("SeparateCatalogStreamingTest".to_string(), config).unwrap();
1639
1640        let quote = QuoteTick::new(
1641            InstrumentId::from("AUD/USD.SIM"),
1642            Price::from("1.00001"),
1643            Price::from("1.00003"),
1644            Quantity::from("100_000"),
1645            Quantity::from("200_000"),
1646            UnixNanos::from(1),
1647            UnixNanos::from(1),
1648        );
1649
1650        msgbus::publish_quote(MStr::from("data.quotes.SIM.AUD/USD"), &quote);
1651        kernel.close_streaming_writer().unwrap();
1652
1653        let mut catalog = ParquetDataCatalog::new(&catalog_path, None, None, None, None);
1654        let promoted = catalog
1655            .query::<QuoteTick>(None, None, None, None, None, true)
1656            .unwrap();
1657        let run_directory = writer_directory
1658            .path()
1659            .join("backtest")
1660            .join(instance_id.to_string());
1661        assert_eq!(promoted, vec![quote]);
1662        assert!(run_directory.join("quotes").is_dir());
1663        assert!(!catalog_path.join("backtest").exists());
1664        kernel.dispose();
1665    }
1666}
1667
1668#[cfg(test)]
1669mod lifecycle_tests {
1670    use futures::FutureExt;
1671    use indexmap::IndexMap;
1672    use nautilus_common::{
1673        actor::registry::get_actor_unchecked,
1674        cache::Cache,
1675        clock::VirtualClock,
1676        messages::data::{DataCommand, SubscribeCommand, UnsubscribeCommand},
1677        msgbus::stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
1678    };
1679    use nautilus_execution::engine::SnapshotAnchorer;
1680    use nautilus_model::{
1681        enums::{OrderSide, OrderStatus, OrderType, TriggerType},
1682        identifiers::{ActorId, ClientOrderId, StrategyId},
1683        instruments::{
1684            CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
1685        },
1686        orders::{Order, OrderAny, OrderTestBuilder},
1687        types::{Price, Quantity},
1688    };
1689    use nautilus_testkit::{
1690        cache::TestCacheDatabaseControl,
1691        components::{StateActor, StateStrategy},
1692    };
1693    use rstest::rstest;
1694    use ustr::Ustr;
1695
1696    use super::*;
1697    use crate::{
1698        builder::NautilusKernelBuilder,
1699        event_store::{KernelEventStore, RegisteredComponents},
1700    };
1701
1702    #[derive(Debug)]
1703    struct RecordingEventStore {
1704        control: TestCacheDatabaseControl,
1705        opened: bool,
1706    }
1707
1708    impl KernelEventStore for RecordingEventStore {
1709        fn restore_parent_cache(
1710            &mut self,
1711            _instance_id: UUID4,
1712            _cache: &mut Cache,
1713        ) -> anyhow::Result<()> {
1714            self.control.record("event_store.restore");
1715            Ok(())
1716        }
1717
1718        fn open(
1719            &mut self,
1720            _instance_id: UUID4,
1721            _components: &RegisteredComponents,
1722            _environment: Environment,
1723        ) -> anyhow::Result<()> {
1724            self.control.record("event_store.open");
1725            self.opened = true;
1726            Ok(())
1727        }
1728
1729        fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
1730            None
1731        }
1732
1733        fn seal(&mut self, _ts_init: UnixNanos) {
1734            if self.opened {
1735                self.control.record("event_store.seal");
1736                self.opened = false;
1737            }
1738        }
1739
1740        fn run_id(&self) -> Option<&str> {
1741            None
1742        }
1743
1744        fn parent_run_id(&self) -> Option<&str> {
1745            None
1746        }
1747
1748        fn is_halted(&self) -> bool {
1749            false
1750        }
1751    }
1752
1753    fn state(key: &str, value: &[u8]) -> IndexMap<String, Vec<u8>> {
1754        IndexMap::from([(key.to_string(), value.to_vec())])
1755    }
1756
1757    fn finalize(kernel: &mut NautilusKernel) -> anyhow::Result<()> {
1758        kernel
1759            .finalize_stop()
1760            .now_or_never()
1761            .expect("kernel finalization must not yield")
1762    }
1763
1764    fn add_state_components(
1765        kernel: &NautilusKernel,
1766        control: &TestCacheDatabaseControl,
1767        actor: StateActor,
1768        strategy: StateStrategy,
1769    ) {
1770        kernel.trader.borrow_mut().add_actor(actor).unwrap();
1771        kernel.trader.borrow_mut().add_strategy(strategy).unwrap();
1772        control.record("components.registered");
1773    }
1774
1775    fn create_stop_market_order(instrument: &CryptoPerpetual, client_order_id: &str) -> OrderAny {
1776        OrderTestBuilder::new(OrderType::StopMarket)
1777            .instrument_id(instrument.id())
1778            .client_order_id(ClientOrderId::from(client_order_id))
1779            .side(OrderSide::Buy)
1780            .trigger_price(Price::from("5100.00"))
1781            .quantity(Quantity::from(1))
1782            .emulation_trigger(TriggerType::BidAsk)
1783            .build()
1784    }
1785
1786    fn register_data_command_handler(id: &str) -> TypedIntoMessageSavingHandler<DataCommand> {
1787        let (handler, saving_handler) =
1788            get_typed_into_message_saving_handler::<DataCommand>(Some(Ustr::from(id)));
1789        msgbus::register_data_command_endpoint(
1790            MessagingSwitchboard::data_engine_queue_execute(),
1791            handler,
1792        );
1793        saving_handler
1794    }
1795
1796    #[rstest]
1797    fn test_state_persistence_orders_restore_load_start_stop_save_seal_and_dispose() {
1798        let actor_id = ActorId::from("STATE-ACTOR");
1799        let strategy_id = StrategyId::from("STATE-STRATEGY-001");
1800        let actor_load = state("actor-loaded", b"actor-load-value");
1801        let strategy_load = state("strategy-loaded", b"strategy-load-value");
1802        let actor_save = state("actor-saved", b"actor-save-value");
1803        let strategy_save = state("strategy-saved", b"strategy-save-value");
1804        let (database, control) = TestCacheDatabaseControl::create();
1805        control.set_actor_state(actor_id, &actor_load);
1806        control.set_strategy_state(strategy_id, &strategy_load);
1807
1808        let event_store_control = control.clone();
1809        let mut kernel = NautilusKernelBuilder::default()
1810            .with_cache_database(Box::new(database))
1811            .with_event_store(move |_instance_id, _clock| {
1812                Ok(Box::new(RecordingEventStore {
1813                    control: event_store_control,
1814                    opened: false,
1815                }))
1816            })
1817            .build()
1818            .unwrap();
1819
1820        let actor = StateActor::new(actor_id, control.clone(), actor_save.clone());
1821        let strategy = StateStrategy::new(strategy_id, control.clone(), strategy_save.clone());
1822        add_state_components(&kernel, &control, actor, strategy);
1823
1824        kernel.start();
1825        kernel.start_trader().unwrap();
1826
1827        let actor_state = get_actor_unchecked::<StateActor>(&actor_id.inner())
1828            .state_load()
1829            .cloned();
1830        let strategy_state = get_actor_unchecked::<StateStrategy>(&strategy_id.inner())
1831            .state_load()
1832            .cloned();
1833        assert_eq!(actor_state, Some(actor_load));
1834        assert_eq!(strategy_state, Some(strategy_load));
1835
1836        kernel.stop_trader();
1837        kernel.stop_trader();
1838        finalize(&mut kernel).unwrap();
1839        finalize(&mut kernel).unwrap();
1840        kernel.dispose();
1841
1842        assert_eq!(
1843            control.events(),
1844            vec![
1845                "components.registered",
1846                "event_store.restore",
1847                "event_store.open",
1848                "actor.load:STATE-ACTOR",
1849                "actor.on_load",
1850                "strategy.load:STATE-STRATEGY-001",
1851                "strategy.on_load",
1852                "actor.on_start",
1853                "strategy.on_start",
1854                "actor.on_stop",
1855                "strategy.on_stop",
1856                "actor.on_save",
1857                "actor.update:STATE-ACTOR",
1858                "strategy.on_save",
1859                "strategy.update:STATE-STRATEGY-001",
1860                "event_store.seal",
1861                "database.close",
1862            ]
1863        );
1864        assert_eq!(control.actor_state(&actor_id), Some(actor_save));
1865        assert_eq!(control.strategy_state(&strategy_id), Some(strategy_save));
1866    }
1867
1868    #[rstest]
1869    fn test_state_persistence_skips_callbacks_without_cache_backing() {
1870        let actor_id = ActorId::from("NO-BACKING-ACTOR");
1871        let strategy_id = StrategyId::from("NO-BACKING-STRATEGY-001");
1872        let control = TestCacheDatabaseControl::default();
1873        let mut kernel = NautilusKernelBuilder::default().build().unwrap();
1874        let actor = StateActor::new(actor_id, control.clone(), state("actor", b"save"));
1875        let strategy = StateStrategy::new(strategy_id, control.clone(), state("strategy", b"save"));
1876        add_state_components(&kernel, &control, actor, strategy);
1877
1878        kernel.start();
1879        kernel.start_trader().unwrap();
1880        kernel.stop_trader();
1881        finalize(&mut kernel).unwrap();
1882        kernel.dispose();
1883
1884        assert_eq!(
1885            control.events(),
1886            vec![
1887                "components.registered",
1888                "actor.on_start",
1889                "strategy.on_start",
1890                "actor.on_stop",
1891                "strategy.on_stop",
1892            ]
1893        );
1894    }
1895
1896    #[rstest]
1897    fn test_state_persistence_skips_empty_load_and_persists_empty_save() {
1898        let actor_id = ActorId::from("EMPTY-STATE-ACTOR");
1899        let strategy_id = StrategyId::from("EMPTY-STATE-STRATEGY-001");
1900        let (database, control) = TestCacheDatabaseControl::create();
1901        let mut kernel = NautilusKernelBuilder::default()
1902            .with_cache_database(Box::new(database))
1903            .build()
1904            .unwrap();
1905        let actor = StateActor::new(actor_id, control.clone(), IndexMap::new());
1906        let strategy = StateStrategy::new(strategy_id, control.clone(), IndexMap::new());
1907        add_state_components(&kernel, &control, actor, strategy);
1908
1909        kernel.start();
1910        kernel.start_trader().unwrap();
1911        kernel.stop_trader();
1912        finalize(&mut kernel).unwrap();
1913
1914        assert_eq!(
1915            control.events(),
1916            vec![
1917                "components.registered",
1918                "actor.load:EMPTY-STATE-ACTOR",
1919                "strategy.load:EMPTY-STATE-STRATEGY-001",
1920                "actor.on_start",
1921                "strategy.on_start",
1922                "actor.on_stop",
1923                "strategy.on_stop",
1924                "actor.on_save",
1925                "actor.update:EMPTY-STATE-ACTOR",
1926                "strategy.on_save",
1927                "strategy.update:EMPTY-STATE-STRATEGY-001",
1928            ]
1929        );
1930        assert_eq!(control.actor_state(&actor_id), Some(IndexMap::new()));
1931        assert_eq!(control.strategy_state(&strategy_id), Some(IndexMap::new()));
1932        kernel.dispose();
1933    }
1934
1935    #[rstest]
1936    fn test_state_save_reports_all_callback_errors_and_continues_shutdown() {
1937        let actor_id = ActorId::from("FAIL-SAVE-ACTOR");
1938        let strategy_id = StrategyId::from("FAIL-SAVE-STRATEGY-001");
1939        let (database, control) = TestCacheDatabaseControl::create();
1940        let event_store_control = control.clone();
1941        let mut kernel = NautilusKernelBuilder::default()
1942            .with_cache_database(Box::new(database))
1943            .with_event_store(move |_instance_id, _clock| {
1944                Ok(Box::new(RecordingEventStore {
1945                    control: event_store_control,
1946                    opened: false,
1947                }))
1948            })
1949            .build()
1950            .unwrap();
1951        let actor = StateActor::new(actor_id, control.clone(), IndexMap::new()).with_fail_save();
1952        let strategy =
1953            StateStrategy::new(strategy_id, control.clone(), IndexMap::new()).with_fail_save();
1954        add_state_components(&kernel, &control, actor, strategy);
1955
1956        kernel.start();
1957        kernel.start_trader().unwrap();
1958        kernel.stop_trader();
1959        let expected_shutdown = kernel.clock.borrow().timestamp_ns();
1960        let error = finalize(&mut kernel).unwrap_err();
1961        kernel.dispose();
1962
1963        assert_eq!(
1964            error.to_string(),
1965            "Failed to save component state: actor FAIL-SAVE-ACTOR callback: test actor on_save \
1966             failure; strategy FAIL-SAVE-STRATEGY-001 callback: test strategy on_save failure"
1967        );
1968        assert_eq!(kernel.ts_shutdown, Some(expected_shutdown));
1969        assert_eq!(
1970            control.events(),
1971            vec![
1972                "components.registered",
1973                "event_store.restore",
1974                "event_store.open",
1975                "actor.load:FAIL-SAVE-ACTOR",
1976                "strategy.load:FAIL-SAVE-STRATEGY-001",
1977                "actor.on_start",
1978                "strategy.on_start",
1979                "actor.on_stop",
1980                "strategy.on_stop",
1981                "actor.on_save",
1982                "strategy.on_save",
1983                "event_store.seal",
1984                "database.close",
1985            ]
1986        );
1987    }
1988
1989    #[rstest]
1990    fn test_state_load_callback_failure_prevents_start_and_save() {
1991        let actor_id = ActorId::from("FAIL-LOAD-ACTOR");
1992        let strategy_id = StrategyId::from("FAIL-LOAD-STRATEGY-001");
1993        let (database, control) = TestCacheDatabaseControl::create();
1994        control.set_actor_state(actor_id, &state("actor", b"load"));
1995        control.set_strategy_state(strategy_id, &state("strategy", b"load"));
1996        let mut kernel = NautilusKernelBuilder::default()
1997            .with_cache_database(Box::new(database))
1998            .build()
1999            .unwrap();
2000        let actor = StateActor::new(actor_id, control.clone(), IndexMap::new()).with_fail_load();
2001        let strategy = StateStrategy::new(strategy_id, control.clone(), IndexMap::new());
2002        add_state_components(&kernel, &control, actor, strategy);
2003
2004        kernel.start();
2005        let error = kernel.start_trader().unwrap_err();
2006        kernel.dispose();
2007
2008        assert_eq!(
2009            error.to_string(),
2010            "Failed to load actor and strategy state: Failed to restore actor FAIL-LOAD-ACTOR \
2011             state: test actor on_load failure"
2012        );
2013        assert_eq!(
2014            control.events(),
2015            vec![
2016                "components.registered",
2017                "actor.load:FAIL-LOAD-ACTOR",
2018                "actor.on_load",
2019                "database.close",
2020            ]
2021        );
2022    }
2023
2024    #[rstest]
2025    fn test_state_save_reports_all_persistence_errors() {
2026        let actor_id = ActorId::from("FAIL-UPDATE-ACTOR");
2027        let strategy_id = StrategyId::from("FAIL-UPDATE-STRATEGY-001");
2028        let (database, control) = TestCacheDatabaseControl::create();
2029        control.set_fail_update_actor(true);
2030        control.set_fail_update_strategy(true);
2031        let mut kernel = NautilusKernelBuilder::default()
2032            .with_cache_database(Box::new(database))
2033            .build()
2034            .unwrap();
2035        let actor = StateActor::new(actor_id, control.clone(), state("actor", b"save"));
2036        let strategy = StateStrategy::new(strategy_id, control.clone(), state("strategy", b"save"));
2037        add_state_components(&kernel, &control, actor, strategy);
2038
2039        kernel.start();
2040        kernel.start_trader().unwrap();
2041        kernel.stop_trader();
2042        let error = finalize(&mut kernel).unwrap_err();
2043        kernel.dispose();
2044
2045        assert_eq!(
2046            error.to_string(),
2047            "Failed to save component state: actor FAIL-UPDATE-ACTOR persistence: test actor \
2048             update failure; strategy FAIL-UPDATE-STRATEGY-001 persistence: test strategy update \
2049             failure"
2050        );
2051        assert_eq!(
2052            control.events(),
2053            vec![
2054                "components.registered",
2055                "actor.load:FAIL-UPDATE-ACTOR",
2056                "strategy.load:FAIL-UPDATE-STRATEGY-001",
2057                "actor.on_start",
2058                "strategy.on_start",
2059                "actor.on_stop",
2060                "strategy.on_stop",
2061                "actor.on_save",
2062                "actor.update:FAIL-UPDATE-ACTOR",
2063                "strategy.on_save",
2064                "strategy.update:FAIL-UPDATE-STRATEGY-001",
2065                "database.close",
2066            ]
2067        );
2068    }
2069
2070    #[rstest]
2071    fn test_partial_startup_stops_and_saves_once() {
2072        let actor_id = ActorId::from("PARTIAL-ACTOR");
2073        let strategy_id = StrategyId::from("PARTIAL-STRATEGY-001");
2074        let (database, control) = TestCacheDatabaseControl::create();
2075        let mut kernel = NautilusKernelBuilder::default()
2076            .with_cache_database(Box::new(database))
2077            .build()
2078            .unwrap();
2079        let actor = StateActor::new(actor_id, control.clone(), state("actor", b"partial"));
2080        let strategy =
2081            StateStrategy::new(strategy_id, control.clone(), state("strategy", b"partial"))
2082                .with_fail_start();
2083        add_state_components(&kernel, &control, actor, strategy);
2084
2085        kernel.start();
2086        let error = kernel.start_trader().unwrap_err();
2087        kernel.dispose();
2088
2089        assert_eq!(
2090            error.to_string(),
2091            "Failed to start trader: test strategy on_start failure"
2092        );
2093        assert_eq!(
2094            control.events(),
2095            vec![
2096                "components.registered",
2097                "actor.load:PARTIAL-ACTOR",
2098                "strategy.load:PARTIAL-STRATEGY-001",
2099                "actor.on_start",
2100                "strategy.on_start",
2101                "actor.on_stop",
2102                "strategy.on_stop",
2103                "actor.on_save",
2104                "actor.update:PARTIAL-ACTOR",
2105                "strategy.on_save",
2106                "strategy.update:PARTIAL-STRATEGY-001",
2107                "database.close",
2108            ]
2109        );
2110        assert_eq!(
2111            control.actor_state(&actor_id),
2112            Some(state("actor", b"partial"))
2113        );
2114        assert_eq!(
2115            control.strategy_state(&strategy_id),
2116            Some(state("strategy", b"partial"))
2117        );
2118    }
2119
2120    #[rstest]
2121    fn test_forced_dispose_stops_and_saves_once() {
2122        let actor_id = ActorId::from("FORCED-ACTOR");
2123        let strategy_id = StrategyId::from("FORCED-STRATEGY-001");
2124        let (database, control) = TestCacheDatabaseControl::create();
2125        let mut kernel = NautilusKernelBuilder::default()
2126            .with_cache_database(Box::new(database))
2127            .build()
2128            .unwrap();
2129        let actor = StateActor::new(actor_id, control.clone(), state("actor", b"forced"));
2130        let strategy =
2131            StateStrategy::new(strategy_id, control.clone(), state("strategy", b"forced"));
2132        add_state_components(&kernel, &control, actor, strategy);
2133
2134        kernel.start();
2135        kernel.start_trader().unwrap();
2136        kernel.dispose();
2137
2138        assert_eq!(
2139            control.events(),
2140            vec![
2141                "components.registered",
2142                "actor.load:FORCED-ACTOR",
2143                "strategy.load:FORCED-STRATEGY-001",
2144                "actor.on_start",
2145                "strategy.on_start",
2146                "actor.on_stop",
2147                "strategy.on_stop",
2148                "actor.on_save",
2149                "actor.update:FORCED-ACTOR",
2150                "strategy.on_save",
2151                "strategy.update:FORCED-STRATEGY-001",
2152                "database.close",
2153            ]
2154        );
2155    }
2156
2157    #[rstest]
2158    fn test_start_trader_starts_order_emulator_for_cached_emulated_orders() {
2159        let mut kernel = NautilusKernelBuilder::default().build().unwrap();
2160        let data_commands = register_data_command_handler("DataEngine.queue_execute.kernel_start");
2161        let instrument = crypto_perpetual_ethusdt();
2162        let instrument_id = instrument.id();
2163        let first_order = create_stop_market_order(&instrument, "O-KERNEL-001");
2164        let second_order = create_stop_market_order(&instrument, "O-KERNEL-002");
2165        let first_client_order_id = first_order.client_order_id();
2166        let second_client_order_id = second_order.client_order_id();
2167        kernel
2168            .cache
2169            .borrow_mut()
2170            .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
2171            .unwrap();
2172        kernel
2173            .cache
2174            .borrow_mut()
2175            .add_order(first_order, None, None, false)
2176            .unwrap();
2177        kernel
2178            .cache
2179            .borrow_mut()
2180            .add_order(second_order, None, None, false)
2181            .unwrap();
2182
2183        kernel.start();
2184        assert!(
2185            kernel
2186                .order_emulator
2187                .get_emulator()
2188                .get_matching_core(&instrument_id)
2189                .is_none()
2190        );
2191        kernel.start_trader().unwrap();
2192
2193        let commands = data_commands.get_messages();
2194        let cache = kernel.cache.borrow();
2195        let first_status = cache.order(&first_client_order_id).unwrap().status();
2196        let second_status = cache.order(&second_client_order_id).unwrap().status();
2197        drop(cache);
2198        let emulator = kernel.order_emulator.get_emulator();
2199        assert!(emulator.get_matching_core(&instrument_id).is_some());
2200        assert_eq!(emulator.subscribed_quotes(), vec![instrument_id]);
2201        assert_eq!(first_status, OrderStatus::Emulated);
2202        assert_eq!(second_status, OrderStatus::Emulated);
2203        assert!(commands.iter().any(|command| matches!(
2204            command,
2205            DataCommand::Subscribe(SubscribeCommand::Quotes(command))
2206                if command.instrument_id == instrument_id
2207        )));
2208
2209        data_commands.clear();
2210        drop(emulator);
2211        kernel.stop_trader();
2212        kernel.dispose();
2213
2214        let commands = data_commands.get_messages();
2215        let emulator = kernel.order_emulator.get_emulator();
2216        assert!(emulator.subscribed_quotes().is_empty());
2217        assert!(emulator.get_matching_core(&instrument_id).is_none());
2218        assert!(commands.iter().any(|command| matches!(
2219            command,
2220            DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
2221                if command.instrument_id == instrument_id
2222        )));
2223    }
2224
2225    #[rstest]
2226    fn test_reset_resets_order_emulator_state() {
2227        let mut kernel = NautilusKernelBuilder::default().build().unwrap();
2228        let data_commands = register_data_command_handler("DataEngine.queue_execute.kernel_reset");
2229        let instrument = crypto_perpetual_ethusdt();
2230        let instrument_id = instrument.id();
2231        let order = create_stop_market_order(&instrument, "O-KERNEL-RESET-001");
2232        kernel
2233            .cache
2234            .borrow_mut()
2235            .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
2236            .unwrap();
2237        kernel
2238            .cache
2239            .borrow_mut()
2240            .add_order(order, None, None, false)
2241            .unwrap();
2242
2243        kernel.start();
2244        kernel.start_trader().unwrap();
2245        assert!(
2246            kernel
2247                .order_emulator
2248                .get_emulator()
2249                .get_matching_core(&instrument_id)
2250                .is_some()
2251        );
2252        kernel.stop_trader();
2253        data_commands.clear();
2254
2255        kernel.reset();
2256
2257        let commands = data_commands.get_messages();
2258        let emulator = kernel.order_emulator.get_emulator();
2259        assert!(emulator.subscribed_quotes().is_empty());
2260        assert!(emulator.get_matching_core(&instrument_id).is_none());
2261        assert!(commands.iter().any(|command| matches!(
2262            command,
2263            DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
2264                if command.instrument_id == instrument_id
2265        )));
2266
2267        drop(emulator);
2268        kernel.dispose();
2269    }
2270
2271    #[rstest]
2272    fn test_start_after_reset_skips_trader_initialize() {
2273        let mut kernel = NautilusKernelBuilder::default().build().unwrap();
2274        kernel.start();
2275        kernel.start_trader().unwrap();
2276        kernel.stop_trader();
2277        kernel.reset();
2278        let restart_ns = UnixNanos::from(1_700_000_000_000_000_000);
2279        kernel
2280            .clock
2281            .borrow_mut()
2282            .as_any_mut()
2283            .downcast_mut::<VirtualClock>()
2284            .unwrap()
2285            .set_time(restart_ns);
2286
2287        kernel.start();
2288        let ts_started = kernel.ts_started();
2289        kernel.start_trader().unwrap();
2290        let trader_state = kernel.trader.borrow().state();
2291        kernel.stop_trader();
2292        kernel.dispose();
2293
2294        assert_eq!(ts_started, Some(restart_ns));
2295        assert_eq!(trader_state, ComponentState::Running);
2296    }
2297}