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