1use std::{
17 cell::{Cell, Ref, RefCell},
18 fmt::Debug,
19 rc::Rc,
20 time::Duration,
21};
22
23use nautilus_common::{
24 cache::{Cache, CacheConfig, database::CacheDatabaseAdapter},
25 clock::Clock,
26 component::Component,
27 enums::Environment,
28 logging::{
29 arm_shutdown_on_error, disarm_shutdown_on_error, headers, init_logging,
30 logger::{LogGuard, LoggerConfig},
31 try_drain_shutdown_on_error_trigger,
32 },
33 messages::system::ShutdownSystem,
34 msgbus::{
35 self, MessageBus, MessagingSwitchboard, ShareableMessageHandler, get_message_bus,
36 set_message_bus,
37 },
38};
39use nautilus_core::{UUID4, UnixNanos};
40use nautilus_data::engine::DataEngine;
41use nautilus_execution::{
42 engine::ExecutionEngine,
43 order_emulator::{adapter::OrderEmulatorAdapter, emulator::OrderEmulator},
44};
45use nautilus_model::identifiers::{ClientId, TraderId};
46use nautilus_portfolio::portfolio::Portfolio;
47use nautilus_risk::engine::RiskEngine;
48use ustr::Ustr;
49
50use crate::{
51 builder::NautilusKernelBuilder,
52 clock_factory::ClockFactory,
53 config::NautilusKernelConfig,
54 event_store::{EventStoreFactory, KernelEventStore, RegisteredComponents},
55 trader::Trader,
56};
57
58#[derive(Debug)]
62pub struct NautilusKernel {
63 pub name: String,
65 pub instance_id: UUID4,
67 pub machine_id: String,
69 pub config: Box<dyn NautilusKernelConfig>,
71 pub cache: Rc<RefCell<Cache>>,
73 pub clock: Rc<RefCell<dyn Clock>>,
75 pub portfolio: Rc<RefCell<Portfolio>>,
77 pub log_guard: LogGuard,
79 pub data_engine: Rc<RefCell<DataEngine>>,
81 pub risk_engine: Rc<RefCell<RiskEngine>>,
83 pub exec_engine: Rc<RefCell<ExecutionEngine>>,
85 pub order_emulator: OrderEmulatorAdapter,
87 pub trader: Rc<RefCell<Trader>>,
89 pub ts_created: UnixNanos,
91 pub ts_started: Option<UnixNanos>,
93 pub ts_shutdown: Option<UnixNanos>,
95 shutdown_requested: Rc<Cell<bool>>,
96 event_store: Option<Box<dyn KernelEventStore>>,
97 event_store_replay: bool,
98}
99
100#[derive(Default)]
102pub struct NautilusKernelDependencies {
103 clock_factory: Option<ClockFactory>,
104 cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
105 event_store_factory: Option<EventStoreFactory>,
106}
107
108impl Debug for NautilusKernelDependencies {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.debug_struct(stringify!(NautilusKernelDependencies))
111 .field("clock_factory", &self.clock_factory.is_some())
112 .field("cache_database", &self.cache_database.is_some())
113 .field("event_store_factory", &self.event_store_factory.is_some())
114 .finish()
115 }
116}
117
118impl NautilusKernelDependencies {
119 #[must_use]
121 pub fn with_clock_factory(mut self, clock_factory: Option<ClockFactory>) -> Self {
122 self.clock_factory = clock_factory;
123 self
124 }
125
126 #[must_use]
128 pub fn with_cache_database(
129 mut self,
130 cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
131 ) -> Self {
132 self.cache_database = cache_database;
133 self
134 }
135
136 #[must_use]
138 pub fn with_event_store_factory(
139 mut self,
140 event_store_factory: Option<EventStoreFactory>,
141 ) -> Self {
142 self.event_store_factory = event_store_factory;
143 self
144 }
145}
146
147impl NautilusKernel {
148 #[must_use]
150 pub const fn builder(
151 name: String,
152 trader_id: TraderId,
153 environment: Environment,
154 ) -> NautilusKernelBuilder {
155 NautilusKernelBuilder::new(name, trader_id, environment)
156 }
157
158 pub fn new<T: NautilusKernelConfig + 'static>(name: String, config: T) -> anyhow::Result<Self> {
164 Self::new_with(name, config, None, None)
165 }
166
167 pub fn new_with_cache_database<T: NautilusKernelConfig + 'static>(
178 name: String,
179 config: T,
180 cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
181 ) -> anyhow::Result<Self> {
182 Self::new_with(name, config, cache_database, None)
183 }
184
185 pub fn new_with<T: NautilusKernelConfig + 'static>(
197 name: String,
198 config: T,
199 cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
200 event_store_factory: Option<EventStoreFactory>,
201 ) -> anyhow::Result<Self> {
202 Self::new_with_dependencies(
203 name,
204 config,
205 NautilusKernelDependencies::default()
206 .with_cache_database(cache_database)
207 .with_event_store_factory(event_store_factory),
208 )
209 }
210
211 pub fn new_with_dependencies<T: NautilusKernelConfig + 'static>(
217 name: String,
218 config: T,
219 dependencies: NautilusKernelDependencies,
220 ) -> anyhow::Result<Self> {
221 let NautilusKernelDependencies {
222 clock_factory,
223 cache_database,
224 event_store_factory,
225 } = dependencies;
226 let instance_id = config.instance_id().unwrap_or_default();
227 let machine_id = Self::determine_machine_id()?;
228
229 let logger_config = config.logging();
230 let log_guard = Self::initialize_logging(config.trader_id(), instance_id, logger_config)?;
231 headers::log_header(
232 config.trader_id(),
233 &machine_id,
234 instance_id,
235 Ustr::from(&name),
236 );
237
238 log::info!("Building system kernel");
239
240 let clock_factory =
241 clock_factory.unwrap_or_else(|| ClockFactory::for_environment(config.environment()));
242 let clock = clock_factory.clock();
243 let event_store = match event_store_factory {
244 Some(factory) => Some(factory(instance_id, clock.clone())?),
245 None => None,
246 };
247 let cache = Self::initialize_cache(config.cache(), cache_database);
248
249 let msgbus = Rc::new(RefCell::new(MessageBus::new(
250 config.trader_id(),
251 instance_id,
252 Some(name.clone()),
253 None,
254 )));
255 set_message_bus(msgbus);
256
257 if let Some(config) = config.msgbus()
258 && let Some(filter) = config.types_filter
259 {
260 get_message_bus().borrow_mut().set_types_filter(filter);
261 }
262
263 let portfolio = Rc::new(RefCell::new(Portfolio::new(
264 clock.clone(),
265 cache.clone(),
266 config.portfolio(),
267 )));
268
269 let risk_engine = RiskEngine::new(
270 config.risk_engine().unwrap_or_default(),
271 portfolio.borrow().clone_shallow(),
272 clock.clone(),
273 cache.clone(),
274 );
275 let risk_engine = Rc::new(RefCell::new(risk_engine));
276
277 let exec_engine = ExecutionEngine::new(clock.clone(), cache.clone(), config.exec_engine());
278 let exec_engine = Rc::new(RefCell::new(exec_engine));
279
280 let order_emulator = OrderEmulatorAdapter::new(clock.clone(), cache.clone());
281
282 let data_engine = DataEngine::new(clock.clone(), cache.clone(), config.data_engine());
283 let data_engine = Rc::new(RefCell::new(data_engine));
284
285 DataEngine::register_msgbus_handlers(&data_engine);
286 RiskEngine::register_msgbus_handlers(&risk_engine);
287 ExecutionEngine::register_msgbus_handlers(&exec_engine);
288 OrderEmulator::register_msgbus_handlers(&order_emulator.emulator());
289
290 let shutdown_requested = Rc::new(Cell::new(false));
291 Self::register_shutdown_handler(config.trader_id(), shutdown_requested.clone());
292
293 let trader = Rc::new(RefCell::new(Trader::new(
294 config.trader_id(),
295 instance_id,
296 config.environment(),
297 clock_factory,
298 cache.clone(),
299 portfolio.clone(),
300 )));
301
302 let ts_created = clock.borrow().timestamp_ns();
303
304 Ok(Self {
305 name,
306 instance_id,
307 machine_id,
308 event_store,
309 config: Box::new(config),
310 cache,
311 clock,
312 portfolio,
313 log_guard,
314 data_engine,
315 risk_engine,
316 exec_engine,
317 order_emulator,
318 trader,
319 ts_created,
320 ts_started: None,
321 ts_shutdown: None,
322 shutdown_requested,
323 event_store_replay: false,
324 })
325 }
326
327 fn register_shutdown_handler(trader_id: TraderId, shutdown_requested: Rc<Cell<bool>>) {
328 let handler = ShareableMessageHandler::from_typed(move |cmd: &ShutdownSystem| {
329 if cmd.trader_id != trader_id {
330 log::warn!("Received {cmd} not for this trader {trader_id}, ignoring");
331 return;
332 }
333
334 if shutdown_requested.get() {
335 log::debug!("Shutdown already requested, ignoring {cmd}");
336 return;
337 }
338
339 log::info!("Received {cmd}, requesting shutdown");
340 shutdown_requested.set(true);
341 });
342 let topic = MessagingSwitchboard::shutdown_system_topic();
343 msgbus::subscribe_any(topic.into(), handler, None);
344 }
345
346 fn determine_machine_id() -> anyhow::Result<String> {
347 sysinfo::System::host_name().ok_or_else(|| anyhow::anyhow!("Failed to determine hostname"))
348 }
349
350 fn initialize_logging(
351 trader_id: TraderId,
352 instance_id: UUID4,
353 config: LoggerConfig,
354 ) -> anyhow::Result<LogGuard> {
355 #[cfg(feature = "tracing-bridge")]
356 let use_tracing = config.use_tracing;
357
358 let file_config = config.file_config.clone().unwrap_or_default();
359 let log_guard = match init_logging(trader_id, instance_id, config, file_config) {
360 Ok(guard) => guard,
361 Err(e) => {
362 if e.downcast_ref::<log::SetLoggerError>().is_some() {
367 if let Some(guard) = LogGuard::new() {
368 guard
369 } else {
370 return Err(e.context(
371 "A non-Nautilus logger is already registered; \
372 cannot initialize Nautilus logging",
373 ));
374 }
375 } else {
376 return Err(e);
377 }
378 }
379 };
380
381 #[cfg(feature = "tracing-bridge")]
383 if use_tracing && !nautilus_common::logging::bridge::tracing_is_initialized() {
384 nautilus_common::logging::bridge::init_tracing()?;
385 }
386
387 Ok(log_guard)
388 }
389
390 fn initialize_cache(
391 cache_config: Option<CacheConfig>,
392 cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
393 ) -> Rc<RefCell<Cache>> {
394 let cache_config = cache_config.unwrap_or_default();
395 let cache = Cache::new(Some(cache_config), cache_database);
396
397 Rc::new(RefCell::new(cache))
398 }
399
400 fn cancel_timers(&self) {
401 self.clock.borrow_mut().cancel_timers();
402 }
403
404 #[must_use]
405 pub fn generate_timestamp_ns(&self) -> UnixNanos {
406 self.clock.borrow().timestamp_ns()
407 }
408
409 #[must_use]
411 pub fn environment(&self) -> Environment {
412 self.config.environment()
413 }
414
415 #[must_use]
417 pub const fn name(&self) -> &str {
418 self.name.as_str()
419 }
420
421 #[must_use]
423 pub fn trader_id(&self) -> TraderId {
424 self.config.trader_id()
425 }
426
427 #[must_use]
429 pub fn machine_id(&self) -> &str {
430 &self.machine_id
431 }
432
433 #[must_use]
435 pub const fn instance_id(&self) -> UUID4 {
436 self.instance_id
437 }
438
439 #[must_use]
441 pub fn delay_post_stop(&self) -> Duration {
442 self.config.delay_post_stop()
443 }
444
445 #[must_use]
447 pub const fn ts_created(&self) -> UnixNanos {
448 self.ts_created
449 }
450
451 #[must_use]
453 pub const fn ts_started(&self) -> Option<UnixNanos> {
454 self.ts_started
455 }
456
457 #[must_use]
459 pub const fn ts_shutdown(&self) -> Option<UnixNanos> {
460 self.ts_shutdown
461 }
462
463 #[must_use]
467 pub fn is_shutdown_requested(&self) -> bool {
468 self.drain_shutdown_on_error_trigger();
469 self.shutdown_requested.get()
470 }
471
472 pub fn reset_shutdown_flag(&self) {
477 self.shutdown_requested.set(false);
478 }
479
480 #[must_use]
483 pub fn shutdown_flag(&self) -> Rc<Cell<bool>> {
484 self.shutdown_requested.clone()
485 }
486
487 fn drain_shutdown_on_error_trigger(&self) {
488 try_drain_shutdown_on_error_trigger(|trigger| {
489 let command = ShutdownSystem::new(
490 self.config.trader_id(),
491 trigger.component,
492 Some(format!(
493 "Error log received from {}: {}",
494 trigger.component, trigger.message
495 )),
496 UUID4::new(),
497 trigger.timestamp,
498 None,
499 );
500
501 msgbus::try_publish_any(
502 MessagingSwitchboard::shutdown_system_topic(),
503 command.as_any(),
504 )
505 });
506 }
507
508 #[must_use]
510 pub fn load_state(&self) -> bool {
511 self.config.load_state()
512 }
513
514 #[must_use]
516 pub fn save_state(&self) -> bool {
517 self.config.save_state()
518 }
519
520 #[must_use]
522 pub fn clock(&self) -> Rc<RefCell<dyn Clock>> {
523 self.clock.clone()
524 }
525
526 #[must_use]
528 pub fn cache(&self) -> Rc<RefCell<Cache>> {
529 self.cache.clone()
530 }
531
532 #[must_use]
534 pub fn portfolio(&self) -> Ref<'_, Portfolio> {
535 self.portfolio.borrow()
536 }
537
538 #[must_use]
540 pub fn data_engine(&self) -> Ref<'_, DataEngine> {
541 self.data_engine.borrow()
542 }
543
544 #[must_use]
546 pub const fn risk_engine(&self) -> &Rc<RefCell<RiskEngine>> {
547 &self.risk_engine
548 }
549
550 #[must_use]
552 pub const fn exec_engine(&self) -> &Rc<RefCell<ExecutionEngine>> {
553 &self.exec_engine
554 }
555
556 #[must_use]
558 pub fn trader(&self) -> &Rc<RefCell<Trader>> {
559 &self.trader
560 }
561
562 pub fn start(&mut self) {
564 arm_shutdown_on_error(self.config.shutdown_on_error());
565 log::info!("Starting");
566
567 self.event_store_replay = false;
568
569 if let Some(event_store) = self.event_store.as_deref_mut() {
570 self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
571
572 let components = Self::collect_registered_components(&self.trader);
573 let environment = self.config.environment();
574 let event_store_replay_configured = event_store.is_event_store_replay_configured();
575
576 if event_store_replay_configured && !self.config.load_state() {
577 log::error!("Event-store replay requires load_state=true");
578 return;
579 }
580
581 if self.config.load_state()
582 && let Err(e) =
583 event_store.restore_parent_cache(self.instance_id, &mut self.cache.borrow_mut())
584 {
585 log::error!("Failed to restore cache from event-store replay source: {e}");
586 return;
587 }
588
589 if let Err(e) = event_store.open(self.instance_id, &components, environment) {
590 log::error!("Failed to open event-store run: {e}");
591 return;
592 }
593
594 let anchorer = event_store.snapshot_anchorer();
595 self.exec_engine
596 .borrow_mut()
597 .set_snapshot_anchorer(anchorer);
598 self.event_store_replay = event_store_replay_configured;
599 }
600
601 if self.event_store_replay {
602 log::info!(
603 "Event-store replay loaded; skipping engines, clients, trader startup, and live reconciliation",
604 );
605 self.ts_started = Some(self.clock.borrow().timestamp_ns());
606 log::info!("Started");
607 return;
608 }
609
610 self.start_engines();
611
612 log::info!("Initializing trader");
613 if let Err(e) = self.trader.borrow_mut().initialize() {
614 log::error!("Error initializing trader: {e:?}");
615 return;
616 }
617
618 self.ts_started = Some(self.clock.borrow().timestamp_ns());
621 log::info!("Started");
622 }
623
624 fn collect_registered_components(trader: &Rc<RefCell<Trader>>) -> RegisteredComponents {
625 let trader = trader.borrow();
626 let mut components = RegisteredComponents::default();
627 for actor_id in trader.actor_ids() {
628 components
629 .actors
630 .insert(actor_id.to_string(), String::new());
631 }
632
633 for strategy_id in trader.strategy_ids() {
634 components
635 .strategies
636 .insert(strategy_id.to_string(), String::new());
637 }
638
639 for algo_id in trader.exec_algorithm_ids() {
640 components
641 .algorithms
642 .insert(algo_id.to_string(), String::new());
643 }
644 components
645 }
646
647 #[expect(
649 clippy::unused_async,
650 reason = "keeps the public async kernel API shape stable"
651 )]
652 pub async fn start_async(&mut self) {
653 self.start();
654 }
655
656 pub fn start_trader(&mut self) {
660 log::info!("Starting trader...");
661 self.order_emulator.start();
662 if let Err(e) = self.trader.borrow_mut().start() {
663 log::error!("Error starting trader: {e:?}");
664 }
665 log::info!("Trader started");
666 }
667
668 pub fn stop_trader(&mut self) {
674 disarm_shutdown_on_error();
675
676 if !self.trader.borrow().is_running() {
677 return;
678 }
679
680 log::info!("Stopping trader...");
681
682 if let Err(e) = self.trader.borrow_mut().stop() {
683 log::error!("Error stopping trader: {e}");
684 }
685 }
686
687 #[expect(
692 clippy::unused_async,
693 reason = "keeps the public async kernel API shape stable"
694 )]
695 pub async fn finalize_stop(&mut self) {
696 disarm_shutdown_on_error();
697
698 self.stop_engines();
701 self.cancel_timers();
702
703 let ts_shutdown = self.clock.borrow().timestamp_ns();
704
705 if let Some(event_store) = self.event_store.as_deref_mut() {
706 self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
707 event_store.seal(ts_shutdown);
708 }
709 self.ts_shutdown = Some(ts_shutdown);
710 log::info!("Stopped");
711 }
712
713 #[must_use]
719 pub fn event_store(&self) -> Option<&dyn KernelEventStore> {
720 self.event_store.as_deref()
721 }
722
723 #[must_use]
725 pub fn is_event_store_replay(&self) -> bool {
726 self.event_store_replay
727 }
728
729 #[must_use]
731 pub fn is_event_store_replay_configured(&self) -> bool {
732 self.event_store
733 .as_deref()
734 .is_some_and(KernelEventStore::is_event_store_replay_configured)
735 }
736
737 pub fn reset(&mut self) {
739 disarm_shutdown_on_error();
740 log::info!("Resetting");
741
742 if let Err(e) = self.trader.borrow_mut().reset() {
743 log::error!("Error resetting trader: {e:?}");
744 }
745
746 self.data_engine.borrow_mut().reset();
747 self.exec_engine.borrow_mut().reset();
748 self.risk_engine.borrow_mut().reset();
749 self.order_emulator.reset();
750 self.portfolio.borrow_mut().reset();
751
752 self.ts_started = None;
753 self.ts_shutdown = None;
754
755 log::info!("Reset");
756 }
757
758 pub fn dispose(&mut self) {
760 disarm_shutdown_on_error();
761 log::info!("Disposing");
762
763 if let Err(e) = self.trader.borrow_mut().dispose() {
764 log::error!("Error disposing trader: {e:?}");
765 }
766
767 self.stop_engines();
768 self.portfolio.borrow_mut().reset();
769 self.cancel_timers();
770
771 if let Some(event_store) = self.event_store.as_deref_mut() {
776 self.exec_engine.borrow_mut().set_snapshot_anchorer(None);
777 let ts_dispose = self.clock.borrow().timestamp_ns();
778 event_store.seal(ts_dispose);
779 }
780
781 self.data_engine.borrow_mut().dispose();
782 self.exec_engine.borrow_mut().dispose();
783 self.risk_engine.borrow_mut().dispose();
784 self.order_emulator.dispose();
785 self.cache.borrow_mut().dispose();
786 get_message_bus().borrow_mut().dispose();
787
788 log::info!("Disposed");
789 }
790
791 fn start_engines(&self) {
793 self.data_engine.borrow_mut().start();
794 self.exec_engine.borrow_mut().start();
795 self.risk_engine.borrow_mut().start();
796 }
797
798 fn stop_engines(&self) {
800 self.data_engine.borrow_mut().stop();
801 self.exec_engine.borrow_mut().stop();
802 self.risk_engine.borrow_mut().stop();
803 self.order_emulator.stop();
804 }
805
806 #[expect(clippy::await_holding_refcell_ref)] pub async fn connect_data_clients(&mut self) {
812 log::info!("Connecting data clients...");
813 self.data_engine.borrow_mut().connect().await;
814 }
815
816 #[expect(clippy::await_holding_refcell_ref)] pub async fn connect_exec_clients(&mut self) {
822 log::info!("Connecting execution clients...");
823 self.exec_engine.borrow_mut().connect().await;
824 }
825
826 #[expect(clippy::await_holding_refcell_ref)] pub async fn disconnect_clients(&mut self) -> anyhow::Result<()> {
833 log::info!("Disconnecting clients...");
834 self.data_engine.borrow_mut().disconnect().await?;
835 self.exec_engine.borrow_mut().disconnect().await?;
836 Ok(())
837 }
838
839 #[must_use]
841 pub fn check_engines_connected(&self) -> bool {
842 self.data_engine.borrow().check_connected() && self.exec_engine.borrow().check_connected()
843 }
844
845 #[must_use]
847 pub fn check_engines_disconnected(&self) -> bool {
848 self.data_engine.borrow().check_disconnected()
849 && self.exec_engine.borrow().check_disconnected()
850 }
851
852 #[must_use]
854 pub fn data_client_connection_status(&self) -> Vec<(ClientId, bool)> {
855 self.data_engine.borrow().client_connection_status()
856 }
857
858 #[must_use]
860 pub fn exec_client_connection_status(&self) -> Vec<(ClientId, bool)> {
861 self.exec_engine.borrow().client_connection_status()
862 }
863}
864
865#[cfg(all(test, feature = "python"))]
866mod tests {
867 use nautilus_common::messages::system::ShutdownSystem;
868 use nautilus_core::UUID4;
869 use rstest::*;
870 use ustr::Ustr;
871
872 use super::*;
873 use crate::builder::NautilusKernelBuilder;
874
875 #[rstest]
876 fn test_shutdown_system_sets_kernel_flag() {
877 let kernel = NautilusKernelBuilder::default().build().unwrap();
878 assert!(!kernel.is_shutdown_requested());
879
880 let command = ShutdownSystem::new(
881 kernel.trader_id(),
882 Ustr::from("TestComponent"),
883 Some("unit test".to_string()),
884 UUID4::new(),
885 kernel.generate_timestamp_ns(),
886 None, );
888
889 msgbus::publish_any(
890 MessagingSwitchboard::shutdown_system_topic(),
891 command.as_any(),
892 );
893 assert!(kernel.is_shutdown_requested());
894
895 kernel.reset_shutdown_flag();
896 assert!(!kernel.is_shutdown_requested());
897 }
898
899 #[rstest]
900 fn test_shutdown_system_idempotent() {
901 let kernel = NautilusKernelBuilder::default().build().unwrap();
902
903 let make_cmd = || {
904 ShutdownSystem::new(
905 kernel.trader_id(),
906 Ustr::from("TestComponent"),
907 None,
908 UUID4::new(),
909 kernel.generate_timestamp_ns(),
910 None, )
912 };
913
914 let topic = MessagingSwitchboard::shutdown_system_topic();
915 msgbus::publish_any(topic, make_cmd().as_any());
916 assert!(kernel.is_shutdown_requested());
917
918 msgbus::publish_any(topic, make_cmd().as_any());
919 assert!(kernel.is_shutdown_requested());
920
921 kernel.reset_shutdown_flag();
922 assert!(!kernel.is_shutdown_requested());
923
924 msgbus::publish_any(topic, make_cmd().as_any());
925 assert!(kernel.is_shutdown_requested());
926 }
927
928 #[rstest]
929 fn test_shutdown_system_ignores_other_trader() {
930 let kernel = NautilusKernelBuilder::default().build().unwrap();
931
932 let command = ShutdownSystem::new(
933 TraderId::from("OTHER-TRADER"),
934 Ustr::from("TestComponent"),
935 None,
936 UUID4::new(),
937 kernel.generate_timestamp_ns(),
938 None, );
940
941 msgbus::publish_any(
942 MessagingSwitchboard::shutdown_system_topic(),
943 command.as_any(),
944 );
945 assert!(!kernel.is_shutdown_requested());
946 }
947}
948
949#[cfg(test)]
950mod lifecycle_tests {
951 use nautilus_common::{
952 messages::data::{DataCommand, SubscribeCommand, UnsubscribeCommand},
953 msgbus::stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
954 };
955 use nautilus_model::{
956 enums::{OrderSide, OrderStatus, OrderType, TriggerType},
957 identifiers::ClientOrderId,
958 instruments::{
959 CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
960 },
961 orders::{Order, OrderAny, OrderTestBuilder},
962 types::{Price, Quantity},
963 };
964 use rstest::rstest;
965 use ustr::Ustr;
966
967 use super::*;
968 use crate::builder::NautilusKernelBuilder;
969
970 fn create_stop_market_order(instrument: &CryptoPerpetual, client_order_id: &str) -> OrderAny {
971 OrderTestBuilder::new(OrderType::StopMarket)
972 .instrument_id(instrument.id())
973 .client_order_id(ClientOrderId::from(client_order_id))
974 .side(OrderSide::Buy)
975 .trigger_price(Price::from("5100.00"))
976 .quantity(Quantity::from(1))
977 .emulation_trigger(TriggerType::BidAsk)
978 .build()
979 }
980
981 fn register_data_command_handler(id: &str) -> TypedIntoMessageSavingHandler<DataCommand> {
982 let (handler, saving_handler) =
983 get_typed_into_message_saving_handler::<DataCommand>(Some(Ustr::from(id)));
984 msgbus::register_data_command_endpoint(
985 MessagingSwitchboard::data_engine_queue_execute(),
986 handler,
987 );
988 saving_handler
989 }
990
991 #[rstest]
992 fn test_start_trader_starts_order_emulator_for_cached_emulated_orders() {
993 let mut kernel = NautilusKernelBuilder::default().build().unwrap();
994 let data_commands = register_data_command_handler("DataEngine.queue_execute.kernel_start");
995 let instrument = crypto_perpetual_ethusdt();
996 let instrument_id = instrument.id();
997 let first_order = create_stop_market_order(&instrument, "O-KERNEL-001");
998 let second_order = create_stop_market_order(&instrument, "O-KERNEL-002");
999 let first_client_order_id = first_order.client_order_id();
1000 let second_client_order_id = second_order.client_order_id();
1001 kernel
1002 .cache
1003 .borrow_mut()
1004 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
1005 .unwrap();
1006 kernel
1007 .cache
1008 .borrow_mut()
1009 .add_order(first_order, None, None, false)
1010 .unwrap();
1011 kernel
1012 .cache
1013 .borrow_mut()
1014 .add_order(second_order, None, None, false)
1015 .unwrap();
1016
1017 kernel.start();
1018 assert!(
1019 kernel
1020 .order_emulator
1021 .get_emulator()
1022 .get_matching_core(&instrument_id)
1023 .is_none()
1024 );
1025 kernel.start_trader();
1026
1027 let commands = data_commands.get_messages();
1028 let cache = kernel.cache.borrow();
1029 let first_status = cache.order(&first_client_order_id).unwrap().status();
1030 let second_status = cache.order(&second_client_order_id).unwrap().status();
1031 drop(cache);
1032 let emulator = kernel.order_emulator.get_emulator();
1033 assert!(emulator.get_matching_core(&instrument_id).is_some());
1034 assert_eq!(emulator.subscribed_quotes(), vec![instrument_id]);
1035 assert_eq!(first_status, OrderStatus::Emulated);
1036 assert_eq!(second_status, OrderStatus::Emulated);
1037 assert!(commands.iter().any(|command| matches!(
1038 command,
1039 DataCommand::Subscribe(SubscribeCommand::Quotes(command))
1040 if command.instrument_id == instrument_id
1041 )));
1042
1043 data_commands.clear();
1044 drop(emulator);
1045 kernel.stop_trader();
1046 kernel.dispose();
1047
1048 let commands = data_commands.get_messages();
1049 let emulator = kernel.order_emulator.get_emulator();
1050 assert!(emulator.subscribed_quotes().is_empty());
1051 assert!(emulator.get_matching_core(&instrument_id).is_none());
1052 assert!(commands.iter().any(|command| matches!(
1053 command,
1054 DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
1055 if command.instrument_id == instrument_id
1056 )));
1057 }
1058
1059 #[rstest]
1060 fn test_reset_resets_order_emulator_state() {
1061 let mut kernel = NautilusKernelBuilder::default().build().unwrap();
1062 let data_commands = register_data_command_handler("DataEngine.queue_execute.kernel_reset");
1063 let instrument = crypto_perpetual_ethusdt();
1064 let instrument_id = instrument.id();
1065 let order = create_stop_market_order(&instrument, "O-KERNEL-RESET-001");
1066 kernel
1067 .cache
1068 .borrow_mut()
1069 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
1070 .unwrap();
1071 kernel
1072 .cache
1073 .borrow_mut()
1074 .add_order(order, None, None, false)
1075 .unwrap();
1076
1077 kernel.start();
1078 kernel.start_trader();
1079 assert!(
1080 kernel
1081 .order_emulator
1082 .get_emulator()
1083 .get_matching_core(&instrument_id)
1084 .is_some()
1085 );
1086 kernel.stop_trader();
1087 data_commands.clear();
1088
1089 kernel.reset();
1090
1091 let commands = data_commands.get_messages();
1092 let emulator = kernel.order_emulator.get_emulator();
1093 assert!(emulator.subscribed_quotes().is_empty());
1094 assert!(emulator.get_matching_core(&instrument_id).is_none());
1095 assert!(commands.iter().any(|command| matches!(
1096 command,
1097 DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
1098 if command.instrument_id == instrument_id
1099 )));
1100
1101 drop(emulator);
1102 kernel.dispose();
1103 }
1104}