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