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
46#[cfg(feature = "streaming")]
47use anyhow::Context;
48#[cfg(feature = "streaming")]
49use jiff::tz::TimeZone;
50use nautilus_common::{
51 cache::{Cache, CacheConfig, database::CacheDatabaseAdapter},
52 clock::Clock,
53 component::Component,
54 enums::{ComponentState, Environment},
55 logging::{
56 arm_shutdown_on_error, disarm_shutdown_on_error, headers, init_logging,
57 logger::{LogGuard, LoggerConfig},
58 try_drain_shutdown_on_error_trigger,
59 },
60 messages::system::ShutdownSystem,
61 msgbus::{
62 self, MessageBus, MessagingSwitchboard, ShareableMessageHandler, get_message_bus,
63 set_message_bus,
64 },
65};
66use nautilus_core::{UUID4, UnixNanos};
67use nautilus_data::engine::DataEngine;
68use nautilus_execution::{
69 engine::ExecutionEngine,
70 order_emulator::{adapter::OrderEmulatorAdapter, emulator::OrderEmulator},
71};
72use nautilus_model::identifiers::{ClientId, TraderId};
73#[cfg(feature = "streaming")]
74use nautilus_persistence::{
75 backend::default_writer_factories,
76 catalog::traits::NautilusDataTypePrefix,
77 config::StreamingConfig,
78 writer::{
79 factory::{WriterConnectConfig, create_writer, replace_existing_writer_data},
80 feather::{RotationConfig as WriterRotationConfig, WriterClock},
81 filter::WriterRecordFilter,
82 subscription::StreamingSinkSubscription,
83 traits::StreamingDataSink,
84 },
85};
86use nautilus_portfolio::portfolio::Portfolio;
87use nautilus_risk::engine::RiskEngine;
88use ustr::Ustr;
89
90use crate::{
91 builder::NautilusKernelBuilder,
92 clock_factory::ClockFactory,
93 config::NautilusKernelConfig,
94 event_store::{EventStoreFactory, KernelEventStore, RegisteredComponents},
95 trader::Trader,
96};
97
98pub struct NautilusKernel {
102 pub name: String,
104 pub instance_id: UUID4,
106 pub machine_id: String,
108 pub config: Box<dyn NautilusKernelConfig>,
110 pub cache: Rc<RefCell<Cache>>,
112 pub clock: Rc<RefCell<dyn Clock>>,
114 pub portfolio: Rc<RefCell<Portfolio>>,
116 pub log_guard: LogGuard,
118 pub data_engine: Rc<RefCell<DataEngine>>,
120 pub risk_engine: Rc<RefCell<RiskEngine>>,
122 pub exec_engine: Rc<RefCell<ExecutionEngine>>,
124 pub order_emulator: OrderEmulatorAdapter,
126 pub trader: Rc<RefCell<Trader>>,
128 pub ts_created: UnixNanos,
130 pub ts_started: Option<UnixNanos>,
132 pub ts_shutdown: Option<UnixNanos>,
134 shutdown_requested: Rc<Cell<bool>>,
135 event_store: Option<Box<dyn KernelEventStore>>,
136 event_store_replay: bool,
137 state_save_armed: bool,
138 #[cfg(feature = "streaming")]
139 streaming_writer: Option<Rc<RefCell<StreamingDataSink>>>,
140 #[cfg(feature = "streaming")]
141 streaming_subscriptions: Option<StreamingSinkSubscription>,
142}
143
144impl Debug for NautilusKernel {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 f.debug_struct(stringify!(NautilusKernel))
147 .field("name", &self.name)
148 .field("instance_id", &self.instance_id)
149 .field("machine_id", &self.machine_id)
150 .field("environment", &self.config.environment())
151 .finish_non_exhaustive()
152 }
153}
154
155#[derive(Default)]
157pub struct NautilusKernelDependencies {
158 clock_factory: Option<ClockFactory>,
159 cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
160 event_store_factory: Option<EventStoreFactory>,
161}
162
163impl Debug for NautilusKernelDependencies {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 f.debug_struct(stringify!(NautilusKernelDependencies))
166 .field("clock_factory", &self.clock_factory.is_some())
167 .field("cache_database", &self.cache_database.is_some())
168 .field("event_store_factory", &self.event_store_factory.is_some())
169 .finish()
170 }
171}
172
173impl NautilusKernelDependencies {
174 #[must_use]
176 pub fn with_clock_factory(mut self, clock_factory: Option<ClockFactory>) -> Self {
177 self.clock_factory = clock_factory;
178 self
179 }
180
181 #[must_use]
183 pub fn with_cache_database(
184 mut self,
185 cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
186 ) -> Self {
187 self.cache_database = cache_database;
188 self
189 }
190
191 #[must_use]
193 pub fn with_event_store_factory(
194 mut self,
195 event_store_factory: Option<EventStoreFactory>,
196 ) -> Self {
197 self.event_store_factory = event_store_factory;
198 self
199 }
200}
201
202impl NautilusKernel {
203 #[cfg(feature = "streaming")]
204 fn writer_record_filter(config: &StreamingConfig) -> Option<WriterRecordFilter> {
205 let mut filter = config
206 .record_types
207 .clone()
208 .map(WriterRecordFilter::from_record_types)
209 .unwrap_or_default();
210
211 if let Some(data_types) = &config.data_types {
212 for data_type in data_types {
213 filter.insert_prefix(data_type.path_prefix().into_owned(), None);
214 }
215 }
216
217 if let Some(instrument_types) = &config.instrument_types {
218 for instrument_type in instrument_types {
219 filter.insert_instrument_type(instrument_type);
220 }
221 }
222
223 if let Some(entries) = &config.record_filters {
224 for entry in entries {
225 filter.insert(&entry.record_type, entry.identifiers.clone());
226 }
227 }
228
229 (!filter.is_empty()).then_some(filter)
230 }
231
232 #[must_use]
234 pub const fn builder(
235 name: String,
236 trader_id: TraderId,
237 environment: Environment,
238 ) -> NautilusKernelBuilder {
239 NautilusKernelBuilder::new(name, trader_id, environment)
240 }
241
242 pub fn new<T: NautilusKernelConfig + 'static>(name: String, config: T) -> anyhow::Result<Self> {
248 Self::new_with(name, config, None, None)
249 }
250
251 pub fn new_with_cache_database<T: NautilusKernelConfig + 'static>(
262 name: String,
263 config: T,
264 cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
265 ) -> anyhow::Result<Self> {
266 Self::new_with(name, config, cache_database, None)
267 }
268
269 pub fn new_with<T: NautilusKernelConfig + 'static>(
281 name: String,
282 config: T,
283 cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
284 event_store_factory: Option<EventStoreFactory>,
285 ) -> anyhow::Result<Self> {
286 Self::new_with_dependencies(
287 name,
288 config,
289 NautilusKernelDependencies::default()
290 .with_cache_database(cache_database)
291 .with_event_store_factory(event_store_factory),
292 )
293 }
294
295 #[expect(
301 clippy::too_many_lines,
302 reason = "kernel construction keeps initialization order and ownership visible in one place"
303 )]
304 pub fn new_with_dependencies<T: NautilusKernelConfig + 'static>(
305 name: String,
306 config: T,
307 dependencies: NautilusKernelDependencies,
308 ) -> anyhow::Result<Self> {
309 let NautilusKernelDependencies {
310 clock_factory,
311 cache_database,
312 event_store_factory,
313 } = dependencies;
314 let instance_id = config.instance_id().unwrap_or_default();
315 let machine_id = Self::determine_machine_id()?;
316
317 let logger_config = config.logging();
318 let log_guard = Self::initialize_logging(config.trader_id(), instance_id, logger_config)?;
319 headers::log_header(
320 config.trader_id(),
321 &machine_id,
322 instance_id,
323 Ustr::from(&name),
324 );
325
326 log::info!("Building system kernel");
327
328 let clock_factory =
329 clock_factory.unwrap_or_else(|| ClockFactory::for_environment(config.environment()));
330 let clock = clock_factory.clock();
331 let event_store = match event_store_factory {
332 Some(factory) => Some(factory(instance_id, clock.clone())?),
333 None => None,
334 };
335 let cache = Self::initialize_cache(config.cache(), cache_database);
336
337 let msgbus = Rc::new(RefCell::new(MessageBus::new(
338 config.trader_id(),
339 instance_id,
340 Some(name.clone()),
341 None,
342 )));
343 set_message_bus(msgbus);
344
345 if let Some(config) = config.msgbus()
346 && let Some(filter) = config.types_filter
347 {
348 get_message_bus().borrow_mut().set_types_filter(filter);
349 }
350
351 let portfolio = Rc::new(RefCell::new(Portfolio::new(
352 clock.clone(),
353 cache.clone(),
354 config.portfolio(),
355 )));
356
357 let risk_engine = RiskEngine::new(
358 config.risk_engine().unwrap_or_default(),
359 portfolio.borrow().clone_shallow(),
360 clock.clone(),
361 cache.clone(),
362 );
363 let risk_engine = Rc::new(RefCell::new(risk_engine));
364
365 let exec_engine = ExecutionEngine::new(clock.clone(), cache.clone(), config.exec_engine());
366 let exec_engine = Rc::new(RefCell::new(exec_engine));
367
368 let order_emulator = OrderEmulatorAdapter::new(clock.clone(), cache.clone());
369
370 let data_engine = DataEngine::new(clock.clone(), cache.clone(), config.data_engine());
371 #[cfg(feature = "streaming")]
372 let mut data_engine = data_engine;
373 #[cfg(feature = "streaming")]
374 {
375 let mut unnamed_index = 0;
376 let mut catalog_names = HashSet::new();
377
378 for catalog_config in config.catalogs() {
379 let name = catalog_config.name().map_or_else(
380 || {
381 let name = format!("catalog_{unnamed_index}");
382 unnamed_index += 1;
383 name
384 },
385 str::to_owned,
386 );
387 anyhow::ensure!(
388 catalog_names.insert(name.clone()),
389 "Duplicate data catalog name '{name}'",
390 );
391 let catalog = catalog_config.create_catalog().with_context(|| {
392 format!(
393 "Failed to create data catalog from '{}'",
394 catalog_config.path()
395 )
396 })?;
397 data_engine.register_catalog(catalog, Some(&name));
398 }
399 }
400 let data_engine = Rc::new(RefCell::new(data_engine));
401
402 DataEngine::register_msgbus_handlers(&data_engine);
403 RiskEngine::register_msgbus_handlers(&risk_engine);
404 ExecutionEngine::register_msgbus_handlers(&exec_engine);
405 OrderEmulator::register_msgbus_handlers(&order_emulator.emulator());
406
407 let shutdown_requested = Rc::new(Cell::new(false));
408 Self::register_shutdown_handler(config.trader_id(), shutdown_requested.clone());
409
410 let trader = Rc::new(RefCell::new(Trader::new(
411 config.trader_id(),
412 instance_id,
413 config.environment(),
414 clock_factory,
415 cache.clone(),
416 portfolio.clone(),
417 )));
418
419 let ts_created = clock.borrow().timestamp_ns();
420
421 #[cfg(feature = "streaming")]
422 let (streaming_writer, streaming_subscriptions) = match config.streaming() {
423 Some(streaming_config) => {
424 let environment = config.environment().to_string().to_ascii_lowercase();
425 let base_uri = match streaming_config.fs_protocol.as_str() {
426 "file" => streaming_config
427 .catalog_path
428 .trim_end_matches('/')
429 .to_string(),
430 _ if streaming_config.catalog_path.contains("://") => streaming_config
431 .catalog_path
432 .trim_end_matches('/')
433 .to_string(),
434 protocol => format!(
435 "{protocol}://{}",
436 streaming_config.catalog_path.trim_end_matches('/'),
437 ),
438 };
439 let uri = format!("{base_uri}/{environment}/{instance_id}");
440 let rotation_config = writer_rotation_config(&streaming_config.rotation_config);
441 let mut connect = WriterConnectConfig::new(&uri, None);
442 connect.rotation_config = rotation_config;
443 connect.flush_interval_ms = Some(streaming_config.flush_interval_ms);
444 connect.params.clone_from(&streaming_config.params);
445 connect.record_filter = Self::writer_record_filter(&streaming_config);
446 if streaming_config.replace_existing {
447 replace_existing_writer_data(&connect)?;
448 }
449 let (writer_clock, bridge) = WriterClock::from_shared_clock(&clock);
450 let writer = Rc::new(RefCell::new(create_writer(
451 &streaming_config.writer_backend,
452 &connect,
453 writer_clock,
454 &default_writer_factories(),
455 )?));
456 let handler = StreamingSinkSubscription::subscribe_named(
457 writer.clone(),
458 bridge.map(|atomic| (Rc::clone(&clock), atomic)),
459 uri.clone(),
460 );
461 log::info!("Writing data and events to {uri}");
462 (Some(writer), Some(handler))
463 }
464 None => (None, None),
465 };
466
467 Ok(Self {
468 name,
469 instance_id,
470 machine_id,
471 event_store,
472 config: Box::new(config),
473 cache,
474 clock,
475 portfolio,
476 log_guard,
477 data_engine,
478 risk_engine,
479 exec_engine,
480 order_emulator,
481 trader,
482 ts_created,
483 ts_started: None,
484 ts_shutdown: None,
485 shutdown_requested,
486 event_store_replay: false,
487 state_save_armed: false,
488 #[cfg(feature = "streaming")]
489 streaming_writer,
490 #[cfg(feature = "streaming")]
491 streaming_subscriptions,
492 })
493 }
494
495 fn register_shutdown_handler(trader_id: TraderId, shutdown_requested: Rc<Cell<bool>>) {
496 let handler = ShareableMessageHandler::from_typed(move |cmd: &ShutdownSystem| {
497 if cmd.trader_id != trader_id {
498 log::warn!("Received {cmd} not for this trader {trader_id}, ignoring");
499 return;
500 }
501
502 if shutdown_requested.get() {
503 log::debug!("Shutdown already requested, ignoring {cmd}");
504 return;
505 }
506
507 log::info!("Received {cmd}, requesting shutdown");
508 shutdown_requested.set(true);
509 });
510 let topic = MessagingSwitchboard::shutdown_system_topic();
511 msgbus::subscribe_any(topic.into(), handler, None);
512 }
513
514 fn determine_machine_id() -> anyhow::Result<String> {
515 sysinfo::System::host_name().ok_or_else(|| anyhow::anyhow!("Failed to determine hostname"))
516 }
517
518 fn initialize_logging(
519 trader_id: TraderId,
520 instance_id: UUID4,
521 config: LoggerConfig,
522 ) -> anyhow::Result<LogGuard> {
523 #[cfg(feature = "tracing-bridge")]
524 let use_tracing = config.use_tracing;
525
526 let file_config = config.file_config.clone().unwrap_or_default();
527 let log_guard = match init_logging(trader_id, instance_id, config, file_config) {
528 Ok(guard) => guard,
529 Err(e) => {
530 if e.downcast_ref::<log::SetLoggerError>().is_some() {
535 if let Some(guard) = LogGuard::new() {
536 guard
537 } else {
538 return Err(e.context(
539 "A non-Nautilus logger is already registered; \
540 cannot initialize Nautilus logging",
541 ));
542 }
543 } else {
544 return Err(e);
545 }
546 }
547 };
548
549 #[cfg(feature = "tracing-bridge")]
551 if use_tracing && !nautilus_common::logging::bridge::tracing_is_initialized() {
552 nautilus_common::logging::bridge::init_tracing()?;
553 }
554
555 Ok(log_guard)
556 }
557
558 fn initialize_cache(
559 cache_config: Option<CacheConfig>,
560 cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
561 ) -> Rc<RefCell<Cache>> {
562 let cache_config = cache_config.unwrap_or_default();
563 let cache = Cache::new(Some(cache_config), cache_database);
564
565 Rc::new(RefCell::new(cache))
566 }
567
568 fn cancel_timers(&self) {
569 self.clock.borrow_mut().cancel_timers();
570 }
571
572 #[must_use]
573 pub fn generate_timestamp_ns(&self) -> UnixNanos {
574 self.clock.borrow().timestamp_ns()
575 }
576
577 #[must_use]
579 pub fn environment(&self) -> Environment {
580 self.config.environment()
581 }
582
583 #[must_use]
585 pub const fn name(&self) -> &str {
586 self.name.as_str()
587 }
588
589 #[must_use]
591 pub fn trader_id(&self) -> TraderId {
592 self.config.trader_id()
593 }
594
595 #[must_use]
597 pub fn machine_id(&self) -> &str {
598 &self.machine_id
599 }
600
601 #[must_use]
603 pub const fn instance_id(&self) -> UUID4 {
604 self.instance_id
605 }
606
607 #[must_use]
609 pub fn delay_post_stop(&self) -> Duration {
610 self.config.delay_post_stop()
611 }
612
613 #[must_use]
615 pub const fn ts_created(&self) -> UnixNanos {
616 self.ts_created
617 }
618
619 #[must_use]
621 pub const fn ts_started(&self) -> Option<UnixNanos> {
622 self.ts_started
623 }
624
625 #[must_use]
627 pub const fn ts_shutdown(&self) -> Option<UnixNanos> {
628 self.ts_shutdown
629 }
630
631 #[must_use]
635 pub fn is_shutdown_requested(&self) -> bool {
636 self.drain_shutdown_on_error_trigger();
637 self.shutdown_requested.get()
638 }
639
640 pub fn reset_shutdown_flag(&self) {
645 self.shutdown_requested.set(false);
646 }
647
648 #[must_use]
651 pub fn shutdown_flag(&self) -> Rc<Cell<bool>> {
652 self.shutdown_requested.clone()
653 }
654
655 fn drain_shutdown_on_error_trigger(&self) {
656 try_drain_shutdown_on_error_trigger(|trigger| {
657 let command = ShutdownSystem::new(
658 self.config.trader_id(),
659 trigger.component,
660 Some(format!(
661 "Error log received from {}: {}",
662 trigger.component, trigger.message
663 )),
664 UUID4::new(),
665 trigger.timestamp,
666 None,
667 );
668
669 msgbus::try_publish_any(
670 MessagingSwitchboard::shutdown_system_topic(),
671 command.as_any(),
672 )
673 });
674 }
675
676 #[must_use]
678 pub fn load_state(&self) -> bool {
679 self.config.load_state()
680 }
681
682 #[must_use]
684 pub fn save_state(&self) -> bool {
685 self.config.save_state()
686 }
687
688 #[must_use]
690 pub fn clock(&self) -> Rc<RefCell<dyn Clock>> {
691 self.clock.clone()
692 }
693
694 #[must_use]
696 pub fn cache(&self) -> Rc<RefCell<Cache>> {
697 self.cache.clone()
698 }
699
700 #[must_use]
702 pub fn portfolio(&self) -> Ref<'_, Portfolio> {
703 self.portfolio.borrow()
704 }
705
706 #[must_use]
708 pub fn data_engine(&self) -> Ref<'_, DataEngine> {
709 self.data_engine.borrow()
710 }
711
712 #[must_use]
714 pub const fn risk_engine(&self) -> &Rc<RefCell<RiskEngine>> {
715 &self.risk_engine
716 }
717
718 #[must_use]
720 pub const fn exec_engine(&self) -> &Rc<RefCell<ExecutionEngine>> {
721 &self.exec_engine
722 }
723
724 #[must_use]
726 pub fn trader(&self) -> &Rc<RefCell<Trader>> {
727 &self.trader
728 }
729
730 pub fn start(&mut self) {
732 arm_shutdown_on_error(self.config.shutdown_on_error());
733 log::info!("Starting");
734
735 self.event_store_replay = false;
736
737 if let Some(event_store) = self.event_store.as_deref_mut() {
738 self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
739
740 let components = Self::collect_registered_components(&self.trader);
741 let environment = self.config.environment();
742 let event_store_replay_configured = event_store.is_event_store_replay_configured();
743
744 if event_store_replay_configured && !self.config.load_state() {
745 log::error!("Event-store replay requires load_state=true");
746 return;
747 }
748
749 if self.config.load_state()
750 && let Err(e) =
751 event_store.restore_parent_cache(self.instance_id, &mut self.cache.borrow_mut())
752 {
753 log::error!("Failed to restore cache from event-store replay source: {e}");
754 return;
755 }
756
757 if let Err(e) = event_store.open(self.instance_id, &components, environment) {
758 log::error!("Failed to open event-store run: {e}");
759 return;
760 }
761
762 let anchorer = event_store.snapshot_anchorer();
763 self.exec_engine
764 .borrow_mut()
765 .set_snapshot_anchorer(anchorer);
766 self.event_store_replay = event_store_replay_configured;
767 }
768
769 if self.event_store_replay {
770 log::info!(
771 "Event-store replay loaded; skipping engines, clients, trader startup, and live reconciliation",
772 );
773 self.ts_started = Some(self.clock.borrow().timestamp_ns());
774 log::info!("Started");
775 return;
776 }
777
778 self.start_engines();
779
780 log::info!("Initializing trader");
781 if let Err(e) = self.trader.borrow_mut().initialize() {
782 log::error!("Error initializing trader: {e:?}");
783 return;
784 }
785
786 self.ts_started = Some(self.clock.borrow().timestamp_ns());
789 log::info!("Started");
790 }
791
792 fn collect_registered_components(trader: &Rc<RefCell<Trader>>) -> RegisteredComponents {
793 let trader = trader.borrow();
794 let mut components = RegisteredComponents::default();
795 for actor_id in trader.actor_ids() {
796 components
797 .actors
798 .insert(actor_id.to_string(), String::new());
799 }
800
801 for strategy_id in trader.strategy_ids() {
802 components
803 .strategies
804 .insert(strategy_id.to_string(), String::new());
805 }
806
807 for algo_id in trader.exec_algorithm_ids() {
808 components
809 .algorithms
810 .insert(algo_id.to_string(), String::new());
811 }
812 components
813 }
814
815 #[expect(
817 clippy::unused_async,
818 reason = "keeps the public async kernel API shape stable"
819 )]
820 pub async fn start_async(&mut self) {
821 self.start();
822 }
823
824 pub fn start_trader(&mut self) -> anyhow::Result<()> {
833 log::info!("Starting trader...");
834
835 let load_state = self.config.load_state();
836 let save_state = self.config.save_state();
837
838 if (load_state || save_state) && !self.cache.borrow().has_backing() {
839 log::warn!(
840 "Cache has no database backing, load_state={load_state} and save_state={save_state} will have no effect"
841 );
842 }
843
844 if load_state {
845 Trader::load_state(&self.trader)
846 .map_err(|e| anyhow::anyhow!("Failed to load actor and strategy state: {e:#}"))?;
847 }
848
849 self.state_save_armed = save_state;
850 self.order_emulator.start();
851
852 if let Err(start_err) = Trader::start_with_component_callbacks(&self.trader) {
853 let stop_result = self.stop_trader_after_start_failure();
854 self.order_emulator.stop();
855 let save_result = self.save_trader_state();
856
857 let mut errors = vec![format!("Failed to start trader: {start_err}")];
858 if let Err(e) = stop_result {
859 errors.push(format!("failed to stop partial trader start: {e}"));
860 }
861
862 if let Err(e) = save_result {
863 errors.push(format!("failed to save partial trader state: {e}"));
864 }
865 anyhow::bail!("{}", errors.join("; "));
866 }
867
868 log::info!("Trader started");
869 Ok(())
870 }
871
872 pub fn stop_trader(&mut self) {
878 disarm_shutdown_on_error();
879
880 if !self.trader.borrow().is_running() {
881 return;
882 }
883
884 log::info!("Stopping trader...");
885
886 if let Err(e) = self.trader.borrow_mut().stop() {
887 log::error!("Error stopping trader: {e}");
888 }
889 }
890
891 pub fn stop_trader_after_start_failure(&mut self) -> anyhow::Result<()> {
897 disarm_shutdown_on_error();
898
899 if !matches!(
900 self.trader.borrow().state(),
901 ComponentState::Starting | ComponentState::Running
902 ) {
903 return Ok(());
904 }
905
906 log::info!("Stopping trader immediately...");
907 self.trader.borrow_mut().stop_after_start_failure()
908 }
909
910 #[allow(unknown_lints)]
919 #[expect(
920 clippy::unused_async,
921 clippy::unused_async_trait_impl,
922 reason = "keeps the public async kernel API shape stable"
923 )]
924 pub async fn finalize_stop(&mut self) -> anyhow::Result<()> {
925 disarm_shutdown_on_error();
926
927 let save_result = self.save_trader_state();
930 self.portfolio.borrow_mut().finalize_equity_curve();
931 self.stop_engines();
932 self.cancel_timers();
933
934 let ts_shutdown = self.clock.borrow().timestamp_ns();
935
936 if let Some(event_store) = self.event_store.as_deref_mut() {
937 self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
938 event_store.seal(ts_shutdown);
939 }
940 self.ts_shutdown = Some(ts_shutdown);
941 log::info!("Stopped");
942 save_result?;
943 self.flush_streaming()
944 }
945
946 pub fn save_trader_state(&mut self) -> anyhow::Result<()> {
952 if !std::mem::take(&mut self.state_save_armed) {
953 return Ok(());
954 }
955
956 Trader::save_state(&self.trader)
957 }
958
959 #[must_use]
965 pub fn event_store(&self) -> Option<&dyn KernelEventStore> {
966 self.event_store.as_deref()
967 }
968
969 #[must_use]
971 pub fn is_event_store_replay(&self) -> bool {
972 self.event_store_replay
973 }
974
975 #[must_use]
977 pub fn is_event_store_replay_configured(&self) -> bool {
978 self.event_store
979 .as_deref()
980 .is_some_and(KernelEventStore::is_event_store_replay_configured)
981 }
982
983 pub fn reset(&mut self) {
985 disarm_shutdown_on_error();
986 log::info!("Resetting");
987
988 if let Err(e) = self.trader.borrow_mut().reset() {
989 log::error!("Error resetting trader: {e:?}");
990 }
991
992 self.data_engine.borrow_mut().reset();
993 self.exec_engine.borrow_mut().reset();
994 self.risk_engine.borrow_mut().reset();
995 self.order_emulator.reset();
996 self.portfolio.borrow_mut().reset();
997
998 self.ts_started = None;
999 self.ts_shutdown = None;
1000 self.state_save_armed = false;
1001
1002 log::info!("Reset");
1003 }
1004
1005 pub fn dispose(&mut self) {
1007 disarm_shutdown_on_error();
1008 log::info!("Disposing");
1009
1010 let trader_state = self.trader.borrow().state();
1011 match trader_state {
1012 ComponentState::Running => self.stop_trader(),
1013 ComponentState::Starting => {
1014 if let Err(e) = self.stop_trader_after_start_failure() {
1015 log::error!("Error stopping partial trader start during disposal: {e:?}");
1016 }
1017 }
1018 _ => {}
1019 }
1020
1021 if let Err(e) = self.save_trader_state() {
1022 log::error!("Error saving trader state during disposal: {e:?}");
1023 }
1024
1025 {
1026 let mut trader = self.trader.borrow_mut();
1027 if trader.state() == ComponentState::PreInitialized
1028 && let Err(e) = trader.initialize()
1029 {
1030 log::error!("Error initializing trader for disposal: {e:?}");
1031 }
1032
1033 if !trader.is_disposed()
1034 && let Err(e) = trader.dispose()
1035 {
1036 log::error!("Error disposing trader: {e:?}");
1037 }
1038 }
1039
1040 self.stop_engines();
1041 self.portfolio.borrow_mut().reset();
1042 self.cancel_timers();
1043
1044 if let Some(event_store) = self.event_store.as_deref_mut() {
1049 self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
1050 let ts_dispose = self.clock.borrow().timestamp_ns();
1051 event_store.seal(ts_dispose);
1052 }
1053
1054 #[cfg(feature = "streaming")]
1055 {
1056 if let Some(subscriptions) = self.streaming_subscriptions.take()
1057 && let Err(e) = subscriptions.close()
1058 {
1059 log::warn!("Failed to close streaming writer: {e}");
1060 }
1061
1062 drop(self.streaming_writer.take());
1063 }
1064
1065 self.data_engine.borrow_mut().dispose();
1066 self.exec_engine.borrow_mut().dispose();
1067 self.risk_engine.borrow_mut().dispose();
1068 self.order_emulator.dispose();
1069 self.cache.borrow_mut().dispose();
1070 get_message_bus().borrow_mut().dispose();
1071
1072 log::info!("Disposed");
1073 }
1074
1075 pub fn flush_streaming(&mut self) -> anyhow::Result<()> {
1081 #[cfg(feature = "streaming")]
1082 if let Some(writer) = &self.streaming_writer {
1083 writer
1084 .borrow_mut()
1085 .flush()
1086 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
1087 }
1088 Ok(())
1089 }
1090
1091 fn start_engines(&self) {
1093 self.data_engine.borrow_mut().start();
1094 self.exec_engine.borrow_mut().start();
1095 self.risk_engine.borrow_mut().start();
1096 }
1097
1098 fn stop_engines(&self) {
1100 self.data_engine.borrow_mut().stop();
1101 self.exec_engine.borrow_mut().stop();
1102 self.risk_engine.borrow_mut().stop();
1103 self.order_emulator.stop();
1104 }
1105
1106 #[expect(clippy::await_holding_refcell_ref)] pub async fn connect_data_clients(&mut self) {
1112 log::info!("Connecting data clients...");
1113 self.data_engine.borrow_mut().connect().await;
1114 }
1115
1116 #[expect(clippy::await_holding_refcell_ref)] pub async fn connect_exec_clients(&mut self) {
1122 log::info!("Connecting execution clients...");
1123 self.exec_engine.borrow_mut().connect().await;
1124 }
1125
1126 #[expect(clippy::await_holding_refcell_ref)] pub async fn disconnect_clients(&mut self) -> anyhow::Result<()> {
1133 log::info!("Disconnecting clients...");
1134 let mut data_engine = self.data_engine.borrow_mut();
1135 let mut exec_engine = self.exec_engine.borrow_mut();
1136 let (data_result, exec_result) =
1137 futures::join!(data_engine.disconnect(), exec_engine.disconnect());
1138
1139 match (data_result, exec_result) {
1140 (Ok(()), Ok(())) => Ok(()),
1141 (Err(data_err), Ok(())) => Err(data_err),
1142 (Ok(()), Err(exec_err)) => Err(exec_err),
1143 (Err(data_err), Err(exec_err)) => anyhow::bail!(
1144 "Failed to disconnect data clients: {data_err}; failed to disconnect execution \
1145 clients: {exec_err}"
1146 ),
1147 }
1148 }
1149
1150 #[must_use]
1152 pub fn check_engines_connected(&self) -> bool {
1153 self.data_engine.borrow().check_connected() && self.exec_engine.borrow().check_connected()
1154 }
1155
1156 #[must_use]
1158 pub fn check_engines_disconnected(&self) -> bool {
1159 self.data_engine.borrow().check_disconnected()
1160 && self.exec_engine.borrow().check_disconnected()
1161 }
1162
1163 #[must_use]
1165 pub fn data_client_connection_status(&self) -> Vec<(ClientId, bool)> {
1166 self.data_engine.borrow().client_connection_status()
1167 }
1168
1169 #[must_use]
1171 pub fn exec_client_connection_status(&self) -> Vec<(ClientId, bool)> {
1172 self.exec_engine.borrow().client_connection_status()
1173 }
1174}
1175
1176#[cfg(feature = "streaming")]
1177fn writer_rotation_config(config: &crate::config::RotationConfig) -> WriterRotationConfig {
1178 match config {
1179 crate::config::RotationConfig::Size { max_size } => WriterRotationConfig::Size {
1180 max_size: *max_size,
1181 },
1182 crate::config::RotationConfig::Interval { interval_ns } => WriterRotationConfig::Interval {
1183 interval_ns: interval_ns.as_u64(),
1184 },
1185 crate::config::RotationConfig::ScheduledDates {
1186 interval_ns,
1187 schedule_ns,
1188 } => WriterRotationConfig::ScheduledDates {
1189 interval_ns: interval_ns.as_u64(),
1190 rotation_time: *schedule_ns,
1191 rotation_timezone: TimeZone::UTC,
1192 },
1193 crate::config::RotationConfig::NoRotation => WriterRotationConfig::NoRotation,
1194 }
1195}
1196
1197#[cfg(all(test, feature = "python"))]
1198mod tests {
1199 use nautilus_common::messages::system::ShutdownSystem;
1200 use nautilus_core::UUID4;
1201 use rstest::*;
1202 use ustr::Ustr;
1203
1204 use super::*;
1205 use crate::builder::NautilusKernelBuilder;
1206
1207 #[rstest]
1208 fn test_shutdown_system_sets_kernel_flag() {
1209 let kernel = NautilusKernelBuilder::default().build().unwrap();
1210 assert!(!kernel.is_shutdown_requested());
1211
1212 let command = ShutdownSystem::new(
1213 kernel.trader_id(),
1214 Ustr::from("TestComponent"),
1215 Some("unit test".to_string()),
1216 UUID4::new(),
1217 kernel.generate_timestamp_ns(),
1218 None, );
1220
1221 msgbus::publish_any(
1222 MessagingSwitchboard::shutdown_system_topic(),
1223 command.as_any(),
1224 );
1225 assert!(kernel.is_shutdown_requested());
1226
1227 kernel.reset_shutdown_flag();
1228 assert!(!kernel.is_shutdown_requested());
1229 }
1230
1231 #[rstest]
1232 fn test_shutdown_system_idempotent() {
1233 let kernel = NautilusKernelBuilder::default().build().unwrap();
1234
1235 let make_cmd = || {
1236 ShutdownSystem::new(
1237 kernel.trader_id(),
1238 Ustr::from("TestComponent"),
1239 None,
1240 UUID4::new(),
1241 kernel.generate_timestamp_ns(),
1242 None, )
1244 };
1245
1246 let topic = MessagingSwitchboard::shutdown_system_topic();
1247 msgbus::publish_any(topic, make_cmd().as_any());
1248 assert!(kernel.is_shutdown_requested());
1249
1250 msgbus::publish_any(topic, make_cmd().as_any());
1251 assert!(kernel.is_shutdown_requested());
1252
1253 kernel.reset_shutdown_flag();
1254 assert!(!kernel.is_shutdown_requested());
1255
1256 msgbus::publish_any(topic, make_cmd().as_any());
1257 assert!(kernel.is_shutdown_requested());
1258 }
1259
1260 #[rstest]
1261 fn test_shutdown_system_ignores_other_trader() {
1262 let kernel = NautilusKernelBuilder::default().build().unwrap();
1263
1264 let command = ShutdownSystem::new(
1265 TraderId::from("OTHER-TRADER"),
1266 Ustr::from("TestComponent"),
1267 None,
1268 UUID4::new(),
1269 kernel.generate_timestamp_ns(),
1270 None, );
1272
1273 msgbus::publish_any(
1274 MessagingSwitchboard::shutdown_system_topic(),
1275 command.as_any(),
1276 );
1277 assert!(!kernel.is_shutdown_requested());
1278 }
1279}
1280
1281#[cfg(all(test, feature = "streaming"))]
1282mod streaming_tests {
1283 use std::{cell::RefCell, rc::Rc, sync::Arc};
1284
1285 use nautilus_common::{
1286 clock::VirtualClock,
1287 messages::data::{DataCommand, QuotesResponse, RequestCommand, RequestQuotes},
1288 msgbus::{self, MStr, ShareableMessageHandler},
1289 };
1290 use nautilus_core::DurationNanos;
1291 use nautilus_model::{
1292 data::{CustomData, DataType, NautilusDataType, QuoteTick},
1293 identifiers::InstrumentId,
1294 types::{Price, Quantity},
1295 };
1296 use nautilus_persistence::{
1297 backend::parquet::catalog::ParquetDataCatalog, config::DataCatalogConfig,
1298 test_data::RustTestCustomData, writer::feather::RotationConfig as WriterRotationConfig,
1299 };
1300 use nautilus_serialization::ensure_custom_data_registered;
1301 use rstest::rstest;
1302 use tempfile::tempdir;
1303
1304 use super::*;
1305 use crate::config::{KernelConfig, RotationConfig, StreamingConfig};
1306
1307 #[rstest]
1308 #[case(
1309 RotationConfig::Size { max_size: 17 },
1310 WriterRotationConfig::Size { max_size: 17 }
1311 )]
1312 #[case(
1313 RotationConfig::Interval {
1314 interval_ns: DurationNanos::new(23),
1315 },
1316 WriterRotationConfig::Interval {
1317 interval_ns: 23,
1318 }
1319 )]
1320 #[case(
1321 RotationConfig::ScheduledDates {
1322 interval_ns: DurationNanos::new(31),
1323 schedule_ns: UnixNanos::from(37),
1324 },
1325 WriterRotationConfig::ScheduledDates {
1326 interval_ns: 31,
1327 rotation_time: UnixNanos::from(37),
1328 rotation_timezone: TimeZone::UTC,
1329 }
1330 )]
1331 #[case(RotationConfig::NoRotation, WriterRotationConfig::NoRotation)]
1332 fn test_writer_rotation_config(
1333 #[case] config: RotationConfig,
1334 #[case] expected: WriterRotationConfig,
1335 ) {
1336 let actual = writer_rotation_config(&config);
1337
1338 match (actual, expected) {
1339 (
1340 WriterRotationConfig::Size { max_size: actual },
1341 WriterRotationConfig::Size { max_size: expected },
1342 )
1343 | (
1344 WriterRotationConfig::Interval {
1345 interval_ns: actual,
1346 },
1347 WriterRotationConfig::Interval {
1348 interval_ns: expected,
1349 },
1350 ) => assert_eq!(actual, expected),
1351 (
1352 WriterRotationConfig::ScheduledDates {
1353 interval_ns: actual_interval,
1354 rotation_time: actual_time,
1355 rotation_timezone: actual_timezone,
1356 },
1357 WriterRotationConfig::ScheduledDates {
1358 interval_ns: expected_interval,
1359 rotation_time: expected_time,
1360 rotation_timezone: expected_timezone,
1361 },
1362 ) => {
1363 assert_eq!(actual_interval, expected_interval);
1364 assert_eq!(actual_time, expected_time);
1365 assert_eq!(actual_timezone, expected_timezone);
1366 }
1367 (WriterRotationConfig::NoRotation, WriterRotationConfig::NoRotation) => {}
1368 (actual, expected) => panic!("rotation mismatch: {actual:?} != {expected:?}"),
1369 }
1370 }
1371
1372 #[rstest]
1373 fn test_configured_catalog_serves_builtin_quotes() {
1374 let directory = tempdir().unwrap();
1375 let instrument_id = InstrumentId::from("AUD/USD.SIM");
1376 let quotes = vec![
1377 QuoteTick::new(
1378 instrument_id,
1379 Price::from("1.00001"),
1380 Price::from("1.00003"),
1381 Quantity::from("100_000"),
1382 Quantity::from("200_000"),
1383 UnixNanos::from(1),
1384 UnixNanos::from(1),
1385 ),
1386 QuoteTick::new(
1387 instrument_id,
1388 Price::from("1.00002"),
1389 Price::from("1.00004"),
1390 Quantity::from("300_000"),
1391 Quantity::from("400_000"),
1392 UnixNanos::from(2),
1393 UnixNanos::from(2),
1394 ),
1395 ];
1396 let catalog = ParquetDataCatalog::new(directory.path(), None, None, None, None);
1397 catalog.write_to_parquet("es, None, None, None).unwrap();
1398 let config = KernelConfig {
1399 catalogs: vec![
1400 DataCatalogConfig::new(
1401 directory.path().to_string_lossy().into_owned(),
1402 Some("file".to_string()),
1403 None,
1404 )
1405 .with_name(Some("history".to_string())),
1406 ],
1407 ..KernelConfig::default()
1408 };
1409 let mut kernel = NautilusKernel::new("CatalogQueryTest".to_string(), config).unwrap();
1410 kernel
1411 .clock
1412 .borrow_mut()
1413 .as_any_mut()
1414 .downcast_mut::<VirtualClock>()
1415 .unwrap()
1416 .set_time(UnixNanos::from(3));
1417 let request_id = UUID4::new();
1418 let received = Rc::new(RefCell::new(Vec::new()));
1419 let received_quotes = received.clone();
1420 msgbus::register_response_handler(
1421 &request_id,
1422 ShareableMessageHandler::from_typed(move |response: &QuotesResponse| {
1423 *received_quotes.borrow_mut() = response.data.clone();
1424 }),
1425 );
1426 let request = RequestQuotes::new(
1427 instrument_id,
1428 Some(UnixNanos::from(1).to_datetime_utc()),
1429 Some(UnixNanos::from(2).to_datetime_utc()),
1430 None,
1431 None,
1432 request_id,
1433 UnixNanos::from(3),
1434 None,
1435 );
1436
1437 kernel
1438 .data_engine
1439 .borrow_mut()
1440 .execute(DataCommand::Request(RequestCommand::Quotes(request)));
1441
1442 assert_eq!(*received.borrow(), quotes);
1443 assert_eq!(kernel.data_engine.borrow().request_count(), 1);
1444 assert_eq!(kernel.data_engine.borrow().response_count(), 1);
1445 kernel.dispose();
1446 }
1447
1448 #[rstest]
1449 fn test_configured_streaming_excludes_custom_data() {
1450 ensure_custom_data_registered::<RustTestCustomData>();
1451
1452 let directory = tempdir().unwrap();
1453 let instance_id = UUID4::new();
1454 let mut streaming = StreamingConfig::new(
1455 directory.path().to_string_lossy().into_owned(),
1456 "file".to_string(),
1457 1_000,
1458 false,
1459 RotationConfig::NoRotation,
1460 );
1461 streaming.data_types = Some(vec![NautilusDataType::QuoteTick]);
1462 let config = KernelConfig {
1463 instance_id: Some(instance_id),
1464 streaming: Some(streaming),
1465 ..KernelConfig::default()
1466 };
1467 let mut kernel = NautilusKernel::new("BuiltInStreamingTest".to_string(), config).unwrap();
1468 let instrument_id = InstrumentId::from("RUST.TEST");
1469 let custom = CustomData::new(
1470 Arc::new(RustTestCustomData {
1471 instrument_id,
1472 value: 1.23,
1473 flag: true,
1474 ts_event: UnixNanos::from(1_000),
1475 ts_init: UnixNanos::from(1_000),
1476 }),
1477 DataType::new("RustTestCustomData", None, Some(instrument_id.to_string())),
1478 );
1479
1480 msgbus::publish_any(MStr::from("data.custom"), &custom);
1481 kernel.flush_streaming().unwrap();
1482
1483 let custom_path = directory
1484 .path()
1485 .join("backtest")
1486 .join(instance_id.to_string())
1487 .join("data/custom");
1488 assert!(!custom_path.exists());
1489 kernel.dispose();
1490 }
1491}
1492
1493#[cfg(test)]
1494mod lifecycle_tests {
1495 use futures::FutureExt;
1496 use indexmap::IndexMap;
1497 use nautilus_common::{
1498 actor::registry::get_actor_unchecked,
1499 cache::Cache,
1500 messages::data::{DataCommand, SubscribeCommand, UnsubscribeCommand},
1501 msgbus::stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
1502 };
1503 use nautilus_execution::engine::SnapshotAnchorer;
1504 use nautilus_model::{
1505 enums::{OrderSide, OrderStatus, OrderType, TriggerType},
1506 identifiers::{ActorId, ClientOrderId, StrategyId},
1507 instruments::{
1508 CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
1509 },
1510 orders::{Order, OrderAny, OrderTestBuilder},
1511 types::{Price, Quantity},
1512 };
1513 use nautilus_testkit::{
1514 cache::TestCacheDatabaseControl,
1515 components::{StateActor, StateStrategy},
1516 };
1517 use rstest::rstest;
1518 use ustr::Ustr;
1519
1520 use super::*;
1521 use crate::{
1522 builder::NautilusKernelBuilder,
1523 event_store::{KernelEventStore, RegisteredComponents},
1524 };
1525
1526 #[derive(Debug)]
1527 struct RecordingEventStore {
1528 control: TestCacheDatabaseControl,
1529 opened: bool,
1530 }
1531
1532 impl KernelEventStore for RecordingEventStore {
1533 fn restore_parent_cache(
1534 &mut self,
1535 _instance_id: UUID4,
1536 _cache: &mut Cache,
1537 ) -> anyhow::Result<()> {
1538 self.control.record("event_store.restore");
1539 Ok(())
1540 }
1541
1542 fn open(
1543 &mut self,
1544 _instance_id: UUID4,
1545 _components: &RegisteredComponents,
1546 _environment: Environment,
1547 ) -> anyhow::Result<()> {
1548 self.control.record("event_store.open");
1549 self.opened = true;
1550 Ok(())
1551 }
1552
1553 fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
1554 None
1555 }
1556
1557 fn seal(&mut self, _ts_init: UnixNanos) {
1558 if self.opened {
1559 self.control.record("event_store.seal");
1560 self.opened = false;
1561 }
1562 }
1563
1564 fn run_id(&self) -> Option<&str> {
1565 None
1566 }
1567
1568 fn parent_run_id(&self) -> Option<&str> {
1569 None
1570 }
1571
1572 fn is_halted(&self) -> bool {
1573 false
1574 }
1575 }
1576
1577 fn state(key: &str, value: &[u8]) -> IndexMap<String, Vec<u8>> {
1578 IndexMap::from([(key.to_string(), value.to_vec())])
1579 }
1580
1581 fn finalize(kernel: &mut NautilusKernel) -> anyhow::Result<()> {
1582 kernel
1583 .finalize_stop()
1584 .now_or_never()
1585 .expect("kernel finalization must not yield")
1586 }
1587
1588 fn add_state_components(
1589 kernel: &NautilusKernel,
1590 control: &TestCacheDatabaseControl,
1591 actor: StateActor,
1592 strategy: StateStrategy,
1593 ) {
1594 kernel.trader.borrow_mut().add_actor(actor).unwrap();
1595 kernel.trader.borrow_mut().add_strategy(strategy).unwrap();
1596 control.record("components.registered");
1597 }
1598
1599 fn create_stop_market_order(instrument: &CryptoPerpetual, client_order_id: &str) -> OrderAny {
1600 OrderTestBuilder::new(OrderType::StopMarket)
1601 .instrument_id(instrument.id())
1602 .client_order_id(ClientOrderId::from(client_order_id))
1603 .side(OrderSide::Buy)
1604 .trigger_price(Price::from("5100.00"))
1605 .quantity(Quantity::from(1))
1606 .emulation_trigger(TriggerType::BidAsk)
1607 .build()
1608 }
1609
1610 fn register_data_command_handler(id: &str) -> TypedIntoMessageSavingHandler<DataCommand> {
1611 let (handler, saving_handler) =
1612 get_typed_into_message_saving_handler::<DataCommand>(Some(Ustr::from(id)));
1613 msgbus::register_data_command_endpoint(
1614 MessagingSwitchboard::data_engine_queue_execute(),
1615 handler,
1616 );
1617 saving_handler
1618 }
1619
1620 #[rstest]
1621 fn test_state_persistence_orders_restore_load_start_stop_save_seal_and_dispose() {
1622 let actor_id = ActorId::from("STATE-ACTOR");
1623 let strategy_id = StrategyId::from("STATE-STRATEGY-001");
1624 let actor_load = state("actor-loaded", b"actor-load-value");
1625 let strategy_load = state("strategy-loaded", b"strategy-load-value");
1626 let actor_save = state("actor-saved", b"actor-save-value");
1627 let strategy_save = state("strategy-saved", b"strategy-save-value");
1628 let (database, control) = TestCacheDatabaseControl::create();
1629 control.set_actor_state(actor_id, &actor_load);
1630 control.set_strategy_state(strategy_id, &strategy_load);
1631
1632 let event_store_control = control.clone();
1633 let mut kernel = NautilusKernelBuilder::default()
1634 .with_cache_database(Box::new(database))
1635 .with_event_store(move |_instance_id, _clock| {
1636 Ok(Box::new(RecordingEventStore {
1637 control: event_store_control,
1638 opened: false,
1639 }))
1640 })
1641 .build()
1642 .unwrap();
1643
1644 let actor = StateActor::new(actor_id, control.clone(), actor_save.clone());
1645 let strategy = StateStrategy::new(strategy_id, control.clone(), strategy_save.clone());
1646 add_state_components(&kernel, &control, actor, strategy);
1647
1648 kernel.start();
1649 kernel.start_trader().unwrap();
1650
1651 let actor_state = get_actor_unchecked::<StateActor>(&actor_id.inner())
1652 .state_load()
1653 .cloned();
1654 let strategy_state = get_actor_unchecked::<StateStrategy>(&strategy_id.inner())
1655 .state_load()
1656 .cloned();
1657 assert_eq!(actor_state, Some(actor_load));
1658 assert_eq!(strategy_state, Some(strategy_load));
1659
1660 kernel.stop_trader();
1661 kernel.stop_trader();
1662 finalize(&mut kernel).unwrap();
1663 finalize(&mut kernel).unwrap();
1664 kernel.dispose();
1665
1666 assert_eq!(
1667 control.events(),
1668 vec![
1669 "components.registered",
1670 "event_store.restore",
1671 "event_store.open",
1672 "actor.load:STATE-ACTOR",
1673 "actor.on_load",
1674 "strategy.load:STATE-STRATEGY-001",
1675 "strategy.on_load",
1676 "actor.on_start",
1677 "strategy.on_start",
1678 "actor.on_stop",
1679 "strategy.on_stop",
1680 "actor.on_save",
1681 "actor.update:STATE-ACTOR",
1682 "strategy.on_save",
1683 "strategy.update:STATE-STRATEGY-001",
1684 "event_store.seal",
1685 "database.close",
1686 ]
1687 );
1688 assert_eq!(control.actor_state(&actor_id), Some(actor_save));
1689 assert_eq!(control.strategy_state(&strategy_id), Some(strategy_save));
1690 }
1691
1692 #[rstest]
1693 fn test_state_persistence_skips_callbacks_without_cache_backing() {
1694 let actor_id = ActorId::from("NO-BACKING-ACTOR");
1695 let strategy_id = StrategyId::from("NO-BACKING-STRATEGY-001");
1696 let control = TestCacheDatabaseControl::default();
1697 let mut kernel = NautilusKernelBuilder::default().build().unwrap();
1698 let actor = StateActor::new(actor_id, control.clone(), state("actor", b"save"));
1699 let strategy = StateStrategy::new(strategy_id, control.clone(), state("strategy", b"save"));
1700 add_state_components(&kernel, &control, actor, strategy);
1701
1702 kernel.start();
1703 kernel.start_trader().unwrap();
1704 kernel.stop_trader();
1705 finalize(&mut kernel).unwrap();
1706 kernel.dispose();
1707
1708 assert_eq!(
1709 control.events(),
1710 vec![
1711 "components.registered",
1712 "actor.on_start",
1713 "strategy.on_start",
1714 "actor.on_stop",
1715 "strategy.on_stop",
1716 ]
1717 );
1718 }
1719
1720 #[rstest]
1721 fn test_state_persistence_skips_empty_load_and_persists_empty_save() {
1722 let actor_id = ActorId::from("EMPTY-STATE-ACTOR");
1723 let strategy_id = StrategyId::from("EMPTY-STATE-STRATEGY-001");
1724 let (database, control) = TestCacheDatabaseControl::create();
1725 let mut kernel = NautilusKernelBuilder::default()
1726 .with_cache_database(Box::new(database))
1727 .build()
1728 .unwrap();
1729 let actor = StateActor::new(actor_id, control.clone(), IndexMap::new());
1730 let strategy = StateStrategy::new(strategy_id, control.clone(), IndexMap::new());
1731 add_state_components(&kernel, &control, actor, strategy);
1732
1733 kernel.start();
1734 kernel.start_trader().unwrap();
1735 kernel.stop_trader();
1736 finalize(&mut kernel).unwrap();
1737
1738 assert_eq!(
1739 control.events(),
1740 vec![
1741 "components.registered",
1742 "actor.load:EMPTY-STATE-ACTOR",
1743 "strategy.load:EMPTY-STATE-STRATEGY-001",
1744 "actor.on_start",
1745 "strategy.on_start",
1746 "actor.on_stop",
1747 "strategy.on_stop",
1748 "actor.on_save",
1749 "actor.update:EMPTY-STATE-ACTOR",
1750 "strategy.on_save",
1751 "strategy.update:EMPTY-STATE-STRATEGY-001",
1752 ]
1753 );
1754 assert_eq!(control.actor_state(&actor_id), Some(IndexMap::new()));
1755 assert_eq!(control.strategy_state(&strategy_id), Some(IndexMap::new()));
1756 kernel.dispose();
1757 }
1758
1759 #[rstest]
1760 fn test_state_save_reports_all_callback_errors_and_continues_shutdown() {
1761 let actor_id = ActorId::from("FAIL-SAVE-ACTOR");
1762 let strategy_id = StrategyId::from("FAIL-SAVE-STRATEGY-001");
1763 let (database, control) = TestCacheDatabaseControl::create();
1764 let event_store_control = control.clone();
1765 let mut kernel = NautilusKernelBuilder::default()
1766 .with_cache_database(Box::new(database))
1767 .with_event_store(move |_instance_id, _clock| {
1768 Ok(Box::new(RecordingEventStore {
1769 control: event_store_control,
1770 opened: false,
1771 }))
1772 })
1773 .build()
1774 .unwrap();
1775 let actor = StateActor::new(actor_id, control.clone(), IndexMap::new()).with_fail_save();
1776 let strategy =
1777 StateStrategy::new(strategy_id, control.clone(), IndexMap::new()).with_fail_save();
1778 add_state_components(&kernel, &control, actor, strategy);
1779
1780 kernel.start();
1781 kernel.start_trader().unwrap();
1782 kernel.stop_trader();
1783 let expected_shutdown = kernel.clock.borrow().timestamp_ns();
1784 let error = finalize(&mut kernel).unwrap_err();
1785 kernel.dispose();
1786
1787 assert_eq!(
1788 error.to_string(),
1789 "Failed to save component state: actor FAIL-SAVE-ACTOR callback: test actor on_save \
1790 failure; strategy FAIL-SAVE-STRATEGY-001 callback: test strategy on_save failure"
1791 );
1792 assert_eq!(kernel.ts_shutdown, Some(expected_shutdown));
1793 assert_eq!(
1794 control.events(),
1795 vec![
1796 "components.registered",
1797 "event_store.restore",
1798 "event_store.open",
1799 "actor.load:FAIL-SAVE-ACTOR",
1800 "strategy.load:FAIL-SAVE-STRATEGY-001",
1801 "actor.on_start",
1802 "strategy.on_start",
1803 "actor.on_stop",
1804 "strategy.on_stop",
1805 "actor.on_save",
1806 "strategy.on_save",
1807 "event_store.seal",
1808 "database.close",
1809 ]
1810 );
1811 }
1812
1813 #[rstest]
1814 fn test_state_load_callback_failure_prevents_start_and_save() {
1815 let actor_id = ActorId::from("FAIL-LOAD-ACTOR");
1816 let strategy_id = StrategyId::from("FAIL-LOAD-STRATEGY-001");
1817 let (database, control) = TestCacheDatabaseControl::create();
1818 control.set_actor_state(actor_id, &state("actor", b"load"));
1819 control.set_strategy_state(strategy_id, &state("strategy", b"load"));
1820 let mut kernel = NautilusKernelBuilder::default()
1821 .with_cache_database(Box::new(database))
1822 .build()
1823 .unwrap();
1824 let actor = StateActor::new(actor_id, control.clone(), IndexMap::new()).with_fail_load();
1825 let strategy = StateStrategy::new(strategy_id, control.clone(), IndexMap::new());
1826 add_state_components(&kernel, &control, actor, strategy);
1827
1828 kernel.start();
1829 let error = kernel.start_trader().unwrap_err();
1830 kernel.dispose();
1831
1832 assert_eq!(
1833 error.to_string(),
1834 "Failed to load actor and strategy state: Failed to restore actor FAIL-LOAD-ACTOR \
1835 state: test actor on_load failure"
1836 );
1837 assert_eq!(
1838 control.events(),
1839 vec![
1840 "components.registered",
1841 "actor.load:FAIL-LOAD-ACTOR",
1842 "actor.on_load",
1843 "database.close",
1844 ]
1845 );
1846 }
1847
1848 #[rstest]
1849 fn test_state_save_reports_all_persistence_errors() {
1850 let actor_id = ActorId::from("FAIL-UPDATE-ACTOR");
1851 let strategy_id = StrategyId::from("FAIL-UPDATE-STRATEGY-001");
1852 let (database, control) = TestCacheDatabaseControl::create();
1853 control.set_fail_update_actor(true);
1854 control.set_fail_update_strategy(true);
1855 let mut kernel = NautilusKernelBuilder::default()
1856 .with_cache_database(Box::new(database))
1857 .build()
1858 .unwrap();
1859 let actor = StateActor::new(actor_id, control.clone(), state("actor", b"save"));
1860 let strategy = StateStrategy::new(strategy_id, control.clone(), state("strategy", b"save"));
1861 add_state_components(&kernel, &control, actor, strategy);
1862
1863 kernel.start();
1864 kernel.start_trader().unwrap();
1865 kernel.stop_trader();
1866 let error = finalize(&mut kernel).unwrap_err();
1867 kernel.dispose();
1868
1869 assert_eq!(
1870 error.to_string(),
1871 "Failed to save component state: actor FAIL-UPDATE-ACTOR persistence: test actor \
1872 update failure; strategy FAIL-UPDATE-STRATEGY-001 persistence: test strategy update \
1873 failure"
1874 );
1875 assert_eq!(
1876 control.events(),
1877 vec![
1878 "components.registered",
1879 "actor.load:FAIL-UPDATE-ACTOR",
1880 "strategy.load:FAIL-UPDATE-STRATEGY-001",
1881 "actor.on_start",
1882 "strategy.on_start",
1883 "actor.on_stop",
1884 "strategy.on_stop",
1885 "actor.on_save",
1886 "actor.update:FAIL-UPDATE-ACTOR",
1887 "strategy.on_save",
1888 "strategy.update:FAIL-UPDATE-STRATEGY-001",
1889 "database.close",
1890 ]
1891 );
1892 }
1893
1894 #[rstest]
1895 fn test_partial_startup_stops_and_saves_once() {
1896 let actor_id = ActorId::from("PARTIAL-ACTOR");
1897 let strategy_id = StrategyId::from("PARTIAL-STRATEGY-001");
1898 let (database, control) = TestCacheDatabaseControl::create();
1899 let mut kernel = NautilusKernelBuilder::default()
1900 .with_cache_database(Box::new(database))
1901 .build()
1902 .unwrap();
1903 let actor = StateActor::new(actor_id, control.clone(), state("actor", b"partial"));
1904 let strategy =
1905 StateStrategy::new(strategy_id, control.clone(), state("strategy", b"partial"))
1906 .with_fail_start();
1907 add_state_components(&kernel, &control, actor, strategy);
1908
1909 kernel.start();
1910 let error = kernel.start_trader().unwrap_err();
1911 kernel.dispose();
1912
1913 assert_eq!(
1914 error.to_string(),
1915 "Failed to start trader: test strategy on_start failure"
1916 );
1917 assert_eq!(
1918 control.events(),
1919 vec![
1920 "components.registered",
1921 "actor.load:PARTIAL-ACTOR",
1922 "strategy.load:PARTIAL-STRATEGY-001",
1923 "actor.on_start",
1924 "strategy.on_start",
1925 "actor.on_stop",
1926 "strategy.on_stop",
1927 "actor.on_save",
1928 "actor.update:PARTIAL-ACTOR",
1929 "strategy.on_save",
1930 "strategy.update:PARTIAL-STRATEGY-001",
1931 "database.close",
1932 ]
1933 );
1934 assert_eq!(
1935 control.actor_state(&actor_id),
1936 Some(state("actor", b"partial"))
1937 );
1938 assert_eq!(
1939 control.strategy_state(&strategy_id),
1940 Some(state("strategy", b"partial"))
1941 );
1942 }
1943
1944 #[rstest]
1945 fn test_forced_dispose_stops_and_saves_once() {
1946 let actor_id = ActorId::from("FORCED-ACTOR");
1947 let strategy_id = StrategyId::from("FORCED-STRATEGY-001");
1948 let (database, control) = TestCacheDatabaseControl::create();
1949 let mut kernel = NautilusKernelBuilder::default()
1950 .with_cache_database(Box::new(database))
1951 .build()
1952 .unwrap();
1953 let actor = StateActor::new(actor_id, control.clone(), state("actor", b"forced"));
1954 let strategy =
1955 StateStrategy::new(strategy_id, control.clone(), state("strategy", b"forced"));
1956 add_state_components(&kernel, &control, actor, strategy);
1957
1958 kernel.start();
1959 kernel.start_trader().unwrap();
1960 kernel.dispose();
1961
1962 assert_eq!(
1963 control.events(),
1964 vec![
1965 "components.registered",
1966 "actor.load:FORCED-ACTOR",
1967 "strategy.load:FORCED-STRATEGY-001",
1968 "actor.on_start",
1969 "strategy.on_start",
1970 "actor.on_stop",
1971 "strategy.on_stop",
1972 "actor.on_save",
1973 "actor.update:FORCED-ACTOR",
1974 "strategy.on_save",
1975 "strategy.update:FORCED-STRATEGY-001",
1976 "database.close",
1977 ]
1978 );
1979 }
1980
1981 #[rstest]
1982 fn test_start_trader_starts_order_emulator_for_cached_emulated_orders() {
1983 let mut kernel = NautilusKernelBuilder::default().build().unwrap();
1984 let data_commands = register_data_command_handler("DataEngine.queue_execute.kernel_start");
1985 let instrument = crypto_perpetual_ethusdt();
1986 let instrument_id = instrument.id();
1987 let first_order = create_stop_market_order(&instrument, "O-KERNEL-001");
1988 let second_order = create_stop_market_order(&instrument, "O-KERNEL-002");
1989 let first_client_order_id = first_order.client_order_id();
1990 let second_client_order_id = second_order.client_order_id();
1991 kernel
1992 .cache
1993 .borrow_mut()
1994 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
1995 .unwrap();
1996 kernel
1997 .cache
1998 .borrow_mut()
1999 .add_order(first_order, None, None, false)
2000 .unwrap();
2001 kernel
2002 .cache
2003 .borrow_mut()
2004 .add_order(second_order, None, None, false)
2005 .unwrap();
2006
2007 kernel.start();
2008 assert!(
2009 kernel
2010 .order_emulator
2011 .get_emulator()
2012 .get_matching_core(&instrument_id)
2013 .is_none()
2014 );
2015 kernel.start_trader().unwrap();
2016
2017 let commands = data_commands.get_messages();
2018 let cache = kernel.cache.borrow();
2019 let first_status = cache.order(&first_client_order_id).unwrap().status();
2020 let second_status = cache.order(&second_client_order_id).unwrap().status();
2021 drop(cache);
2022 let emulator = kernel.order_emulator.get_emulator();
2023 assert!(emulator.get_matching_core(&instrument_id).is_some());
2024 assert_eq!(emulator.subscribed_quotes(), vec![instrument_id]);
2025 assert_eq!(first_status, OrderStatus::Emulated);
2026 assert_eq!(second_status, OrderStatus::Emulated);
2027 assert!(commands.iter().any(|command| matches!(
2028 command,
2029 DataCommand::Subscribe(SubscribeCommand::Quotes(command))
2030 if command.instrument_id == instrument_id
2031 )));
2032
2033 data_commands.clear();
2034 drop(emulator);
2035 kernel.stop_trader();
2036 kernel.dispose();
2037
2038 let commands = data_commands.get_messages();
2039 let emulator = kernel.order_emulator.get_emulator();
2040 assert!(emulator.subscribed_quotes().is_empty());
2041 assert!(emulator.get_matching_core(&instrument_id).is_none());
2042 assert!(commands.iter().any(|command| matches!(
2043 command,
2044 DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
2045 if command.instrument_id == instrument_id
2046 )));
2047 }
2048
2049 #[rstest]
2050 fn test_reset_resets_order_emulator_state() {
2051 let mut kernel = NautilusKernelBuilder::default().build().unwrap();
2052 let data_commands = register_data_command_handler("DataEngine.queue_execute.kernel_reset");
2053 let instrument = crypto_perpetual_ethusdt();
2054 let instrument_id = instrument.id();
2055 let order = create_stop_market_order(&instrument, "O-KERNEL-RESET-001");
2056 kernel
2057 .cache
2058 .borrow_mut()
2059 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
2060 .unwrap();
2061 kernel
2062 .cache
2063 .borrow_mut()
2064 .add_order(order, None, None, false)
2065 .unwrap();
2066
2067 kernel.start();
2068 kernel.start_trader().unwrap();
2069 assert!(
2070 kernel
2071 .order_emulator
2072 .get_emulator()
2073 .get_matching_core(&instrument_id)
2074 .is_some()
2075 );
2076 kernel.stop_trader();
2077 data_commands.clear();
2078
2079 kernel.reset();
2080
2081 let commands = data_commands.get_messages();
2082 let emulator = kernel.order_emulator.get_emulator();
2083 assert!(emulator.subscribed_quotes().is_empty());
2084 assert!(emulator.get_matching_core(&instrument_id).is_none());
2085 assert!(commands.iter().any(|command| matches!(
2086 command,
2087 DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
2088 if command.instrument_id == instrument_id
2089 )));
2090
2091 drop(emulator);
2092 kernel.dispose();
2093 }
2094}