1use std::{fmt::Debug, time::Duration};
17
18use nautilus_common::{
19 cache::CacheConfig,
20 config::{ConfigError, ConfigErrorCollector, ConfigResult},
21 enums::Environment,
22 logging::logger::LoggerConfig,
23 msgbus::MessageBusConfig,
24};
25use nautilus_core::{UUID4, UnixNanos};
26use nautilus_data::engine::config::DataEngineConfig;
27use nautilus_execution::engine::config::ExecutionEngineConfig;
28use nautilus_model::identifiers::TraderId;
29#[cfg(feature = "streaming")]
30use nautilus_persistence::config::DataCatalogConfig;
31use nautilus_portfolio::config::PortfolioConfig;
32use nautilus_risk::engine::config::RiskEngineConfig;
33use serde::{Deserialize, Serialize};
34
35pub trait NautilusKernelConfig: Debug {
37 fn environment(&self) -> Environment;
39 fn trader_id(&self) -> TraderId;
41 fn load_state(&self) -> bool;
43 fn save_state(&self) -> bool;
45 fn shutdown_on_error(&self) -> bool;
49 fn logging(&self) -> LoggerConfig;
51 fn instance_id(&self) -> Option<UUID4>;
53 fn timeout_connection(&self) -> Duration;
55 fn timeout_reconciliation(&self) -> Duration;
57 fn timeout_portfolio(&self) -> Duration;
59 fn timeout_disconnection(&self) -> Duration;
61 fn delay_post_stop(&self) -> Duration;
63 fn timeout_shutdown(&self) -> Duration;
65 fn cache(&self) -> Option<CacheConfig>;
67 fn msgbus(&self) -> Option<MessageBusConfig>;
69 fn data_engine(&self) -> Option<DataEngineConfig>;
71 fn risk_engine(&self) -> Option<RiskEngineConfig>;
73 fn exec_engine(&self) -> Option<ExecutionEngineConfig>;
75 fn portfolio(&self) -> Option<PortfolioConfig>;
77 fn streaming(&self) -> Option<StreamingConfig>;
79 #[cfg(feature = "streaming")]
81 fn catalogs(&self) -> Vec<DataCatalogConfig> {
82 Vec::new()
83 }
84}
85
86#[derive(Debug, Clone, bon::Builder)]
88pub struct KernelConfig {
89 #[builder(default = Environment::Backtest)]
91 pub environment: Environment,
92 #[builder(default)]
94 pub trader_id: TraderId,
95 #[builder(default)]
97 pub load_state: bool,
98 #[builder(default)]
100 pub save_state: bool,
101 #[builder(default)]
105 pub shutdown_on_error: bool,
106 #[builder(default)]
108 pub logging: LoggerConfig,
109 pub instance_id: Option<UUID4>,
111 #[builder(default = Duration::from_mins(1))]
113 pub timeout_connection: Duration,
114 #[builder(default = Duration::from_secs(30))]
116 pub timeout_reconciliation: Duration,
117 #[builder(default = Duration::from_secs(10))]
119 pub timeout_portfolio: Duration,
120 #[builder(default = Duration::from_secs(10))]
122 pub timeout_disconnection: Duration,
123 #[builder(default = Duration::from_secs(10))]
125 pub delay_post_stop: Duration,
126 #[builder(default = Duration::from_secs(5))]
128 pub timeout_shutdown: Duration,
129 pub cache: Option<CacheConfig>,
131 pub msgbus: Option<MessageBusConfig>,
133 pub data_engine: Option<DataEngineConfig>,
135 pub risk_engine: Option<RiskEngineConfig>,
137 pub exec_engine: Option<ExecutionEngineConfig>,
139 pub portfolio: Option<PortfolioConfig>,
141 pub streaming: Option<StreamingConfig>,
143 #[cfg(feature = "streaming")]
145 #[builder(default)]
146 pub catalogs: Vec<DataCatalogConfig>,
147}
148
149impl NautilusKernelConfig for KernelConfig {
150 fn environment(&self) -> Environment {
151 self.environment
152 }
153
154 fn trader_id(&self) -> TraderId {
155 self.trader_id
156 }
157
158 fn load_state(&self) -> bool {
159 self.load_state
160 }
161
162 fn save_state(&self) -> bool {
163 self.save_state
164 }
165
166 fn shutdown_on_error(&self) -> bool {
167 self.shutdown_on_error
168 }
169
170 fn logging(&self) -> LoggerConfig {
171 self.logging.clone()
172 }
173
174 fn instance_id(&self) -> Option<UUID4> {
175 self.instance_id
176 }
177
178 fn timeout_connection(&self) -> Duration {
179 self.timeout_connection
180 }
181
182 fn timeout_reconciliation(&self) -> Duration {
183 self.timeout_reconciliation
184 }
185
186 fn timeout_portfolio(&self) -> Duration {
187 self.timeout_portfolio
188 }
189
190 fn timeout_disconnection(&self) -> Duration {
191 self.timeout_disconnection
192 }
193
194 fn delay_post_stop(&self) -> Duration {
195 self.delay_post_stop
196 }
197
198 fn timeout_shutdown(&self) -> Duration {
199 self.timeout_shutdown
200 }
201
202 fn cache(&self) -> Option<CacheConfig> {
203 self.cache.clone()
204 }
205
206 fn msgbus(&self) -> Option<MessageBusConfig> {
207 self.msgbus.clone()
208 }
209
210 fn data_engine(&self) -> Option<DataEngineConfig> {
211 self.data_engine.clone()
212 }
213
214 fn risk_engine(&self) -> Option<RiskEngineConfig> {
215 self.risk_engine.clone()
216 }
217
218 fn exec_engine(&self) -> Option<ExecutionEngineConfig> {
219 self.exec_engine.clone()
220 }
221
222 fn portfolio(&self) -> Option<PortfolioConfig> {
223 self.portfolio
224 }
225
226 fn streaming(&self) -> Option<StreamingConfig> {
227 self.streaming.clone()
228 }
229
230 #[cfg(feature = "streaming")]
231 fn catalogs(&self) -> Vec<DataCatalogConfig> {
232 self.catalogs.clone()
233 }
234}
235
236impl Default for KernelConfig {
237 fn default() -> Self {
238 Self::builder().build()
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(rename_all = "snake_case")]
245pub enum RotationConfig {
246 Size {
248 max_size: u64,
250 },
251 Interval {
253 interval_ns: u64,
255 },
256 ScheduledDates {
258 interval_ns: u64,
260 schedule_ns: UnixNanos,
262 },
263 NoRotation,
265}
266
267#[cfg_attr(
269 feature = "python",
270 pyo3::pyclass(module = "nautilus_trader.persistence", from_py_object, frozen)
271)]
272#[cfg_attr(
273 feature = "python",
274 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.persistence")
275)]
276#[cfg_attr(
277 feature = "python",
278 expect(
279 clippy::unsafe_derive_deserialize,
280 reason = "config deserializes plain fields; unsafe methods come from generated PyO3 integration"
281 )
282)]
283#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
284#[builder(finish_fn(name = build_inner, vis = ""))]
285#[serde(deny_unknown_fields)]
286pub struct StreamingConfig {
287 pub catalog_path: String,
289 pub fs_protocol: String,
291 pub flush_interval_ms: u64,
293 pub replace_existing: bool,
295 pub rotation_config: RotationConfig,
297}
298
299impl<S: streaming_config_builder::IsComplete> StreamingConfigBuilder<S> {
300 pub fn build(self) -> ConfigResult<StreamingConfig> {
307 let config = self.build_inner();
308 config.validate()?;
309 Ok(config)
310 }
311}
312
313impl StreamingConfig {
314 #[must_use]
316 pub const fn new(
317 catalog_path: String,
318 fs_protocol: String,
319 flush_interval_ms: u64,
320 replace_existing: bool,
321 rotation_config: RotationConfig,
322 ) -> Self {
323 Self {
324 catalog_path,
325 fs_protocol,
326 flush_interval_ms,
327 replace_existing,
328 rotation_config,
329 }
330 }
331
332 pub fn validate(&self) -> ConfigResult<()> {
339 let mut errors = ConfigErrorCollector::new();
340
341 errors.check(
342 !self.catalog_path.trim().is_empty(),
343 ConfigError::empty_field("catalog_path"),
344 );
345 errors.check(
346 !self.fs_protocol.trim().is_empty(),
347 ConfigError::empty_field("fs_protocol"),
348 );
349
350 let flush_interval_ms = self.flush_interval_ms;
351 errors.check(
352 flush_interval_ms > 0,
353 ConfigError::range(
354 "flush_interval_ms",
355 format!("must be a positive number of milliseconds, was {flush_interval_ms}"),
356 ),
357 );
358
359 errors.into_result()
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use rstest::rstest;
366
367 use super::*;
368
369 #[rstest]
370 fn test_kernel_config_default_connection_timeout() {
371 let config = KernelConfig::default();
372
373 assert_eq!(config.timeout_connection, Duration::from_mins(1));
374 }
375
376 #[rstest]
377 fn test_streaming_config_builder_valid() {
378 let config = StreamingConfig::builder()
379 .catalog_path("/data/catalog".to_string())
380 .fs_protocol("file".to_string())
381 .flush_interval_ms(1_000)
382 .replace_existing(false)
383 .rotation_config(RotationConfig::NoRotation)
384 .build();
385
386 assert!(config.is_ok());
387 }
388
389 #[rstest]
390 fn test_streaming_config_zero_flush_interval_rejected() {
391 let result = StreamingConfig::builder()
392 .catalog_path("/data/catalog".to_string())
393 .fs_protocol("file".to_string())
394 .flush_interval_ms(0)
395 .replace_existing(false)
396 .rotation_config(RotationConfig::NoRotation)
397 .build();
398
399 assert!(
400 matches!(result, Err(ConfigError::Range { field, .. }) if field == "flush_interval_ms")
401 );
402 }
403
404 #[rstest]
405 fn test_streaming_config_empty_catalog_path_rejected() {
406 let result = StreamingConfig::builder()
407 .catalog_path(String::new())
408 .fs_protocol("file".to_string())
409 .flush_interval_ms(1_000)
410 .replace_existing(false)
411 .rotation_config(RotationConfig::NoRotation)
412 .build();
413
414 assert!(
415 matches!(result, Err(ConfigError::EmptyField { field }) if field == "catalog_path")
416 );
417 }
418
419 #[rstest]
420 fn test_streaming_config_toml_round_trip() {
421 let config: StreamingConfig = toml::from_str(
422 r#"
423catalog_path = "/data/catalog"
424fs_protocol = "file"
425flush_interval_ms = 1000
426replace_existing = false
427
428[rotation_config.size]
429max_size = 1048576
430"#,
431 )
432 .unwrap();
433
434 assert_eq!(config.catalog_path, "/data/catalog");
435 assert_eq!(config.fs_protocol, "file");
436 assert_eq!(config.flush_interval_ms, 1000);
437 assert!(!config.replace_existing);
438 assert!(matches!(
439 config.rotation_config,
440 RotationConfig::Size {
441 max_size: 1_048_576
442 }
443 ));
444 }
445
446 #[rstest]
447 fn test_streaming_config_with_no_rotation_toml() {
448 let config: StreamingConfig = toml::from_str(
449 r#"
450catalog_path = "/data/catalog"
451fs_protocol = "file"
452flush_interval_ms = 500
453replace_existing = true
454rotation_config = "no_rotation"
455"#,
456 )
457 .unwrap();
458
459 assert!(matches!(config.rotation_config, RotationConfig::NoRotation));
460 assert!(config.replace_existing);
461 }
462}