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