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
16use std::{
17    cell::{Cell, Ref, RefCell},
18    fmt::Debug,
19    rc::Rc,
20    time::Duration,
21};
22
23use nautilus_common::{
24    cache::{Cache, CacheConfig, database::CacheDatabaseAdapter},
25    clock::Clock,
26    component::Component,
27    enums::Environment,
28    logging::{
29        arm_shutdown_on_error, disarm_shutdown_on_error, headers, init_logging,
30        logger::{LogGuard, LoggerConfig},
31        try_drain_shutdown_on_error_trigger,
32    },
33    messages::system::ShutdownSystem,
34    msgbus::{
35        self, MessageBus, MessagingSwitchboard, ShareableMessageHandler, get_message_bus,
36        set_message_bus,
37    },
38};
39use nautilus_core::{UUID4, UnixNanos};
40use nautilus_data::engine::DataEngine;
41use nautilus_execution::{
42    engine::ExecutionEngine,
43    order_emulator::{adapter::OrderEmulatorAdapter, emulator::OrderEmulator},
44};
45use nautilus_model::identifiers::{ClientId, TraderId};
46use nautilus_portfolio::portfolio::Portfolio;
47use nautilus_risk::engine::RiskEngine;
48use ustr::Ustr;
49
50use crate::{
51    builder::NautilusKernelBuilder,
52    clock_factory::ClockFactory,
53    config::NautilusKernelConfig,
54    event_store::{EventStoreFactory, KernelEventStore, RegisteredComponents},
55    trader::Trader,
56};
57
58/// Core Nautilus system kernel.
59///
60/// Orchestrates data and execution engines, cache, clock, and messaging across environments.
61#[derive(Debug)]
62pub struct NautilusKernel {
63    /// The kernel name (for logging and identification).
64    pub name: String,
65    /// The unique instance identifier for this kernel.
66    pub instance_id: UUID4,
67    /// The machine identifier (hostname or similar).
68    pub machine_id: String,
69    /// The kernel configuration.
70    pub config: Box<dyn NautilusKernelConfig>,
71    /// The shared in-memory cache.
72    pub cache: Rc<RefCell<Cache>>,
73    /// The clock driving the kernel.
74    pub clock: Rc<RefCell<dyn Clock>>,
75    /// The portfolio manager.
76    pub portfolio: Rc<RefCell<Portfolio>>,
77    /// Guard for the logging subsystem (keeps logger thread alive).
78    pub log_guard: LogGuard,
79    /// The data engine instance.
80    pub data_engine: Rc<RefCell<DataEngine>>,
81    /// The risk engine instance.
82    pub risk_engine: Rc<RefCell<RiskEngine>>,
83    /// The execution engine instance.
84    pub exec_engine: Rc<RefCell<ExecutionEngine>>,
85    /// The order emulator for handling emulated orders.
86    pub order_emulator: OrderEmulatorAdapter,
87    /// The trader component (shared for [`Controller`](crate::controller::Controller) access).
88    pub trader: Rc<RefCell<Trader>>,
89    /// The UNIX timestamp (nanoseconds) when the kernel was created.
90    pub ts_created: UnixNanos,
91    /// The UNIX timestamp (nanoseconds) when the kernel was last started.
92    pub ts_started: Option<UnixNanos>,
93    /// The UNIX timestamp (nanoseconds) when the kernel was last shutdown.
94    pub ts_shutdown: Option<UnixNanos>,
95    shutdown_requested: Rc<Cell<bool>>,
96    event_store: Option<Box<dyn KernelEventStore>>,
97    event_store_replay: bool,
98}
99
100/// Optional construction-time dependencies for [`NautilusKernel`].
101#[derive(Default)]
102pub struct NautilusKernelDependencies {
103    clock_factory: Option<ClockFactory>,
104    cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
105    event_store_factory: Option<EventStoreFactory>,
106}
107
108impl Debug for NautilusKernelDependencies {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.debug_struct(stringify!(NautilusKernelDependencies))
111            .field("clock_factory", &self.clock_factory.is_some())
112            .field("cache_database", &self.cache_database.is_some())
113            .field("event_store_factory", &self.event_store_factory.is_some())
114            .finish()
115    }
116}
117
118impl NautilusKernelDependencies {
119    /// Add a clock factory.
120    #[must_use]
121    pub fn with_clock_factory(mut self, clock_factory: Option<ClockFactory>) -> Self {
122        self.clock_factory = clock_factory;
123        self
124    }
125
126    /// Add a cache database adapter.
127    #[must_use]
128    pub fn with_cache_database(
129        mut self,
130        cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
131    ) -> Self {
132        self.cache_database = cache_database;
133        self
134    }
135
136    /// Add an event-store factory.
137    #[must_use]
138    pub fn with_event_store_factory(
139        mut self,
140        event_store_factory: Option<EventStoreFactory>,
141    ) -> Self {
142        self.event_store_factory = event_store_factory;
143        self
144    }
145}
146
147impl NautilusKernel {
148    /// Create a new [`NautilusKernelBuilder`] for fluent configuration.
149    #[must_use]
150    pub const fn builder(
151        name: String,
152        trader_id: TraderId,
153        environment: Environment,
154    ) -> NautilusKernelBuilder {
155        NautilusKernelBuilder::new(name, trader_id, environment)
156    }
157
158    /// Create a new [`NautilusKernel`] instance.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the kernel fails to initialize.
163    pub fn new<T: NautilusKernelConfig + 'static>(name: String, config: T) -> anyhow::Result<Self> {
164        Self::new_with(name, config, None, None)
165    }
166
167    /// Create a new [`NautilusKernel`] instance with an injected cache database adapter.
168    ///
169    /// The adapter is passed straight to [`Cache::new`] so the kernel can restore
170    /// generic cache state (including snapshot blobs anchored by the event store) from
171    /// the durable backing store on startup, without an external caller pre-seeding the
172    /// in-memory cache.
173    ///
174    /// # Errors
175    ///
176    /// Returns an error if the kernel fails to initialize.
177    pub fn new_with_cache_database<T: NautilusKernelConfig + 'static>(
178        name: String,
179        config: T,
180        cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
181    ) -> anyhow::Result<Self> {
182        Self::new_with(name, config, cache_database, None)
183    }
184
185    /// Create a new [`NautilusKernel`] instance with optional cache database and event store
186    /// injections.
187    ///
188    /// The cache adapter is passed to [`Cache::new`]; the event-store factory is invoked
189    /// with the kernel's clock so the resulting [`KernelEventStore`] implementation shares
190    /// the same time source the kernel uses to stamp `RunStarted`/`RunEnded` and any
191    /// drop-seal fallback timestamp.
192    ///
193    /// # Errors
194    ///
195    /// Returns an error if the kernel fails to initialize or the event-store factory fails.
196    pub fn new_with<T: NautilusKernelConfig + 'static>(
197        name: String,
198        config: T,
199        cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
200        event_store_factory: Option<EventStoreFactory>,
201    ) -> anyhow::Result<Self> {
202        Self::new_with_dependencies(
203            name,
204            config,
205            NautilusKernelDependencies::default()
206                .with_cache_database(cache_database)
207                .with_event_store_factory(event_store_factory),
208        )
209    }
210
211    /// Create a new [`NautilusKernel`] instance with construction-time dependencies.
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if the kernel fails to initialize or an injected factory fails.
216    pub fn new_with_dependencies<T: NautilusKernelConfig + 'static>(
217        name: String,
218        config: T,
219        dependencies: NautilusKernelDependencies,
220    ) -> anyhow::Result<Self> {
221        let NautilusKernelDependencies {
222            clock_factory,
223            cache_database,
224            event_store_factory,
225        } = dependencies;
226        let instance_id = config.instance_id().unwrap_or_default();
227        let machine_id = Self::determine_machine_id()?;
228
229        let logger_config = config.logging();
230        let log_guard = Self::initialize_logging(config.trader_id(), instance_id, logger_config)?;
231        headers::log_header(
232            config.trader_id(),
233            &machine_id,
234            instance_id,
235            Ustr::from(&name),
236        );
237
238        log::info!("Building system kernel");
239
240        let clock_factory =
241            clock_factory.unwrap_or_else(|| ClockFactory::for_environment(config.environment()));
242        let clock = clock_factory.clock();
243        let event_store = match event_store_factory {
244            Some(factory) => Some(factory(instance_id, clock.clone())?),
245            None => None,
246        };
247        let cache = Self::initialize_cache(config.cache(), cache_database);
248
249        let msgbus = Rc::new(RefCell::new(MessageBus::new(
250            config.trader_id(),
251            instance_id,
252            Some(name.clone()),
253            None,
254        )));
255        set_message_bus(msgbus);
256
257        if let Some(config) = config.msgbus()
258            && let Some(filter) = config.types_filter
259        {
260            get_message_bus().borrow_mut().set_types_filter(filter);
261        }
262
263        let portfolio = Rc::new(RefCell::new(Portfolio::new(
264            clock.clone(),
265            cache.clone(),
266            config.portfolio(),
267        )));
268
269        let risk_engine = RiskEngine::new(
270            config.risk_engine().unwrap_or_default(),
271            portfolio.borrow().clone_shallow(),
272            clock.clone(),
273            cache.clone(),
274        );
275        let risk_engine = Rc::new(RefCell::new(risk_engine));
276
277        let exec_engine = ExecutionEngine::new(clock.clone(), cache.clone(), config.exec_engine());
278        let exec_engine = Rc::new(RefCell::new(exec_engine));
279
280        let order_emulator = OrderEmulatorAdapter::new(clock.clone(), cache.clone());
281
282        let data_engine = DataEngine::new(clock.clone(), cache.clone(), config.data_engine());
283        let data_engine = Rc::new(RefCell::new(data_engine));
284
285        DataEngine::register_msgbus_handlers(&data_engine);
286        RiskEngine::register_msgbus_handlers(&risk_engine);
287        ExecutionEngine::register_msgbus_handlers(&exec_engine);
288        OrderEmulator::register_msgbus_handlers(&order_emulator.emulator());
289
290        let shutdown_requested = Rc::new(Cell::new(false));
291        Self::register_shutdown_handler(config.trader_id(), shutdown_requested.clone());
292
293        let trader = Rc::new(RefCell::new(Trader::new(
294            config.trader_id(),
295            instance_id,
296            config.environment(),
297            clock_factory,
298            cache.clone(),
299            portfolio.clone(),
300        )));
301
302        let ts_created = clock.borrow().timestamp_ns();
303
304        Ok(Self {
305            name,
306            instance_id,
307            machine_id,
308            event_store,
309            config: Box::new(config),
310            cache,
311            clock,
312            portfolio,
313            log_guard,
314            data_engine,
315            risk_engine,
316            exec_engine,
317            order_emulator,
318            trader,
319            ts_created,
320            ts_started: None,
321            ts_shutdown: None,
322            shutdown_requested,
323            event_store_replay: false,
324        })
325    }
326
327    fn register_shutdown_handler(trader_id: TraderId, shutdown_requested: Rc<Cell<bool>>) {
328        let handler = ShareableMessageHandler::from_typed(move |cmd: &ShutdownSystem| {
329            if cmd.trader_id != trader_id {
330                log::warn!("Received {cmd} not for this trader {trader_id}, ignoring");
331                return;
332            }
333
334            if shutdown_requested.get() {
335                log::debug!("Shutdown already requested, ignoring {cmd}");
336                return;
337            }
338
339            log::info!("Received {cmd}, requesting shutdown");
340            shutdown_requested.set(true);
341        });
342        let topic = MessagingSwitchboard::shutdown_system_topic();
343        msgbus::subscribe_any(topic.into(), handler, None);
344    }
345
346    fn determine_machine_id() -> anyhow::Result<String> {
347        sysinfo::System::host_name().ok_or_else(|| anyhow::anyhow!("Failed to determine hostname"))
348    }
349
350    fn initialize_logging(
351        trader_id: TraderId,
352        instance_id: UUID4,
353        config: LoggerConfig,
354    ) -> anyhow::Result<LogGuard> {
355        #[cfg(feature = "tracing-bridge")]
356        let use_tracing = config.use_tracing;
357
358        let file_config = config.file_config.clone().unwrap_or_default();
359        let log_guard = match init_logging(trader_id, instance_id, config, file_config) {
360            Ok(guard) => guard,
361            Err(e) => {
362                // Only recover from SetLoggerError (logger already registered).
363                // This is common in tests where multiple kernels are created and
364                // the log crate's global logger persists after LogGuard teardown.
365                // Any other error (e.g. thread spawn failure) is propagated.
366                if e.downcast_ref::<log::SetLoggerError>().is_some() {
367                    if let Some(guard) = LogGuard::new() {
368                        guard
369                    } else {
370                        return Err(e.context(
371                            "A non-Nautilus logger is already registered; \
372                             cannot initialize Nautilus logging",
373                        ));
374                    }
375                } else {
376                    return Err(e);
377                }
378            }
379        };
380
381        // Initialize tracing subscriber if enabled (idempotent)
382        #[cfg(feature = "tracing-bridge")]
383        if use_tracing && !nautilus_common::logging::bridge::tracing_is_initialized() {
384            nautilus_common::logging::bridge::init_tracing()?;
385        }
386
387        Ok(log_guard)
388    }
389
390    fn initialize_cache(
391        cache_config: Option<CacheConfig>,
392        cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
393    ) -> Rc<RefCell<Cache>> {
394        let cache_config = cache_config.unwrap_or_default();
395        let cache = Cache::new(Some(cache_config), cache_database);
396
397        Rc::new(RefCell::new(cache))
398    }
399
400    fn cancel_timers(&self) {
401        self.clock.borrow_mut().cancel_timers();
402    }
403
404    #[must_use]
405    pub fn generate_timestamp_ns(&self) -> UnixNanos {
406        self.clock.borrow().timestamp_ns()
407    }
408
409    /// Returns the kernel's environment context (Backtest, Sandbox, Live).
410    #[must_use]
411    pub fn environment(&self) -> Environment {
412        self.config.environment()
413    }
414
415    /// Returns the kernel's name.
416    #[must_use]
417    pub const fn name(&self) -> &str {
418        self.name.as_str()
419    }
420
421    /// Returns the kernel's trader ID.
422    #[must_use]
423    pub fn trader_id(&self) -> TraderId {
424        self.config.trader_id()
425    }
426
427    /// Returns the kernel's machine ID.
428    #[must_use]
429    pub fn machine_id(&self) -> &str {
430        &self.machine_id
431    }
432
433    /// Returns the kernel's instance ID.
434    #[must_use]
435    pub const fn instance_id(&self) -> UUID4 {
436        self.instance_id
437    }
438
439    /// Returns the delay after stopping the node to await residual events before final shutdown.
440    #[must_use]
441    pub fn delay_post_stop(&self) -> Duration {
442        self.config.delay_post_stop()
443    }
444
445    /// Returns the UNIX timestamp (ns) when the kernel was created.
446    #[must_use]
447    pub const fn ts_created(&self) -> UnixNanos {
448        self.ts_created
449    }
450
451    /// Returns the UNIX timestamp (ns) when the kernel was last started.
452    #[must_use]
453    pub const fn ts_started(&self) -> Option<UnixNanos> {
454        self.ts_started
455    }
456
457    /// Returns the UNIX timestamp (ns) when the kernel was last shutdown.
458    #[must_use]
459    pub const fn ts_shutdown(&self) -> Option<UnixNanos> {
460        self.ts_shutdown
461    }
462
463    /// Returns `true` if shutdown has been requested.
464    ///
465    /// Drains pending shutdown-on-error logs before checking the kernel flag.
466    #[must_use]
467    pub fn is_shutdown_requested(&self) -> bool {
468        self.drain_shutdown_on_error_trigger();
469        self.shutdown_requested.get()
470    }
471
472    /// Clears the shutdown flag.
473    ///
474    /// Call this before starting a fresh run so a prior `ShutdownSystem`
475    /// command does not abort it.
476    pub fn reset_shutdown_flag(&self) {
477        self.shutdown_requested.set(false);
478    }
479
480    /// Returns a shared handle to the shutdown flag for async runtimes
481    /// that need to poll it outside the kernel's direct borrow.
482    #[must_use]
483    pub fn shutdown_flag(&self) -> Rc<Cell<bool>> {
484        self.shutdown_requested.clone()
485    }
486
487    fn drain_shutdown_on_error_trigger(&self) {
488        try_drain_shutdown_on_error_trigger(|trigger| {
489            let command = ShutdownSystem::new(
490                self.config.trader_id(),
491                trigger.component,
492                Some(format!(
493                    "Error log received from {}: {}",
494                    trigger.component, trigger.message
495                )),
496                UUID4::new(),
497                trigger.timestamp,
498                None,
499            );
500
501            msgbus::try_publish_any(
502                MessagingSwitchboard::shutdown_system_topic(),
503                command.as_any(),
504            )
505        });
506    }
507
508    /// Returns whether the kernel has been configured to load state.
509    #[must_use]
510    pub fn load_state(&self) -> bool {
511        self.config.load_state()
512    }
513
514    /// Returns whether the kernel has been configured to save state.
515    #[must_use]
516    pub fn save_state(&self) -> bool {
517        self.config.save_state()
518    }
519
520    /// Returns the kernel's clock.
521    #[must_use]
522    pub fn clock(&self) -> Rc<RefCell<dyn Clock>> {
523        self.clock.clone()
524    }
525
526    /// Returns the kernel's cache.
527    #[must_use]
528    pub fn cache(&self) -> Rc<RefCell<Cache>> {
529        self.cache.clone()
530    }
531
532    /// Returns the kernel's portfolio.
533    #[must_use]
534    pub fn portfolio(&self) -> Ref<'_, Portfolio> {
535        self.portfolio.borrow()
536    }
537
538    /// Returns the kernel's data engine.
539    #[must_use]
540    pub fn data_engine(&self) -> Ref<'_, DataEngine> {
541        self.data_engine.borrow()
542    }
543
544    /// Returns the kernel's risk engine.
545    #[must_use]
546    pub const fn risk_engine(&self) -> &Rc<RefCell<RiskEngine>> {
547        &self.risk_engine
548    }
549
550    /// Returns the kernel's execution engine.
551    #[must_use]
552    pub const fn exec_engine(&self) -> &Rc<RefCell<ExecutionEngine>> {
553        &self.exec_engine
554    }
555
556    /// Returns the kernel's trader (shared reference).
557    #[must_use]
558    pub fn trader(&self) -> &Rc<RefCell<Trader>> {
559        &self.trader
560    }
561
562    /// Starts the Nautilus system kernel synchronously (for backtest use).
563    pub fn start(&mut self) {
564        arm_shutdown_on_error(self.config.shutdown_on_error());
565        log::info!("Starting");
566
567        self.event_store_replay = false;
568
569        if let Some(event_store) = self.event_store.as_deref_mut() {
570            self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
571
572            let components = Self::collect_registered_components(&self.trader);
573            let environment = self.config.environment();
574            let event_store_replay_configured = event_store.is_event_store_replay_configured();
575
576            if event_store_replay_configured && !self.config.load_state() {
577                log::error!("Event-store replay requires load_state=true");
578                return;
579            }
580
581            if self.config.load_state()
582                && let Err(e) =
583                    event_store.restore_parent_cache(self.instance_id, &mut self.cache.borrow_mut())
584            {
585                log::error!("Failed to restore cache from event-store replay source: {e}");
586                return;
587            }
588
589            if let Err(e) = event_store.open(self.instance_id, &components, environment) {
590                log::error!("Failed to open event-store run: {e}");
591                return;
592            }
593
594            let anchorer = event_store.snapshot_anchorer();
595            self.exec_engine
596                .borrow_mut()
597                .set_snapshot_anchorer(anchorer);
598            self.event_store_replay = event_store_replay_configured;
599        }
600
601        if self.event_store_replay {
602            log::info!(
603                "Event-store replay loaded; skipping engines, clients, trader startup, and live reconciliation",
604            );
605            self.ts_started = Some(self.clock.borrow().timestamp_ns());
606            log::info!("Started");
607            return;
608        }
609
610        self.start_engines();
611
612        log::info!("Initializing trader");
613        if let Err(e) = self.trader.borrow_mut().initialize() {
614            log::error!("Error initializing trader: {e:?}");
615            return;
616        }
617
618        // Execution and data clients are started by their engines via `start_engines` above
619
620        self.ts_started = Some(self.clock.borrow().timestamp_ns());
621        log::info!("Started");
622    }
623
624    fn collect_registered_components(trader: &Rc<RefCell<Trader>>) -> RegisteredComponents {
625        let trader = trader.borrow();
626        let mut components = RegisteredComponents::default();
627        for actor_id in trader.actor_ids() {
628            components
629                .actors
630                .insert(actor_id.to_string(), String::new());
631        }
632
633        for strategy_id in trader.strategy_ids() {
634            components
635                .strategies
636                .insert(strategy_id.to_string(), String::new());
637        }
638
639        for algo_id in trader.exec_algorithm_ids() {
640            components
641                .algorithms
642                .insert(algo_id.to_string(), String::new());
643        }
644        components
645    }
646
647    /// Starts the Nautilus system kernel asynchronously.
648    #[expect(
649        clippy::unused_async,
650        reason = "keeps the public async kernel API shape stable"
651    )]
652    pub async fn start_async(&mut self) {
653        self.start();
654    }
655
656    /// Starts the trader (strategies and actors).
657    ///
658    /// This should be called after clients are connected and instruments are cached.
659    pub fn start_trader(&mut self) {
660        log::info!("Starting trader...");
661        self.order_emulator.start();
662        if let Err(e) = self.trader.borrow_mut().start() {
663            log::error!("Error starting trader: {e:?}");
664        }
665        log::info!("Trader started");
666    }
667
668    /// Stops the trader and its registered components.
669    ///
670    /// This method initiates a graceful shutdown of trading components (strategies, actors)
671    /// which may trigger residual events such as order cancellations. The caller should
672    /// continue processing events after calling this method to handle these residual events.
673    pub fn stop_trader(&mut self) {
674        disarm_shutdown_on_error();
675
676        if !self.trader.borrow().is_running() {
677            return;
678        }
679
680        log::info!("Stopping trader...");
681
682        if let Err(e) = self.trader.borrow_mut().stop() {
683            log::error!("Error stopping trader: {e}");
684        }
685    }
686
687    /// Finalizes the kernel shutdown after the grace period.
688    ///
689    /// This method should be called after the residual events grace period has elapsed
690    /// and all remaining events have been processed. It disconnects clients and stops engines.
691    #[expect(
692        clippy::unused_async,
693        reason = "keeps the public async kernel API shape stable"
694    )]
695    pub async fn finalize_stop(&mut self) {
696        disarm_shutdown_on_error();
697
698        // Execution and data clients are stopped by their engines via `stop_engines` below
699
700        self.stop_engines();
701        self.cancel_timers();
702
703        let ts_shutdown = self.clock.borrow().timestamp_ns();
704
705        if let Some(event_store) = self.event_store.as_deref_mut() {
706            self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
707            event_store.seal(ts_shutdown);
708        }
709        self.ts_shutdown = Some(ts_shutdown);
710        log::info!("Stopped");
711    }
712
713    /// Returns the kernel-managed event-store integration, when one was injected.
714    ///
715    /// Callers wire an implementation through
716    /// [`NautilusKernelBuilder::with_event_store`](crate::builder::NautilusKernelBuilder::with_event_store);
717    /// without an injected adapter this returns `None`.
718    #[must_use]
719    pub fn event_store(&self) -> Option<&dyn KernelEventStore> {
720        self.event_store.as_deref()
721    }
722
723    /// Returns whether the event-store integration is running an event-store replay start.
724    #[must_use]
725    pub fn is_event_store_replay(&self) -> bool {
726        self.event_store_replay
727    }
728
729    /// Returns whether the event-store integration is configured for an event-store replay start.
730    #[must_use]
731    pub fn is_event_store_replay_configured(&self) -> bool {
732        self.event_store
733            .as_deref()
734            .is_some_and(KernelEventStore::is_event_store_replay_configured)
735    }
736
737    /// Resets the Nautilus system kernel to its initial state.
738    pub fn reset(&mut self) {
739        disarm_shutdown_on_error();
740        log::info!("Resetting");
741
742        if let Err(e) = self.trader.borrow_mut().reset() {
743            log::error!("Error resetting trader: {e:?}");
744        }
745
746        self.data_engine.borrow_mut().reset();
747        self.exec_engine.borrow_mut().reset();
748        self.risk_engine.borrow_mut().reset();
749        self.order_emulator.reset();
750        self.portfolio.borrow_mut().reset();
751
752        self.ts_started = None;
753        self.ts_shutdown = None;
754
755        log::info!("Reset");
756    }
757
758    /// Disposes of the Nautilus system kernel, releasing resources.
759    pub fn dispose(&mut self) {
760        disarm_shutdown_on_error();
761        log::info!("Disposing");
762
763        if let Err(e) = self.trader.borrow_mut().dispose() {
764            log::error!("Error disposing trader: {e:?}");
765        }
766
767        self.stop_engines();
768        self.portfolio.borrow_mut().reset();
769        self.cancel_timers();
770
771        // BacktestEngine::end() does not call finalize_stop, so dispose() seals the
772        // run for non-streaming backtests. finalize_stop (live) consumes the session
773        // first; this call is then a no-op. Callers that skip dispose entirely fall
774        // back to the event-store implementation's Drop.
775        if let Some(event_store) = self.event_store.as_deref_mut() {
776            self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
777            let ts_dispose = self.clock.borrow().timestamp_ns();
778            event_store.seal(ts_dispose);
779        }
780
781        self.data_engine.borrow_mut().dispose();
782        self.exec_engine.borrow_mut().dispose();
783        self.risk_engine.borrow_mut().dispose();
784        self.order_emulator.dispose();
785        self.cache.borrow_mut().dispose();
786        get_message_bus().borrow_mut().dispose();
787
788        log::info!("Disposed");
789    }
790
791    /// Starts all engine components.
792    fn start_engines(&self) {
793        self.data_engine.borrow_mut().start();
794        self.exec_engine.borrow_mut().start();
795        self.risk_engine.borrow_mut().start();
796    }
797
798    /// Stops all engine components.
799    fn stop_engines(&self) {
800        self.data_engine.borrow_mut().stop();
801        self.exec_engine.borrow_mut().stop();
802        self.risk_engine.borrow_mut().stop();
803        self.order_emulator.stop();
804    }
805
806    /// Connects data engine clients.
807    ///
808    /// Data clients are connected first so that instruments are published
809    /// and can be drained into the cache before execution clients connect.
810    #[expect(clippy::await_holding_refcell_ref)] // Single-threaded runtime, intentional design
811    pub async fn connect_data_clients(&mut self) {
812        log::info!("Connecting data clients...");
813        self.data_engine.borrow_mut().connect().await;
814    }
815
816    /// Connects execution engine clients.
817    ///
818    /// Must be called after data clients are connected and instrument events
819    /// have been drained into the cache, so execution clients can load instruments.
820    #[expect(clippy::await_holding_refcell_ref)] // Single-threaded runtime, intentional design
821    pub async fn connect_exec_clients(&mut self) {
822        log::info!("Connecting execution clients...");
823        self.exec_engine.borrow_mut().connect().await;
824    }
825
826    /// Disconnects all engine clients.
827    ///
828    /// # Errors
829    ///
830    /// Returns an error if any client fails to disconnect.
831    #[expect(clippy::await_holding_refcell_ref)] // Single-threaded runtime, intentional design
832    pub async fn disconnect_clients(&mut self) -> anyhow::Result<()> {
833        log::info!("Disconnecting clients...");
834        self.data_engine.borrow_mut().disconnect().await?;
835        self.exec_engine.borrow_mut().disconnect().await?;
836        Ok(())
837    }
838
839    /// Returns `true` if all engine clients are connected.
840    #[must_use]
841    pub fn check_engines_connected(&self) -> bool {
842        self.data_engine.borrow().check_connected() && self.exec_engine.borrow().check_connected()
843    }
844
845    /// Returns `true` if all engine clients are disconnected.
846    #[must_use]
847    pub fn check_engines_disconnected(&self) -> bool {
848        self.data_engine.borrow().check_disconnected()
849            && self.exec_engine.borrow().check_disconnected()
850    }
851
852    /// Returns connection status for all data clients.
853    #[must_use]
854    pub fn data_client_connection_status(&self) -> Vec<(ClientId, bool)> {
855        self.data_engine.borrow().client_connection_status()
856    }
857
858    /// Returns connection status for all execution clients.
859    #[must_use]
860    pub fn exec_client_connection_status(&self) -> Vec<(ClientId, bool)> {
861        self.exec_engine.borrow().client_connection_status()
862    }
863}
864
865#[cfg(all(test, feature = "python"))]
866mod tests {
867    use nautilus_common::messages::system::ShutdownSystem;
868    use nautilus_core::UUID4;
869    use rstest::*;
870    use ustr::Ustr;
871
872    use super::*;
873    use crate::builder::NautilusKernelBuilder;
874
875    #[rstest]
876    fn test_shutdown_system_sets_kernel_flag() {
877        let kernel = NautilusKernelBuilder::default().build().unwrap();
878        assert!(!kernel.is_shutdown_requested());
879
880        let command = ShutdownSystem::new(
881            kernel.trader_id(),
882            Ustr::from("TestComponent"),
883            Some("unit test".to_string()),
884            UUID4::new(),
885            kernel.generate_timestamp_ns(),
886            None, // correlation_id
887        );
888
889        msgbus::publish_any(
890            MessagingSwitchboard::shutdown_system_topic(),
891            command.as_any(),
892        );
893        assert!(kernel.is_shutdown_requested());
894
895        kernel.reset_shutdown_flag();
896        assert!(!kernel.is_shutdown_requested());
897    }
898
899    #[rstest]
900    fn test_shutdown_system_idempotent() {
901        let kernel = NautilusKernelBuilder::default().build().unwrap();
902
903        let make_cmd = || {
904            ShutdownSystem::new(
905                kernel.trader_id(),
906                Ustr::from("TestComponent"),
907                None,
908                UUID4::new(),
909                kernel.generate_timestamp_ns(),
910                None, // correlation_id
911            )
912        };
913
914        let topic = MessagingSwitchboard::shutdown_system_topic();
915        msgbus::publish_any(topic, make_cmd().as_any());
916        assert!(kernel.is_shutdown_requested());
917
918        msgbus::publish_any(topic, make_cmd().as_any());
919        assert!(kernel.is_shutdown_requested());
920
921        kernel.reset_shutdown_flag();
922        assert!(!kernel.is_shutdown_requested());
923
924        msgbus::publish_any(topic, make_cmd().as_any());
925        assert!(kernel.is_shutdown_requested());
926    }
927
928    #[rstest]
929    fn test_shutdown_system_ignores_other_trader() {
930        let kernel = NautilusKernelBuilder::default().build().unwrap();
931
932        let command = ShutdownSystem::new(
933            TraderId::from("OTHER-TRADER"),
934            Ustr::from("TestComponent"),
935            None,
936            UUID4::new(),
937            kernel.generate_timestamp_ns(),
938            None, // correlation_id
939        );
940
941        msgbus::publish_any(
942            MessagingSwitchboard::shutdown_system_topic(),
943            command.as_any(),
944        );
945        assert!(!kernel.is_shutdown_requested());
946    }
947}
948
949#[cfg(test)]
950mod lifecycle_tests {
951    use nautilus_common::{
952        messages::data::{DataCommand, SubscribeCommand, UnsubscribeCommand},
953        msgbus::stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
954    };
955    use nautilus_model::{
956        enums::{OrderSide, OrderStatus, OrderType, TriggerType},
957        identifiers::ClientOrderId,
958        instruments::{
959            CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
960        },
961        orders::{Order, OrderAny, OrderTestBuilder},
962        types::{Price, Quantity},
963    };
964    use rstest::rstest;
965    use ustr::Ustr;
966
967    use super::*;
968    use crate::builder::NautilusKernelBuilder;
969
970    fn create_stop_market_order(instrument: &CryptoPerpetual, client_order_id: &str) -> OrderAny {
971        OrderTestBuilder::new(OrderType::StopMarket)
972            .instrument_id(instrument.id())
973            .client_order_id(ClientOrderId::from(client_order_id))
974            .side(OrderSide::Buy)
975            .trigger_price(Price::from("5100.00"))
976            .quantity(Quantity::from(1))
977            .emulation_trigger(TriggerType::BidAsk)
978            .build()
979    }
980
981    fn register_data_command_handler(id: &str) -> TypedIntoMessageSavingHandler<DataCommand> {
982        let (handler, saving_handler) =
983            get_typed_into_message_saving_handler::<DataCommand>(Some(Ustr::from(id)));
984        msgbus::register_data_command_endpoint(
985            MessagingSwitchboard::data_engine_queue_execute(),
986            handler,
987        );
988        saving_handler
989    }
990
991    #[rstest]
992    fn test_start_trader_starts_order_emulator_for_cached_emulated_orders() {
993        let mut kernel = NautilusKernelBuilder::default().build().unwrap();
994        let data_commands = register_data_command_handler("DataEngine.queue_execute.kernel_start");
995        let instrument = crypto_perpetual_ethusdt();
996        let instrument_id = instrument.id();
997        let first_order = create_stop_market_order(&instrument, "O-KERNEL-001");
998        let second_order = create_stop_market_order(&instrument, "O-KERNEL-002");
999        let first_client_order_id = first_order.client_order_id();
1000        let second_client_order_id = second_order.client_order_id();
1001        kernel
1002            .cache
1003            .borrow_mut()
1004            .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
1005            .unwrap();
1006        kernel
1007            .cache
1008            .borrow_mut()
1009            .add_order(first_order, None, None, false)
1010            .unwrap();
1011        kernel
1012            .cache
1013            .borrow_mut()
1014            .add_order(second_order, None, None, false)
1015            .unwrap();
1016
1017        kernel.start();
1018        assert!(
1019            kernel
1020                .order_emulator
1021                .get_emulator()
1022                .get_matching_core(&instrument_id)
1023                .is_none()
1024        );
1025        kernel.start_trader();
1026
1027        let commands = data_commands.get_messages();
1028        let cache = kernel.cache.borrow();
1029        let first_status = cache.order(&first_client_order_id).unwrap().status();
1030        let second_status = cache.order(&second_client_order_id).unwrap().status();
1031        drop(cache);
1032        let emulator = kernel.order_emulator.get_emulator();
1033        assert!(emulator.get_matching_core(&instrument_id).is_some());
1034        assert_eq!(emulator.subscribed_quotes(), vec![instrument_id]);
1035        assert_eq!(first_status, OrderStatus::Emulated);
1036        assert_eq!(second_status, OrderStatus::Emulated);
1037        assert!(commands.iter().any(|command| matches!(
1038            command,
1039            DataCommand::Subscribe(SubscribeCommand::Quotes(command))
1040                if command.instrument_id == instrument_id
1041        )));
1042
1043        data_commands.clear();
1044        drop(emulator);
1045        kernel.stop_trader();
1046        kernel.dispose();
1047
1048        let commands = data_commands.get_messages();
1049        let emulator = kernel.order_emulator.get_emulator();
1050        assert!(emulator.subscribed_quotes().is_empty());
1051        assert!(emulator.get_matching_core(&instrument_id).is_none());
1052        assert!(commands.iter().any(|command| matches!(
1053            command,
1054            DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
1055                if command.instrument_id == instrument_id
1056        )));
1057    }
1058
1059    #[rstest]
1060    fn test_reset_resets_order_emulator_state() {
1061        let mut kernel = NautilusKernelBuilder::default().build().unwrap();
1062        let data_commands = register_data_command_handler("DataEngine.queue_execute.kernel_reset");
1063        let instrument = crypto_perpetual_ethusdt();
1064        let instrument_id = instrument.id();
1065        let order = create_stop_market_order(&instrument, "O-KERNEL-RESET-001");
1066        kernel
1067            .cache
1068            .borrow_mut()
1069            .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
1070            .unwrap();
1071        kernel
1072            .cache
1073            .borrow_mut()
1074            .add_order(order, None, None, false)
1075            .unwrap();
1076
1077        kernel.start();
1078        kernel.start_trader();
1079        assert!(
1080            kernel
1081                .order_emulator
1082                .get_emulator()
1083                .get_matching_core(&instrument_id)
1084                .is_some()
1085        );
1086        kernel.stop_trader();
1087        data_commands.clear();
1088
1089        kernel.reset();
1090
1091        let commands = data_commands.get_messages();
1092        let emulator = kernel.order_emulator.get_emulator();
1093        assert!(emulator.subscribed_quotes().is_empty());
1094        assert!(emulator.get_matching_core(&instrument_id).is_none());
1095        assert!(commands.iter().any(|command| matches!(
1096            command,
1097            DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
1098                if command.instrument_id == instrument_id
1099        )));
1100
1101        drop(emulator);
1102        kernel.dispose();
1103    }
1104}