1use std::{cell::RefCell, fmt::Debug, rc::Rc, time::Duration};
17
18use nautilus_common::{
19 cache::{CacheConfig, database::CacheDatabaseAdapter},
20 clock::Clock,
21 enums::Environment,
22 logging::logger::LoggerConfig,
23 msgbus::{
24 MessageBusBackingFactory, MessageBusConfig, MessageBusExternalEgress,
25 external_egress_from_backing,
26 },
27};
28use nautilus_core::UUID4;
29use nautilus_data::engine::config::DataEngineConfig;
30use nautilus_execution::engine::config::ExecutionEngineConfig;
31use nautilus_model::identifiers::TraderId;
32use nautilus_portfolio::config::PortfolioConfig;
33use nautilus_risk::engine::config::RiskEngineConfig;
34
35use crate::{
36 clock_factory::ClockFactory,
37 config::KernelConfig,
38 event_store::{EventStoreFactory, KernelEventStore},
39 kernel::{NautilusKernel, NautilusKernelDependencies},
40};
41
42pub struct NautilusKernelBuilder {
47 name: String,
48 trader_id: TraderId,
49 environment: Environment,
50 instance_id: Option<UUID4>,
51 load_state: bool,
52 save_state: bool,
53 shutdown_on_error: bool,
54 logging: Option<LoggerConfig>,
55 timeout_connection: Duration,
56 timeout_reconciliation: Duration,
57 timeout_portfolio: Duration,
58 timeout_disconnection: Duration,
59 delay_post_stop: Duration,
60 timeout_shutdown: Duration,
61 clock_factory: Option<ClockFactory>,
62 cache: Option<CacheConfig>,
63 cache_database: Option<Box<dyn CacheDatabaseAdapter>>,
64 data_engine: Option<DataEngineConfig>,
65 risk_engine: Option<RiskEngineConfig>,
66 exec_engine: Option<ExecutionEngineConfig>,
67 portfolio: Option<PortfolioConfig>,
68 msgbus: Option<MessageBusConfig>,
69 event_store_factory: Option<EventStoreFactory>,
70 external_msgbus_factory: Option<Box<dyn MessageBusBackingFactory>>,
71 external_msgbus_egress: Option<Box<dyn MessageBusExternalEgress>>,
72}
73
74impl Debug for NautilusKernelBuilder {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct(stringify!(NautilusKernelBuilder))
77 .field("name", &self.name)
78 .field("trader_id", &self.trader_id)
79 .field("environment", &self.environment)
80 .field("instance_id", &self.instance_id)
81 .field("load_state", &self.load_state)
82 .field("save_state", &self.save_state)
83 .field("shutdown_on_error", &self.shutdown_on_error)
84 .field("logging", &self.logging)
85 .field("timeout_connection", &self.timeout_connection)
86 .field("timeout_reconciliation", &self.timeout_reconciliation)
87 .field("timeout_portfolio", &self.timeout_portfolio)
88 .field("timeout_disconnection", &self.timeout_disconnection)
89 .field("delay_post_stop", &self.delay_post_stop)
90 .field("timeout_shutdown", &self.timeout_shutdown)
91 .field("clock_factory", &self.clock_factory.is_some())
92 .field("cache", &self.cache)
93 .field("cache_database", &self.cache_database.is_some())
94 .field("data_engine", &self.data_engine)
95 .field("risk_engine", &self.risk_engine)
96 .field("exec_engine", &self.exec_engine)
97 .field("portfolio", &self.portfolio)
98 .field("msgbus", &self.msgbus)
99 .field("event_store_factory", &self.event_store_factory.is_some())
100 .field(
101 "external_msgbus_factory",
102 &self.external_msgbus_factory.is_some(),
103 )
104 .field(
105 "external_msgbus_egress",
106 &self.external_msgbus_egress.is_some(),
107 )
108 .finish_non_exhaustive()
109 }
110}
111
112impl NautilusKernelBuilder {
113 #[must_use]
115 pub const fn new(name: String, trader_id: TraderId, environment: Environment) -> Self {
116 Self {
117 name,
118 trader_id,
119 environment,
120 instance_id: None,
121 load_state: true,
122 save_state: true,
123 shutdown_on_error: false,
124 logging: None,
125 timeout_connection: Duration::from_mins(1),
126 timeout_reconciliation: Duration::from_secs(30),
127 timeout_portfolio: Duration::from_secs(10),
128 timeout_disconnection: Duration::from_secs(10),
129 delay_post_stop: Duration::from_secs(10),
130 timeout_shutdown: Duration::from_secs(5),
131 clock_factory: None,
132 cache: None,
133 cache_database: None,
134 data_engine: None,
135 risk_engine: None,
136 exec_engine: None,
137 portfolio: None,
138 msgbus: None,
139 event_store_factory: None,
140 external_msgbus_factory: None,
141 external_msgbus_egress: None,
142 }
143 }
144
145 #[must_use]
147 pub const fn with_instance_id(mut self, instance_id: UUID4) -> Self {
148 self.instance_id = Some(instance_id);
149 self
150 }
151
152 #[must_use]
154 pub const fn with_load_state(mut self, load_state: bool) -> Self {
155 self.load_state = load_state;
156 self
157 }
158
159 #[must_use]
161 pub const fn with_save_state(mut self, save_state: bool) -> Self {
162 self.save_state = save_state;
163 self
164 }
165
166 #[must_use]
170 pub const fn with_shutdown_on_error(mut self, shutdown_on_error: bool) -> Self {
171 self.shutdown_on_error = shutdown_on_error;
172 self
173 }
174
175 #[must_use]
177 pub fn with_logging_config(mut self, config: LoggerConfig) -> Self {
178 self.logging = Some(config);
179 self
180 }
181
182 #[must_use]
184 pub const fn with_timeout_connection(mut self, timeout_secs: u64) -> Self {
185 self.timeout_connection = Duration::from_secs(timeout_secs);
186 self
187 }
188
189 #[must_use]
191 pub const fn with_timeout_reconciliation(mut self, timeout_secs: u64) -> Self {
192 self.timeout_reconciliation = Duration::from_secs(timeout_secs);
193 self
194 }
195
196 #[must_use]
198 pub const fn with_timeout_portfolio(mut self, timeout_secs: u64) -> Self {
199 self.timeout_portfolio = Duration::from_secs(timeout_secs);
200 self
201 }
202
203 #[must_use]
205 pub const fn with_timeout_disconnection(mut self, timeout_secs: u64) -> Self {
206 self.timeout_disconnection = Duration::from_secs(timeout_secs);
207 self
208 }
209
210 #[must_use]
212 pub const fn with_delay_post_stop(mut self, delay_secs: u64) -> Self {
213 self.delay_post_stop = Duration::from_secs(delay_secs);
214 self
215 }
216
217 #[must_use]
219 pub const fn with_timeout_shutdown(mut self, timeout_secs: u64) -> Self {
220 self.timeout_shutdown = Duration::from_secs(timeout_secs);
221 self
222 }
223
224 #[must_use]
230 pub fn with_clock_factory<F>(mut self, factory: F) -> Self
231 where
232 F: Fn() -> Rc<RefCell<dyn Clock>> + 'static,
233 {
234 self.clock_factory = Some(ClockFactory::new(factory));
235 self
236 }
237
238 #[must_use]
240 pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
241 self.cache = Some(config);
242 self
243 }
244
245 #[must_use]
253 pub fn with_cache_database(mut self, adapter: Box<dyn CacheDatabaseAdapter>) -> Self {
254 self.cache_database = Some(adapter);
255 self
256 }
257
258 #[must_use]
260 pub fn with_data_engine_config(mut self, config: DataEngineConfig) -> Self {
261 self.data_engine = Some(config);
262 self
263 }
264
265 #[must_use]
267 pub fn with_risk_engine_config(mut self, config: RiskEngineConfig) -> Self {
268 self.risk_engine = Some(config);
269 self
270 }
271
272 #[must_use]
274 pub fn with_exec_engine_config(mut self, config: ExecutionEngineConfig) -> Self {
275 self.exec_engine = Some(config);
276 self
277 }
278
279 #[must_use]
281 pub const fn with_portfolio_config(mut self, config: PortfolioConfig) -> Self {
282 self.portfolio = Some(config);
283 self
284 }
285
286 #[must_use]
288 pub fn with_msgbus_config(mut self, config: MessageBusConfig) -> Self {
289 self.msgbus = Some(config);
290 self
291 }
292
293 #[must_use]
301 pub fn with_event_store<F>(mut self, factory: F) -> Self
302 where
303 F: FnOnce(UUID4, Rc<RefCell<dyn Clock>>) -> anyhow::Result<Box<dyn KernelEventStore>>
304 + 'static,
305 {
306 self.event_store_factory = Some(Box::new(factory));
307 self
308 }
309
310 #[must_use]
312 pub fn with_external_msgbus_egress(
313 mut self,
314 external_egress: Box<dyn MessageBusExternalEgress>,
315 ) -> Self {
316 self.external_msgbus_egress = Some(external_egress);
317 self
318 }
319
320 #[must_use]
322 pub fn with_external_msgbus_factory(
323 mut self,
324 factory: Box<dyn MessageBusBackingFactory>,
325 ) -> Self {
326 self.external_msgbus_factory = Some(factory);
327 self
328 }
329
330 pub fn build(self) -> anyhow::Result<NautilusKernel> {
336 if self.external_msgbus_factory.is_some() && self.external_msgbus_egress.is_some() {
337 anyhow::bail!("external message bus factory cannot be combined with injected egress");
338 }
339
340 if self.external_msgbus_factory.is_some()
341 && self
342 .msgbus
343 .as_ref()
344 .and_then(|config| config.external_streams.as_ref())
345 .is_some_and(|streams| !streams.is_empty())
346 {
347 anyhow::bail!(
348 "NautilusKernelBuilder cannot consume external message bus streams; \
349 use LiveNodeBuilder::with_external_msgbus_factory for ingress"
350 );
351 }
352
353 let config = KernelConfig {
354 environment: self.environment,
355 trader_id: self.trader_id,
356 load_state: self.load_state,
357 save_state: self.save_state,
358 shutdown_on_error: self.shutdown_on_error,
359 logging: self.logging.unwrap_or_default(),
360 instance_id: self.instance_id,
361 timeout_connection: self.timeout_connection,
362 timeout_reconciliation: self.timeout_reconciliation,
363 timeout_portfolio: self.timeout_portfolio,
364 timeout_disconnection: self.timeout_disconnection,
365 delay_post_stop: self.delay_post_stop,
366 timeout_shutdown: self.timeout_shutdown,
367 cache: self.cache,
368 msgbus: self.msgbus,
369 data_engine: self.data_engine,
370 risk_engine: self.risk_engine,
371 exec_engine: self.exec_engine,
372 portfolio: self.portfolio,
373 streaming: None,
374 };
375
376 let kernel = NautilusKernel::new_with_dependencies(
377 self.name,
378 config,
379 NautilusKernelDependencies::default()
380 .with_clock_factory(self.clock_factory)
381 .with_cache_database(self.cache_database)
382 .with_event_store_factory(self.event_store_factory),
383 )?;
384
385 let config = kernel.config.msgbus().unwrap_or_default();
386 let external_egress = if let Some(factory) = self.external_msgbus_factory {
387 config.validate()?;
388 let backing = factory.create(
389 kernel.config.trader_id(),
390 kernel.instance_id,
391 config.clone(),
392 )?;
393 Some(external_egress_from_backing(backing))
394 } else {
395 self.external_msgbus_egress
396 };
397
398 if let Some(external_egress) = external_egress {
399 nautilus_common::msgbus::get_message_bus()
400 .borrow_mut()
401 .set_external_egress_config(external_egress, &config)?;
402 }
403
404 Ok(kernel)
405 }
406}
407
408impl Default for NautilusKernelBuilder {
409 fn default() -> Self {
411 Self::new(
412 "NautilusKernel".to_string(),
413 TraderId::default(),
414 Environment::Backtest,
415 )
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use std::{
422 cell::Cell,
423 sync::{
424 Arc, Mutex,
425 atomic::{AtomicBool, Ordering},
426 },
427 };
428
429 use ahash::AHashMap;
430 use bytes::Bytes;
431 use nautilus_common::{
432 cache::{
433 Cache,
434 database::{CacheDatabaseAdapter, CacheMap},
435 },
436 clock::Clock,
437 msgbus::{BusMessage, MessageBusBacking, MessageBusBackingFactory},
438 signal::Signal,
439 };
440 use nautilus_core::UnixNanos;
441 use nautilus_execution::engine::SnapshotAnchorer;
442 use nautilus_model::{
443 accounts::AccountAny,
444 data::{
445 Bar, CustomData, DataType, FundingRateUpdate, QuoteTick, TradeTick,
446 greeks::{GreeksData, YieldCurveData},
447 },
448 events::{OrderEventAny, OrderSnapshot, position::snapshot::PositionSnapshot},
449 identifiers::{
450 AccountId, ClientId, ClientOrderId, ComponentId, InstrumentId, PositionId, StrategyId,
451 TraderId, VenueOrderId,
452 },
453 instruments::{InstrumentAny, SyntheticInstrument},
454 orderbook::OrderBook,
455 orders::OrderAny,
456 position::Position,
457 types::{Currency, Money},
458 };
459 use rstest::*;
460 use ustr::Ustr;
461
462 use super::*;
463 use crate::event_store::RegisteredComponents;
464
465 #[rstest]
466 fn test_builder_default() {
467 let builder = NautilusKernelBuilder::default();
468 assert_eq!(builder.name, "NautilusKernel");
469 assert_eq!(builder.environment, Environment::Backtest);
470 assert!(builder.load_state);
471 assert!(builder.save_state);
472 }
473
474 #[rstest]
475 fn test_builder_fluent_api() {
476 let trader_id = TraderId::from("TRADER-001");
477 let instance_id = UUID4::new();
478
479 let builder =
480 NautilusKernelBuilder::new("TestKernel".to_string(), trader_id, Environment::Live)
481 .with_instance_id(instance_id)
482 .with_load_state(false)
483 .with_save_state(false)
484 .with_timeout_connection(30);
485
486 assert_eq!(builder.name, "TestKernel");
487 assert_eq!(builder.trader_id, trader_id);
488 assert_eq!(builder.environment, Environment::Live);
489 assert_eq!(builder.instance_id, Some(instance_id));
490 assert!(!builder.load_state);
491 assert!(!builder.save_state);
492 assert_eq!(builder.timeout_connection, Duration::from_secs(30));
493 }
494
495 #[cfg(feature = "python")]
496 #[rstest]
497 fn test_builder_build() {
498 let result = NautilusKernelBuilder::default().build();
499 assert!(result.is_ok());
500
501 let kernel = result.unwrap();
502 assert_eq!(kernel.name(), "NautilusKernel".to_string());
503 assert_eq!(kernel.environment(), Environment::Backtest);
504 }
505
506 #[rstest]
507 fn test_builder_with_configs() {
508 let cache_config = CacheConfig::default();
509 let data_engine_config = DataEngineConfig::default();
510
511 let builder = NautilusKernelBuilder::default()
512 .with_cache_config(cache_config)
513 .with_data_engine_config(data_engine_config);
514
515 assert!(builder.cache.is_some());
516 assert!(builder.data_engine.is_some());
517 }
518
519 #[rstest]
520 fn test_builder_with_cache_database() {
521 let builder = NautilusKernelBuilder::default().with_cache_database(Box::new(NoopAdapter));
522
523 assert!(builder.cache_database.is_some());
524 }
525
526 #[rstest]
527 fn test_builder_with_external_msgbus_egress_forwards_published_quote() {
528 let (external_egress, publications, closed) = CapturingExternalEgress::new();
529 let kernel = NautilusKernelBuilder::default()
530 .with_external_msgbus_egress(Box::new(external_egress))
531 .build()
532 .expect("kernel builds with external message bus egress");
533 let quote = QuoteTick::default();
534
535 nautilus_common::msgbus::publish_quote("data.quotes.TEST".into(), "e);
536
537 let publications = publications.borrow();
538 assert_eq!(publications.len(), 1);
539 assert_eq!(publications[0].topic, "data.quotes.TEST");
540 assert_eq!(
541 serde_json::from_slice::<QuoteTick>(&publications[0].payload)
542 .expect("JSON payload must decode as QuoteTick"),
543 quote
544 );
545 drop(publications);
546
547 nautilus_common::msgbus::get_message_bus()
548 .borrow_mut()
549 .dispose();
550 assert!(closed.get());
551 drop(kernel);
552 }
553
554 #[rstest]
555 fn test_builder_with_external_msgbus_factory_forwards_published_quote() {
556 let publications = Arc::new(Mutex::new(Vec::new()));
557 let closed = Arc::new(AtomicBool::new(false));
558 let factory = CapturingBackingFactory {
559 publications: publications.clone(),
560 closed: closed.clone(),
561 };
562 let kernel = NautilusKernelBuilder::default()
563 .with_external_msgbus_factory(Box::new(factory))
564 .build()
565 .expect("kernel builds with external message bus factory");
566 let quote = QuoteTick::default();
567
568 nautilus_common::msgbus::publish_quote("data.quotes.TEST".into(), "e);
569
570 let publications = publications.lock().unwrap();
571 assert_eq!(publications.len(), 1);
572 assert_eq!(publications[0].topic, "data.quotes.TEST");
573 assert_eq!(
574 serde_json::from_slice::<QuoteTick>(&publications[0].payload)
575 .expect("JSON payload must decode as QuoteTick"),
576 quote
577 );
578 drop(publications);
579
580 nautilus_common::msgbus::get_message_bus()
581 .borrow_mut()
582 .dispose();
583 assert!(closed.load(Ordering::Relaxed));
584 drop(kernel);
585 }
586
587 #[rstest]
588 fn test_builder_with_external_msgbus_factory_rejects_external_streams() {
589 let factory = CapturingBackingFactory {
590 publications: Arc::new(Mutex::new(Vec::new())),
591 closed: Arc::new(AtomicBool::new(false)),
592 };
593 let config = MessageBusConfig {
594 external_streams: Some(vec!["stream".to_string()]),
595 ..Default::default()
596 };
597
598 let error = NautilusKernelBuilder::default()
599 .with_msgbus_config(config)
600 .with_external_msgbus_factory(Box::new(factory))
601 .build()
602 .expect_err("system builder should reject external ingress streams");
603
604 assert!(
605 error
606 .to_string()
607 .contains("cannot consume external message bus streams")
608 );
609 }
610
611 #[rstest]
612 fn test_builder_with_external_msgbus_factory_rejects_injected_egress() {
613 let factory = CapturingBackingFactory {
614 publications: Arc::new(Mutex::new(Vec::new())),
615 closed: Arc::new(AtomicBool::new(false)),
616 };
617 let (external_egress, _publications, _closed) = CapturingExternalEgress::new();
618
619 let error = NautilusKernelBuilder::default()
620 .with_external_msgbus_factory(Box::new(factory))
621 .with_external_msgbus_egress(Box::new(external_egress))
622 .build()
623 .expect_err("system builder should reject factory plus injected egress");
624
625 assert!(
626 error
627 .to_string()
628 .contains("cannot be combined with injected egress")
629 );
630 }
631
632 #[rstest]
633 fn test_builder_default_has_no_event_store() {
634 let kernel = NautilusKernelBuilder::default()
635 .build()
636 .expect("kernel builds without an event store");
637
638 assert!(kernel.event_store().is_none());
639 }
640
641 #[rstest]
642 fn test_builder_with_event_store_invokes_factory_with_kernel_args() {
643 type FactoryArgs = (UUID4, Rc<RefCell<dyn Clock>>);
644
645 let known_id = UUID4::new();
646 let captured: Rc<RefCell<Option<FactoryArgs>>> = Rc::new(RefCell::new(None));
647 let captured_for_closure = captured.clone();
648
649 let kernel = NautilusKernelBuilder::default()
650 .with_instance_id(known_id)
651 .with_event_store(move |instance_id, clock| {
652 *captured_for_closure.borrow_mut() = Some((instance_id, clock));
653 Ok(Box::new(NoopKernelEventStore))
654 })
655 .build()
656 .expect("kernel");
657
658 let (received_id, received_clock) =
659 captured.borrow_mut().take().expect("factory invoked once");
660
661 assert_eq!(
662 received_id, known_id,
663 "factory must receive kernel instance_id"
664 );
665 assert!(
666 Rc::ptr_eq(&received_clock, &kernel.clock()),
667 "factory must receive the kernel's clock Rc, not a fresh allocation",
668 );
669 }
670
671 #[cfg(feature = "live")]
672 #[rstest]
673 fn test_builder_with_clock_factory_drives_kernel_and_component_clocks() {
674 use nautilus_common::clock::TestClock;
675
676 let calls = Rc::new(Cell::new(0usize));
677 let calls_in_closure = calls.clone();
678
679 let kernel = NautilusKernelBuilder::new(
680 "ClockFactoryKernel".to_string(),
681 TraderId::from("TRADER-CF"),
682 Environment::Live,
683 )
684 .with_clock_factory(move || {
685 calls_in_closure.set(calls_in_closure.get() + 1);
686 Rc::new(RefCell::new(TestClock::new())) as Rc<RefCell<dyn Clock>>
687 })
688 .build()
689 .expect("kernel builds with clock factory");
690
691 assert_eq!(
692 calls.get(),
693 1,
694 "kernel clock must consume exactly one factory call"
695 );
696 assert!(
697 (*kernel.clock().borrow()).as_any().is::<TestClock>(),
698 "kernel clock must be the factory-produced TestClock"
699 );
700
701 let c1 = kernel
702 .trader()
703 .borrow_mut()
704 .create_component_clock(ComponentId::new("COMP-1"));
705 let c2 = kernel
706 .trader()
707 .borrow_mut()
708 .create_component_clock(ComponentId::new("COMP-2"));
709
710 assert_eq!(
711 calls.get(),
712 3,
713 "factory must back kernel clock and each component clock"
714 );
715 assert!((*c1.borrow()).as_any().is::<TestClock>());
716 assert!((*c2.borrow()).as_any().is::<TestClock>());
717 }
718
719 #[cfg(feature = "live")]
720 #[rstest]
721 fn test_builder_without_clock_factory_uses_live_clock_default() {
722 use nautilus_common::live::clock::LiveClock; let kernel = NautilusKernelBuilder::new(
725 "DefaultClockKernel".to_string(),
726 TraderId::from("TRADER-DC"),
727 Environment::Live,
728 )
729 .build()
730 .expect("kernel builds without a clock factory");
731
732 assert!(
733 (*kernel.clock().borrow()).as_any().is::<LiveClock>(),
734 "no factory uses LiveClock::default for the kernel clock"
735 );
736
737 let comp = kernel
738 .trader()
739 .borrow_mut()
740 .create_component_clock(ComponentId::new("COMP-D"));
741 assert!(
742 (*comp.borrow()).as_any().is::<LiveClock>(),
743 "no factory uses LiveClock::default for component clocks"
744 );
745 }
746
747 #[rstest]
748 fn test_builder_with_event_store_propagates_factory_error() {
749 let result = NautilusKernelBuilder::default()
750 .with_event_store(|_instance_id, _clock| Err(anyhow::anyhow!("factory boom")))
751 .build();
752
753 let err = result.expect_err("factory error must surface from build()");
754
755 assert!(
756 err.to_string().contains("factory boom"),
757 "error must propagate the factory's message; got: {err}",
758 );
759 }
760
761 #[rstest]
762 fn test_builder_with_all_engine_configs() {
763 let builder = NautilusKernelBuilder::default()
764 .with_data_engine_config(DataEngineConfig::default())
765 .with_risk_engine_config(RiskEngineConfig::default())
766 .with_exec_engine_config(ExecutionEngineConfig::default())
767 .with_portfolio_config(PortfolioConfig::default());
768
769 assert!(builder.data_engine.is_some());
770 assert!(builder.risk_engine.is_some());
771 assert!(builder.exec_engine.is_some());
772 assert!(builder.portfolio.is_some());
773 }
774
775 #[rstest]
776 fn test_builder_with_all_timeouts() {
777 let builder = NautilusKernelBuilder::default()
778 .with_timeout_connection(10)
779 .with_timeout_reconciliation(20)
780 .with_timeout_portfolio(30)
781 .with_timeout_disconnection(40)
782 .with_delay_post_stop(50)
783 .with_timeout_shutdown(60);
784
785 assert_eq!(builder.timeout_connection, Duration::from_secs(10));
786 assert_eq!(builder.timeout_reconciliation, Duration::from_secs(20));
787 assert_eq!(builder.timeout_portfolio, Duration::from_secs(30));
788 assert_eq!(builder.timeout_disconnection, Duration::from_secs(40));
789 assert_eq!(builder.delay_post_stop, Duration::from_secs(50));
790 assert_eq!(builder.timeout_shutdown, Duration::from_mins(1));
791 }
792
793 #[rstest]
794 fn test_builder_default_timeouts() {
795 let builder = NautilusKernelBuilder::default();
796
797 assert_eq!(builder.timeout_connection, Duration::from_mins(1));
798 assert_eq!(builder.timeout_reconciliation, Duration::from_secs(30));
799 assert_eq!(builder.timeout_portfolio, Duration::from_secs(10));
800 assert_eq!(builder.timeout_disconnection, Duration::from_secs(10));
801 assert_eq!(builder.delay_post_stop, Duration::from_secs(10));
802 assert_eq!(builder.timeout_shutdown, Duration::from_secs(5));
803 }
804
805 #[derive(Debug)]
806 struct CapturedEgressMessage {
807 topic: String,
808 payload: Bytes,
809 }
810
811 type CapturedEgressMessages = Rc<RefCell<Vec<CapturedEgressMessage>>>;
812 type SharedClosed = Rc<Cell<bool>>;
813
814 struct CapturingExternalEgress {
815 publications: CapturedEgressMessages,
816 closed: SharedClosed,
817 }
818
819 impl CapturingExternalEgress {
820 fn new() -> (Self, CapturedEgressMessages, SharedClosed) {
821 let publications = Rc::new(RefCell::new(Vec::new()));
822 let closed = Rc::new(Cell::new(false));
823 (
824 Self {
825 publications: publications.clone(),
826 closed: closed.clone(),
827 },
828 publications,
829 closed,
830 )
831 }
832 }
833
834 impl MessageBusExternalEgress for CapturingExternalEgress {
835 fn is_closed(&self) -> bool {
836 self.closed.get()
837 }
838
839 fn publish(&self, message: BusMessage) {
840 self.publications.borrow_mut().push(CapturedEgressMessage {
841 topic: message.topic.to_string(),
842 payload: message.payload,
843 });
844 }
845
846 fn close(&mut self) {
847 self.closed.set(true);
848 }
849 }
850
851 #[derive(Debug)]
852 struct CapturingBackingFactory {
853 publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
854 closed: Arc<AtomicBool>,
855 }
856
857 impl MessageBusBackingFactory for CapturingBackingFactory {
858 fn create(
859 &self,
860 _trader_id: TraderId,
861 _instance_id: UUID4,
862 _config: MessageBusConfig,
863 ) -> anyhow::Result<Box<dyn MessageBusBacking>> {
864 Ok(Box::new(CapturingBacking {
865 publications: self.publications.clone(),
866 closed: self.closed.clone(),
867 }))
868 }
869 }
870
871 struct CapturingBacking {
872 publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
873 closed: Arc<AtomicBool>,
874 }
875
876 impl MessageBusBacking for CapturingBacking {
877 fn is_closed(&self) -> bool {
878 self.closed.load(Ordering::Relaxed)
879 }
880
881 fn publish(&self, message: BusMessage) {
882 self.publications
883 .lock()
884 .unwrap()
885 .push(CapturedEgressMessage {
886 topic: message.topic.to_string(),
887 payload: message.payload,
888 });
889 }
890
891 fn close(&mut self) {
892 self.closed.store(true, Ordering::Relaxed);
893 }
894 }
895
896 struct NoopAdapter;
897
898 #[async_trait::async_trait]
899 impl CacheDatabaseAdapter for NoopAdapter {
900 fn close(&mut self) -> anyhow::Result<()> {
901 Ok(())
902 }
903
904 fn flush(&mut self) -> anyhow::Result<()> {
905 Ok(())
906 }
907
908 async fn load_all(&self) -> anyhow::Result<CacheMap> {
909 Ok(CacheMap::default())
910 }
911
912 fn load(&self) -> anyhow::Result<AHashMap<String, Bytes>> {
913 Ok(AHashMap::new())
914 }
915
916 async fn load_currencies(&self) -> anyhow::Result<AHashMap<Ustr, Currency>> {
917 Ok(AHashMap::new())
918 }
919
920 async fn load_instruments(&self) -> anyhow::Result<AHashMap<InstrumentId, InstrumentAny>> {
921 Ok(AHashMap::new())
922 }
923
924 async fn load_synthetics(
925 &self,
926 ) -> anyhow::Result<AHashMap<InstrumentId, SyntheticInstrument>> {
927 Ok(AHashMap::new())
928 }
929
930 async fn load_accounts(&self) -> anyhow::Result<AHashMap<AccountId, AccountAny>> {
931 Ok(AHashMap::new())
932 }
933
934 async fn load_orders(&self) -> anyhow::Result<AHashMap<ClientOrderId, OrderAny>> {
935 Ok(AHashMap::new())
936 }
937
938 async fn load_positions(&self) -> anyhow::Result<AHashMap<PositionId, Position>> {
939 Ok(AHashMap::new())
940 }
941
942 fn load_index_order_position(&self) -> anyhow::Result<AHashMap<ClientOrderId, PositionId>> {
943 Ok(AHashMap::new())
944 }
945
946 fn load_index_order_client(&self) -> anyhow::Result<AHashMap<ClientOrderId, ClientId>> {
947 Ok(AHashMap::new())
948 }
949
950 async fn load_currency(&self, _code: &Ustr) -> anyhow::Result<Option<Currency>> {
951 Ok(None)
952 }
953
954 async fn load_instrument(
955 &self,
956 _instrument_id: &InstrumentId,
957 ) -> anyhow::Result<Option<InstrumentAny>> {
958 Ok(None)
959 }
960
961 async fn load_synthetic(
962 &self,
963 _instrument_id: &InstrumentId,
964 ) -> anyhow::Result<Option<SyntheticInstrument>> {
965 Ok(None)
966 }
967
968 async fn load_account(
969 &self,
970 _account_id: &AccountId,
971 ) -> anyhow::Result<Option<AccountAny>> {
972 Ok(None)
973 }
974
975 async fn load_order(
976 &self,
977 _client_order_id: &ClientOrderId,
978 ) -> anyhow::Result<Option<OrderAny>> {
979 Ok(None)
980 }
981
982 async fn load_position(
983 &self,
984 _position_id: &PositionId,
985 ) -> anyhow::Result<Option<Position>> {
986 Ok(None)
987 }
988
989 fn load_actor(
990 &self,
991 _component_id: &ComponentId,
992 ) -> anyhow::Result<AHashMap<String, Bytes>> {
993 Ok(AHashMap::new())
994 }
995
996 fn load_strategy(
997 &self,
998 _strategy_id: &StrategyId,
999 ) -> anyhow::Result<AHashMap<String, Bytes>> {
1000 Ok(AHashMap::new())
1001 }
1002
1003 fn load_signals(&self, _name: &str) -> anyhow::Result<Vec<Signal>> {
1004 Ok(Vec::new())
1005 }
1006
1007 fn load_custom_data(&self, _data_type: &DataType) -> anyhow::Result<Vec<CustomData>> {
1008 Ok(Vec::new())
1009 }
1010
1011 fn load_order_snapshot(
1012 &self,
1013 _client_order_id: &ClientOrderId,
1014 ) -> anyhow::Result<Option<OrderSnapshot>> {
1015 Ok(None)
1016 }
1017
1018 fn load_position_snapshot(
1019 &self,
1020 _position_id: &PositionId,
1021 ) -> anyhow::Result<Option<PositionSnapshot>> {
1022 Ok(None)
1023 }
1024
1025 fn load_quotes(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<QuoteTick>> {
1026 Ok(Vec::new())
1027 }
1028
1029 fn load_trades(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<TradeTick>> {
1030 Ok(Vec::new())
1031 }
1032
1033 fn load_funding_rates(
1034 &self,
1035 _instrument_id: &InstrumentId,
1036 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
1037 Ok(Vec::new())
1038 }
1039
1040 fn load_bars(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<Bar>> {
1041 Ok(Vec::new())
1042 }
1043
1044 fn add(&self, _key: String, _value: Bytes) -> anyhow::Result<()> {
1045 Ok(())
1046 }
1047
1048 fn add_currency(&self, _currency: &Currency) -> anyhow::Result<()> {
1049 Ok(())
1050 }
1051
1052 fn add_instrument(&self, _instrument: &InstrumentAny) -> anyhow::Result<()> {
1053 Ok(())
1054 }
1055
1056 fn add_synthetic(&self, _synthetic: &SyntheticInstrument) -> anyhow::Result<()> {
1057 Ok(())
1058 }
1059
1060 fn add_account(&self, _account: &AccountAny) -> anyhow::Result<()> {
1061 Ok(())
1062 }
1063
1064 fn add_order(&self, _order: &OrderAny, _client_id: Option<ClientId>) -> anyhow::Result<()> {
1065 Ok(())
1066 }
1067
1068 fn add_order_snapshot(&self, _snapshot: &OrderSnapshot) -> anyhow::Result<()> {
1069 Ok(())
1070 }
1071
1072 fn add_position(&self, _position: &Position) -> anyhow::Result<()> {
1073 Ok(())
1074 }
1075
1076 fn add_position_snapshot(&self, _snapshot: &PositionSnapshot) -> anyhow::Result<()> {
1077 Ok(())
1078 }
1079
1080 fn add_order_book(&self, _order_book: &OrderBook) -> anyhow::Result<()> {
1081 Ok(())
1082 }
1083
1084 fn add_signal(&self, _signal: &Signal) -> anyhow::Result<()> {
1085 Ok(())
1086 }
1087
1088 fn add_custom_data(&self, _data: &CustomData) -> anyhow::Result<()> {
1089 Ok(())
1090 }
1091
1092 fn add_quote(&self, _quote: &QuoteTick) -> anyhow::Result<()> {
1093 Ok(())
1094 }
1095
1096 fn add_trade(&self, _trade: &TradeTick) -> anyhow::Result<()> {
1097 Ok(())
1098 }
1099
1100 fn add_funding_rate(&self, _funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
1101 Ok(())
1102 }
1103
1104 fn add_bar(&self, _bar: &Bar) -> anyhow::Result<()> {
1105 Ok(())
1106 }
1107
1108 fn add_greeks(&self, _greeks: &GreeksData) -> anyhow::Result<()> {
1109 Ok(())
1110 }
1111
1112 fn add_yield_curve(&self, _yield_curve: &YieldCurveData) -> anyhow::Result<()> {
1113 Ok(())
1114 }
1115
1116 fn delete_actor(&self, _component_id: &ComponentId) -> anyhow::Result<()> {
1117 Ok(())
1118 }
1119
1120 fn delete_strategy(&self, _component_id: &StrategyId) -> anyhow::Result<()> {
1121 Ok(())
1122 }
1123
1124 fn delete_order(&self, _client_order_id: &ClientOrderId) -> anyhow::Result<()> {
1125 Ok(())
1126 }
1127
1128 fn delete_position(&self, _position_id: &PositionId) -> anyhow::Result<()> {
1129 Ok(())
1130 }
1131
1132 fn delete_account_event(
1133 &self,
1134 _account_id: &AccountId,
1135 _event_id: &str,
1136 ) -> anyhow::Result<()> {
1137 Ok(())
1138 }
1139
1140 fn index_venue_order_id(
1141 &self,
1142 _client_order_id: ClientOrderId,
1143 _venue_order_id: VenueOrderId,
1144 ) -> anyhow::Result<()> {
1145 Ok(())
1146 }
1147
1148 fn index_order_position(
1149 &self,
1150 _client_order_id: ClientOrderId,
1151 _position_id: PositionId,
1152 ) -> anyhow::Result<()> {
1153 Ok(())
1154 }
1155
1156 fn update_actor(
1157 &self,
1158 _component_id: &ComponentId,
1159 _state: &AHashMap<String, Bytes>,
1160 ) -> anyhow::Result<()> {
1161 Ok(())
1162 }
1163
1164 fn update_strategy(
1165 &self,
1166 _strategy_id: &StrategyId,
1167 _state: &AHashMap<String, Bytes>,
1168 ) -> anyhow::Result<()> {
1169 Ok(())
1170 }
1171
1172 fn update_account(&self, _account: &AccountAny) -> anyhow::Result<()> {
1173 Ok(())
1174 }
1175
1176 fn update_order(&self, _order_event: &OrderEventAny) -> anyhow::Result<()> {
1177 Ok(())
1178 }
1179
1180 fn update_position(&self, _position: &Position) -> anyhow::Result<()> {
1181 Ok(())
1182 }
1183
1184 fn snapshot_order_state(&self, _order: &OrderAny) -> anyhow::Result<()> {
1185 Ok(())
1186 }
1187
1188 fn snapshot_position_state(
1189 &self,
1190 _position: &Position,
1191 _ts_snapshot: UnixNanos,
1192 _unrealized_pnl: Option<Money>,
1193 ) -> anyhow::Result<()> {
1194 Ok(())
1195 }
1196
1197 fn heartbeat(&self, _timestamp: UnixNanos) -> anyhow::Result<()> {
1198 Ok(())
1199 }
1200 }
1201
1202 #[derive(Debug)]
1203 struct NoopKernelEventStore;
1204
1205 impl KernelEventStore for NoopKernelEventStore {
1206 fn restore_parent_cache(
1207 &mut self,
1208 _instance_id: UUID4,
1209 _cache: &mut Cache,
1210 ) -> anyhow::Result<()> {
1211 Ok(())
1212 }
1213
1214 fn open(
1215 &mut self,
1216 _instance_id: UUID4,
1217 _components: &RegisteredComponents,
1218 _environment: Environment,
1219 ) -> anyhow::Result<()> {
1220 Ok(())
1221 }
1222
1223 fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
1224 None
1225 }
1226
1227 fn seal(&mut self, _ts_init: UnixNanos) {}
1228
1229 fn run_id(&self) -> Option<&str> {
1230 None
1231 }
1232
1233 fn parent_run_id(&self) -> Option<&str> {
1234 None
1235 }
1236
1237 fn is_halted(&self) -> bool {
1238 false
1239 }
1240 }
1241}