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 #[cfg(feature = "streaming")]
374 streaming: None,
375 #[cfg(feature = "streaming")]
376 catalogs: Vec::new(),
377 };
378
379 let kernel = NautilusKernel::new_with_dependencies(
380 self.name,
381 config,
382 NautilusKernelDependencies::default()
383 .with_clock_factory(self.clock_factory)
384 .with_cache_database(self.cache_database)
385 .with_event_store_factory(self.event_store_factory),
386 )?;
387
388 let config = kernel.config.msgbus().unwrap_or_default();
389 let external_egress = if let Some(factory) = self.external_msgbus_factory {
390 config.validate()?;
391 let backing = factory.create(
392 kernel.config.trader_id(),
393 kernel.instance_id,
394 config.clone(),
395 )?;
396 Some(external_egress_from_backing(backing))
397 } else {
398 self.external_msgbus_egress
399 };
400
401 if let Some(external_egress) = external_egress {
402 nautilus_common::msgbus::get_message_bus()
403 .borrow_mut()
404 .set_external_egress_config(external_egress, &config)?;
405 }
406
407 Ok(kernel)
408 }
409}
410
411impl Default for NautilusKernelBuilder {
412 fn default() -> Self {
414 Self::new(
415 "NautilusKernel".to_string(),
416 TraderId::default(),
417 Environment::Backtest,
418 )
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use std::{
425 cell::Cell,
426 sync::{
427 Arc,
428 atomic::{AtomicBool, Ordering},
429 },
430 };
431
432 use ahash::AHashMap;
433 use bytes::Bytes;
434 use nautilus_common::{
435 cache::{
436 Cache,
437 database::{CacheDatabaseAdapter, CacheMap},
438 },
439 clock::Clock,
440 msgbus::{BusMessage, MessageBusBacking, MessageBusBackingFactory},
441 signal::Signal,
442 };
443 use nautilus_core::UnixNanos;
444 use nautilus_execution::engine::SnapshotAnchorer;
445 #[cfg(feature = "live")]
446 use nautilus_model::identifiers::ComponentId;
447 use nautilus_model::{
448 accounts::AccountAny,
449 data::{
450 Bar, CustomData, DataType, FundingRateUpdate, InstrumentClose, QuoteTick, TradeTick,
451 greeks::{GreeksData, YieldCurveData},
452 },
453 events::{OrderEventAny, OrderSnapshot, position::snapshot::PositionSnapshot},
454 identifiers::{
455 AccountId, ActorId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId,
456 TraderId, VenueOrderId,
457 },
458 instruments::{InstrumentAny, SyntheticInstrument},
459 orderbook::OrderBook,
460 orders::OrderAny,
461 position::Position,
462 types::{Currency, Money},
463 };
464 use parking_lot::Mutex;
465 use rstest::*;
466 use ustr::Ustr;
467
468 use super::*;
469 use crate::event_store::RegisteredComponents;
470
471 #[rstest]
472 fn test_builder_default() {
473 let builder = NautilusKernelBuilder::default();
474 assert_eq!(builder.name, "NautilusKernel");
475 assert_eq!(builder.environment, Environment::Backtest);
476 assert!(builder.load_state);
477 assert!(builder.save_state);
478 }
479
480 #[rstest]
481 fn test_builder_fluent_api() {
482 let trader_id = TraderId::from("TRADER-001");
483 let instance_id = UUID4::new();
484
485 let builder =
486 NautilusKernelBuilder::new("TestKernel".to_string(), trader_id, Environment::Live)
487 .with_instance_id(instance_id)
488 .with_load_state(false)
489 .with_save_state(false)
490 .with_timeout_connection(30);
491
492 assert_eq!(builder.name, "TestKernel");
493 assert_eq!(builder.trader_id, trader_id);
494 assert_eq!(builder.environment, Environment::Live);
495 assert_eq!(builder.instance_id, Some(instance_id));
496 assert!(!builder.load_state);
497 assert!(!builder.save_state);
498 assert_eq!(builder.timeout_connection, Duration::from_secs(30));
499 }
500
501 #[cfg(feature = "python")]
502 #[rstest]
503 fn test_builder_build() {
504 let result = NautilusKernelBuilder::default().build();
505 assert!(result.is_ok());
506
507 let kernel = result.unwrap();
508 assert_eq!(kernel.name(), "NautilusKernel".to_string());
509 assert_eq!(kernel.environment(), Environment::Backtest);
510 }
511
512 #[rstest]
513 fn test_builder_with_configs() {
514 let cache_config = CacheConfig::default();
515 let data_engine_config = DataEngineConfig::default();
516
517 let builder = NautilusKernelBuilder::default()
518 .with_cache_config(cache_config)
519 .with_data_engine_config(data_engine_config);
520
521 assert!(builder.cache.is_some());
522 assert!(builder.data_engine.is_some());
523 }
524
525 #[rstest]
526 fn test_builder_with_cache_database() {
527 let builder = NautilusKernelBuilder::default().with_cache_database(Box::new(NoopAdapter));
528
529 assert!(builder.cache_database.is_some());
530 }
531
532 #[rstest]
533 fn test_builder_with_external_msgbus_egress_forwards_published_quote() {
534 let (external_egress, publications, closed) = CapturingExternalEgress::new();
535 let kernel = NautilusKernelBuilder::default()
536 .with_external_msgbus_egress(Box::new(external_egress))
537 .build()
538 .expect("kernel builds with external message bus egress");
539 let quote = QuoteTick::default();
540
541 nautilus_common::msgbus::publish_quote("data.quotes.TEST".into(), "e);
542
543 let publications = publications.borrow();
544 assert_eq!(publications.len(), 1);
545 assert_eq!(publications[0].topic, "data.quotes.TEST");
546 assert_eq!(
547 serde_json::from_slice::<QuoteTick>(&publications[0].payload)
548 .expect("JSON payload must decode as QuoteTick"),
549 quote
550 );
551 drop(publications);
552
553 nautilus_common::msgbus::get_message_bus()
554 .borrow_mut()
555 .dispose();
556 assert!(closed.get());
557 drop(kernel);
558 }
559
560 #[rstest]
561 fn test_builder_with_external_msgbus_factory_forwards_published_quote() {
562 let publications = Arc::new(Mutex::new(Vec::new()));
563 let closed = Arc::new(AtomicBool::new(false));
564 let factory = CapturingBackingFactory {
565 publications: publications.clone(),
566 closed: closed.clone(),
567 };
568 let kernel = NautilusKernelBuilder::default()
569 .with_external_msgbus_factory(Box::new(factory))
570 .build()
571 .expect("kernel builds with external message bus factory");
572 let quote = QuoteTick::default();
573
574 nautilus_common::msgbus::publish_quote("data.quotes.TEST".into(), "e);
575
576 let publications = publications.lock();
577 assert_eq!(publications.len(), 1);
578 assert_eq!(publications[0].topic, "data.quotes.TEST");
579 assert_eq!(
580 serde_json::from_slice::<QuoteTick>(&publications[0].payload)
581 .expect("JSON payload must decode as QuoteTick"),
582 quote
583 );
584 drop(publications);
585
586 nautilus_common::msgbus::get_message_bus()
587 .borrow_mut()
588 .dispose();
589 assert!(closed.load(Ordering::Relaxed));
590 drop(kernel);
591 }
592
593 #[rstest]
594 fn test_builder_with_external_msgbus_factory_rejects_external_streams() {
595 let factory = CapturingBackingFactory {
596 publications: Arc::new(Mutex::new(Vec::new())),
597 closed: Arc::new(AtomicBool::new(false)),
598 };
599 let config = MessageBusConfig {
600 external_streams: Some(vec!["stream".to_string()]),
601 ..Default::default()
602 };
603
604 let error = NautilusKernelBuilder::default()
605 .with_msgbus_config(config)
606 .with_external_msgbus_factory(Box::new(factory))
607 .build()
608 .expect_err("system builder should reject external ingress streams");
609
610 assert!(
611 error
612 .to_string()
613 .contains("cannot consume external message bus streams")
614 );
615 }
616
617 #[rstest]
618 fn test_builder_with_external_msgbus_factory_rejects_injected_egress() {
619 let factory = CapturingBackingFactory {
620 publications: Arc::new(Mutex::new(Vec::new())),
621 closed: Arc::new(AtomicBool::new(false)),
622 };
623 let (external_egress, _publications, _closed) = CapturingExternalEgress::new();
624
625 let error = NautilusKernelBuilder::default()
626 .with_external_msgbus_factory(Box::new(factory))
627 .with_external_msgbus_egress(Box::new(external_egress))
628 .build()
629 .expect_err("system builder should reject factory plus injected egress");
630
631 assert!(
632 error
633 .to_string()
634 .contains("cannot be combined with injected egress")
635 );
636 }
637
638 #[rstest]
639 fn test_builder_default_has_no_event_store() {
640 let kernel = NautilusKernelBuilder::default()
641 .build()
642 .expect("kernel builds without an event store");
643
644 assert!(kernel.event_store().is_none());
645 }
646
647 #[rstest]
648 fn test_builder_with_event_store_invokes_factory_with_kernel_args() {
649 type FactoryArgs = (UUID4, Rc<RefCell<dyn Clock>>);
650
651 let known_id = UUID4::new();
652 let captured: Rc<RefCell<Option<FactoryArgs>>> = Rc::new(RefCell::new(None));
653 let captured_for_closure = captured.clone();
654
655 let kernel = NautilusKernelBuilder::default()
656 .with_instance_id(known_id)
657 .with_event_store(move |instance_id, clock| {
658 *captured_for_closure.borrow_mut() = Some((instance_id, clock));
659 Ok(Box::new(NoopKernelEventStore))
660 })
661 .build()
662 .expect("kernel");
663
664 let (received_id, received_clock) =
665 captured.borrow_mut().take().expect("factory invoked once");
666
667 assert_eq!(
668 received_id, known_id,
669 "factory must receive kernel instance_id"
670 );
671 assert!(
672 Rc::ptr_eq(&received_clock, &kernel.clock()),
673 "factory must receive the kernel's clock Rc, not a fresh allocation",
674 );
675 }
676
677 #[cfg(feature = "live")]
678 #[rstest]
679 fn test_builder_with_clock_factory_drives_kernel_and_component_clocks() {
680 use nautilus_common::clock::VirtualClock;
681
682 let calls = Rc::new(Cell::new(0usize));
683 let calls_in_closure = calls.clone();
684
685 let kernel = NautilusKernelBuilder::new(
686 "ClockFactoryKernel".to_string(),
687 TraderId::from("TRADER-CF"),
688 Environment::Live,
689 )
690 .with_clock_factory(move || {
691 calls_in_closure.set(calls_in_closure.get() + 1);
692 Rc::new(RefCell::new(VirtualClock::new())) as Rc<RefCell<dyn Clock>>
693 })
694 .build()
695 .expect("kernel builds with clock factory");
696
697 assert_eq!(
698 calls.get(),
699 1,
700 "kernel clock must consume exactly one factory call"
701 );
702 assert!(
703 (*kernel.clock().borrow()).as_any().is::<VirtualClock>(),
704 "kernel clock must be the factory-produced VirtualClock"
705 );
706
707 let c1 = kernel
708 .trader()
709 .borrow_mut()
710 .create_component_clock(ComponentId::new("COMP-1"));
711 let c2 = kernel
712 .trader()
713 .borrow_mut()
714 .create_component_clock(ComponentId::new("COMP-2"));
715
716 assert_eq!(
717 calls.get(),
718 3,
719 "factory must back kernel clock and each component clock"
720 );
721 assert!((*c1.borrow()).as_any().is::<VirtualClock>());
722 assert!((*c2.borrow()).as_any().is::<VirtualClock>());
723 }
724
725 #[cfg(feature = "live")]
726 #[rstest]
727 fn test_builder_without_clock_factory_uses_live_clock_default() {
728 use nautilus_common::live::clock::LiveClock; let kernel = NautilusKernelBuilder::new(
731 "DefaultClockKernel".to_string(),
732 TraderId::from("TRADER-DC"),
733 Environment::Live,
734 )
735 .build()
736 .expect("kernel builds without a clock factory");
737
738 assert!(
739 (*kernel.clock().borrow()).as_any().is::<LiveClock>(),
740 "no factory uses LiveClock::default for the kernel clock"
741 );
742
743 let comp = kernel
744 .trader()
745 .borrow_mut()
746 .create_component_clock(ComponentId::new("COMP-D"));
747 assert!(
748 (*comp.borrow()).as_any().is::<LiveClock>(),
749 "no factory uses LiveClock::default for component clocks"
750 );
751 }
752
753 #[rstest]
754 fn test_builder_with_event_store_propagates_factory_error() {
755 let result = NautilusKernelBuilder::default()
756 .with_event_store(|_instance_id, _clock| Err(anyhow::anyhow!("factory boom")))
757 .build();
758
759 let err = result.expect_err("factory error must surface from build()");
760
761 assert!(
762 err.to_string().contains("factory boom"),
763 "error must propagate the factory's message; got: {err}",
764 );
765 }
766
767 #[rstest]
768 fn test_builder_with_all_engine_configs() {
769 let builder = NautilusKernelBuilder::default()
770 .with_data_engine_config(DataEngineConfig::default())
771 .with_risk_engine_config(RiskEngineConfig::default())
772 .with_exec_engine_config(ExecutionEngineConfig::default())
773 .with_portfolio_config(PortfolioConfig::default());
774
775 assert!(builder.data_engine.is_some());
776 assert!(builder.risk_engine.is_some());
777 assert!(builder.exec_engine.is_some());
778 assert!(builder.portfolio.is_some());
779 }
780
781 #[rstest]
782 fn test_builder_with_all_timeouts() {
783 let builder = NautilusKernelBuilder::default()
784 .with_timeout_connection(10)
785 .with_timeout_reconciliation(20)
786 .with_timeout_portfolio(30)
787 .with_timeout_disconnection(40)
788 .with_delay_post_stop(50)
789 .with_timeout_shutdown(60);
790
791 assert_eq!(builder.timeout_connection, Duration::from_secs(10));
792 assert_eq!(builder.timeout_reconciliation, Duration::from_secs(20));
793 assert_eq!(builder.timeout_portfolio, Duration::from_secs(30));
794 assert_eq!(builder.timeout_disconnection, Duration::from_secs(40));
795 assert_eq!(builder.delay_post_stop, Duration::from_secs(50));
796 assert_eq!(builder.timeout_shutdown, Duration::from_mins(1));
797 }
798
799 #[rstest]
800 fn test_builder_default_timeouts() {
801 let builder = NautilusKernelBuilder::default();
802
803 assert_eq!(builder.timeout_connection, Duration::from_mins(1));
804 assert_eq!(builder.timeout_reconciliation, Duration::from_secs(30));
805 assert_eq!(builder.timeout_portfolio, Duration::from_secs(10));
806 assert_eq!(builder.timeout_disconnection, Duration::from_secs(10));
807 assert_eq!(builder.delay_post_stop, Duration::from_secs(10));
808 assert_eq!(builder.timeout_shutdown, Duration::from_secs(5));
809 }
810
811 #[derive(Debug)]
812 struct CapturedEgressMessage {
813 topic: String,
814 payload: Bytes,
815 }
816
817 type CapturedEgressMessages = Rc<RefCell<Vec<CapturedEgressMessage>>>;
818 type SharedClosed = Rc<Cell<bool>>;
819
820 struct CapturingExternalEgress {
821 publications: CapturedEgressMessages,
822 closed: SharedClosed,
823 }
824
825 impl CapturingExternalEgress {
826 fn new() -> (Self, CapturedEgressMessages, SharedClosed) {
827 let publications = Rc::new(RefCell::new(Vec::new()));
828 let closed = Rc::new(Cell::new(false));
829 (
830 Self {
831 publications: publications.clone(),
832 closed: closed.clone(),
833 },
834 publications,
835 closed,
836 )
837 }
838 }
839
840 impl MessageBusExternalEgress for CapturingExternalEgress {
841 fn is_closed(&self) -> bool {
842 self.closed.get()
843 }
844
845 fn publish(&self, message: BusMessage) {
846 self.publications.borrow_mut().push(CapturedEgressMessage {
847 topic: message.topic.to_string(),
848 payload: message.payload,
849 });
850 }
851
852 fn close(&mut self) {
853 self.closed.set(true);
854 }
855 }
856
857 #[derive(Debug)]
858 struct CapturingBackingFactory {
859 publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
860 closed: Arc<AtomicBool>,
861 }
862
863 impl MessageBusBackingFactory for CapturingBackingFactory {
864 fn create(
865 &self,
866 _trader_id: TraderId,
867 _instance_id: UUID4,
868 _config: MessageBusConfig,
869 ) -> anyhow::Result<Box<dyn MessageBusBacking>> {
870 Ok(Box::new(CapturingBacking {
871 publications: self.publications.clone(),
872 closed: self.closed.clone(),
873 }))
874 }
875 }
876
877 struct CapturingBacking {
878 publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
879 closed: Arc<AtomicBool>,
880 }
881
882 impl MessageBusBacking for CapturingBacking {
883 fn is_closed(&self) -> bool {
884 self.closed.load(Ordering::Relaxed)
885 }
886
887 fn publish(&self, message: BusMessage) {
888 self.publications.lock().push(CapturedEgressMessage {
889 topic: message.topic.to_string(),
890 payload: message.payload,
891 });
892 }
893
894 fn close(&mut self) {
895 self.closed.store(true, Ordering::Relaxed);
896 }
897 }
898
899 struct NoopAdapter;
900
901 #[async_trait::async_trait]
902 impl CacheDatabaseAdapter for NoopAdapter {
903 fn close(&mut self) -> anyhow::Result<()> {
904 Ok(())
905 }
906
907 fn flush(&mut self) -> anyhow::Result<()> {
908 Ok(())
909 }
910
911 async fn load_all(&self) -> anyhow::Result<CacheMap> {
912 Ok(CacheMap::default())
913 }
914
915 fn load(&self) -> anyhow::Result<AHashMap<String, Bytes>> {
916 Ok(AHashMap::new())
917 }
918
919 async fn load_currencies(&self) -> anyhow::Result<AHashMap<Ustr, Currency>> {
920 Ok(AHashMap::new())
921 }
922
923 async fn load_instruments(&self) -> anyhow::Result<AHashMap<InstrumentId, InstrumentAny>> {
924 Ok(AHashMap::new())
925 }
926
927 async fn load_instrument_closes(
928 &self,
929 ) -> anyhow::Result<AHashMap<InstrumentId, InstrumentClose>> {
930 Ok(AHashMap::new())
931 }
932
933 async fn load_synthetics(
934 &self,
935 ) -> anyhow::Result<AHashMap<InstrumentId, SyntheticInstrument>> {
936 Ok(AHashMap::new())
937 }
938
939 async fn load_accounts(&self) -> anyhow::Result<AHashMap<AccountId, AccountAny>> {
940 Ok(AHashMap::new())
941 }
942
943 async fn load_orders(&self) -> anyhow::Result<AHashMap<ClientOrderId, OrderAny>> {
944 Ok(AHashMap::new())
945 }
946
947 async fn load_positions(&self) -> anyhow::Result<AHashMap<PositionId, Position>> {
948 Ok(AHashMap::new())
949 }
950
951 fn load_index_order_position(&self) -> anyhow::Result<AHashMap<ClientOrderId, PositionId>> {
952 Ok(AHashMap::new())
953 }
954
955 fn load_index_order_client(&self) -> anyhow::Result<AHashMap<ClientOrderId, ClientId>> {
956 Ok(AHashMap::new())
957 }
958
959 async fn load_currency(&self, _code: &Ustr) -> anyhow::Result<Option<Currency>> {
960 Ok(None)
961 }
962
963 async fn load_instrument(
964 &self,
965 _instrument_id: &InstrumentId,
966 ) -> anyhow::Result<Option<InstrumentAny>> {
967 Ok(None)
968 }
969
970 async fn load_synthetic(
971 &self,
972 _instrument_id: &InstrumentId,
973 ) -> anyhow::Result<Option<SyntheticInstrument>> {
974 Ok(None)
975 }
976
977 async fn load_account(
978 &self,
979 _account_id: &AccountId,
980 ) -> anyhow::Result<Option<AccountAny>> {
981 Ok(None)
982 }
983
984 async fn load_order(
985 &self,
986 _client_order_id: &ClientOrderId,
987 ) -> anyhow::Result<Option<OrderAny>> {
988 Ok(None)
989 }
990
991 async fn load_position(
992 &self,
993 _position_id: &PositionId,
994 ) -> anyhow::Result<Option<Position>> {
995 Ok(None)
996 }
997
998 fn load_actor(&self, _actor_id: &ActorId) -> anyhow::Result<AHashMap<String, Bytes>> {
999 Ok(AHashMap::new())
1000 }
1001
1002 fn load_strategy(
1003 &self,
1004 _strategy_id: &StrategyId,
1005 ) -> anyhow::Result<AHashMap<String, Bytes>> {
1006 Ok(AHashMap::new())
1007 }
1008
1009 fn load_signals(&self, _name: &str) -> anyhow::Result<Vec<Signal>> {
1010 Ok(Vec::new())
1011 }
1012
1013 fn load_custom_data(&self, _data_type: &DataType) -> anyhow::Result<Vec<CustomData>> {
1014 Ok(Vec::new())
1015 }
1016
1017 fn load_order_snapshot(
1018 &self,
1019 _client_order_id: &ClientOrderId,
1020 ) -> anyhow::Result<Option<OrderSnapshot>> {
1021 Ok(None)
1022 }
1023
1024 fn load_position_snapshot(
1025 &self,
1026 _position_id: &PositionId,
1027 ) -> anyhow::Result<Option<PositionSnapshot>> {
1028 Ok(None)
1029 }
1030
1031 fn load_quotes(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<QuoteTick>> {
1032 Ok(Vec::new())
1033 }
1034
1035 fn load_trades(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<TradeTick>> {
1036 Ok(Vec::new())
1037 }
1038
1039 fn load_funding_rates(
1040 &self,
1041 _instrument_id: &InstrumentId,
1042 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
1043 Ok(Vec::new())
1044 }
1045
1046 fn load_bars(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<Bar>> {
1047 Ok(Vec::new())
1048 }
1049
1050 fn add(&self, _key: String, _value: Bytes) -> anyhow::Result<()> {
1051 Ok(())
1052 }
1053
1054 fn add_currency(&self, _currency: &Currency) -> anyhow::Result<()> {
1055 Ok(())
1056 }
1057
1058 fn add_instrument(&self, _instrument: &InstrumentAny) -> anyhow::Result<()> {
1059 Ok(())
1060 }
1061
1062 fn add_instrument_close(&self, _close: &InstrumentClose) -> anyhow::Result<()> {
1063 Ok(())
1064 }
1065
1066 fn add_synthetic(&self, _synthetic: &SyntheticInstrument) -> anyhow::Result<()> {
1067 Ok(())
1068 }
1069
1070 fn add_account(&self, _account: &AccountAny) -> anyhow::Result<()> {
1071 Ok(())
1072 }
1073
1074 fn add_order(&self, _order: &OrderAny, _client_id: Option<ClientId>) -> anyhow::Result<()> {
1075 Ok(())
1076 }
1077
1078 fn add_order_snapshot(&self, _snapshot: &OrderSnapshot) -> anyhow::Result<()> {
1079 Ok(())
1080 }
1081
1082 fn add_position(&self, _position: &Position) -> anyhow::Result<()> {
1083 Ok(())
1084 }
1085
1086 fn add_position_snapshot(&self, _snapshot: &PositionSnapshot) -> anyhow::Result<()> {
1087 Ok(())
1088 }
1089
1090 fn add_order_book(&self, _order_book: &OrderBook) -> anyhow::Result<()> {
1091 Ok(())
1092 }
1093
1094 fn add_signal(&self, _signal: &Signal) -> anyhow::Result<()> {
1095 Ok(())
1096 }
1097
1098 fn add_custom_data(&self, _data: &CustomData) -> anyhow::Result<()> {
1099 Ok(())
1100 }
1101
1102 fn add_quote(&self, _quote: &QuoteTick) -> anyhow::Result<()> {
1103 Ok(())
1104 }
1105
1106 fn add_trade(&self, _trade: &TradeTick) -> anyhow::Result<()> {
1107 Ok(())
1108 }
1109
1110 fn add_funding_rate(&self, _funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
1111 Ok(())
1112 }
1113
1114 fn add_bar(&self, _bar: &Bar) -> anyhow::Result<()> {
1115 Ok(())
1116 }
1117
1118 fn add_greeks(&self, _greeks: &GreeksData) -> anyhow::Result<()> {
1119 Ok(())
1120 }
1121
1122 fn add_yield_curve(&self, _yield_curve: &YieldCurveData) -> anyhow::Result<()> {
1123 Ok(())
1124 }
1125
1126 fn delete_actor(&self, _actor_id: &ActorId) -> anyhow::Result<()> {
1127 Ok(())
1128 }
1129
1130 fn delete_strategy(&self, _component_id: &StrategyId) -> anyhow::Result<()> {
1131 Ok(())
1132 }
1133
1134 fn delete_order(&self, _client_order_id: &ClientOrderId) -> anyhow::Result<()> {
1135 Ok(())
1136 }
1137
1138 fn delete_position(&self, _position_id: &PositionId) -> anyhow::Result<()> {
1139 Ok(())
1140 }
1141
1142 fn delete_account_event(
1143 &self,
1144 _account_id: &AccountId,
1145 _event_id: &str,
1146 ) -> anyhow::Result<()> {
1147 Ok(())
1148 }
1149
1150 fn index_venue_order_id(
1151 &self,
1152 _client_order_id: ClientOrderId,
1153 _venue_order_id: VenueOrderId,
1154 ) -> anyhow::Result<()> {
1155 Ok(())
1156 }
1157
1158 fn index_order_position(
1159 &self,
1160 _client_order_id: ClientOrderId,
1161 _position_id: PositionId,
1162 ) -> anyhow::Result<()> {
1163 Ok(())
1164 }
1165
1166 fn update_actor(
1167 &self,
1168 _actor_id: &ActorId,
1169 _state: &AHashMap<String, Bytes>,
1170 ) -> anyhow::Result<()> {
1171 Ok(())
1172 }
1173
1174 fn update_strategy(
1175 &self,
1176 _strategy_id: &StrategyId,
1177 _state: &AHashMap<String, Bytes>,
1178 ) -> anyhow::Result<()> {
1179 Ok(())
1180 }
1181
1182 fn update_account(&self, _account: &AccountAny) -> anyhow::Result<()> {
1183 Ok(())
1184 }
1185
1186 fn update_order(&self, _order_event: &OrderEventAny) -> anyhow::Result<()> {
1187 Ok(())
1188 }
1189
1190 fn update_position(&self, _position: &Position) -> anyhow::Result<()> {
1191 Ok(())
1192 }
1193
1194 fn snapshot_order_state(&self, _order: &OrderAny) -> anyhow::Result<()> {
1195 Ok(())
1196 }
1197
1198 fn snapshot_position_state(
1199 &self,
1200 _position: &Position,
1201 _ts_snapshot: UnixNanos,
1202 _unrealized_pnl: Option<Money>,
1203 ) -> anyhow::Result<()> {
1204 Ok(())
1205 }
1206
1207 fn heartbeat(&self, _timestamp: UnixNanos) -> anyhow::Result<()> {
1208 Ok(())
1209 }
1210 }
1211
1212 #[derive(Debug)]
1213 struct NoopKernelEventStore;
1214
1215 impl KernelEventStore for NoopKernelEventStore {
1216 fn restore_parent_cache(
1217 &mut self,
1218 _instance_id: UUID4,
1219 _cache: &mut Cache,
1220 ) -> anyhow::Result<()> {
1221 Ok(())
1222 }
1223
1224 fn open(
1225 &mut self,
1226 _instance_id: UUID4,
1227 _components: &RegisteredComponents,
1228 _environment: Environment,
1229 ) -> anyhow::Result<()> {
1230 Ok(())
1231 }
1232
1233 fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
1234 None
1235 }
1236
1237 fn seal(&mut self, _ts_init: UnixNanos) {}
1238
1239 fn run_id(&self) -> Option<&str> {
1240 None
1241 }
1242
1243 fn parent_run_id(&self) -> Option<&str> {
1244 None
1245 }
1246
1247 fn is_halted(&self) -> bool {
1248 false
1249 }
1250 }
1251}