nautilus_system/
config.rs1use 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;
29use nautilus_portfolio::config::PortfolioConfig;
30use nautilus_risk::engine::config::RiskEngineConfig;
31use serde::{Deserialize, Serialize};
32
33pub trait NautilusKernelConfig: Debug {
35 fn environment(&self) -> Environment;
37 fn trader_id(&self) -> TraderId;
39 fn load_state(&self) -> bool;
41 fn save_state(&self) -> bool;
43 fn shutdown_on_error(&self) -> bool;
47 fn logging(&self) -> LoggerConfig;
49 fn instance_id(&self) -> Option<UUID4>;
51 fn timeout_connection(&self) -> Duration;
53 fn timeout_reconciliation(&self) -> Duration;
55 fn timeout_portfolio(&self) -> Duration;
57 fn timeout_disconnection(&self) -> Duration;
59 fn delay_post_stop(&self) -> Duration;
61 fn timeout_shutdown(&self) -> Duration;
63 fn cache(&self) -> Option<CacheConfig>;
65 fn msgbus(&self) -> Option<MessageBusConfig>;
67 fn data_engine(&self) -> Option<DataEngineConfig>;
69 fn risk_engine(&self) -> Option<RiskEngineConfig>;
71 fn exec_engine(&self) -> Option<ExecutionEngineConfig>;
73 fn portfolio(&self) -> Option<PortfolioConfig>;
75 fn streaming(&self) -> Option<StreamingConfig>;
77}
78
79#[derive(Debug, Clone, bon::Builder)]
81pub struct KernelConfig {
82 #[builder(default = Environment::Backtest)]
84 pub environment: Environment,
85 #[builder(default)]
87 pub trader_id: TraderId,
88 #[builder(default)]
90 pub load_state: bool,
91 #[builder(default)]
93 pub save_state: bool,
94 #[builder(default)]
98 pub shutdown_on_error: bool,
99 #[builder(default)]
101 pub logging: LoggerConfig,
102 pub instance_id: Option<UUID4>,
104 #[builder(default = Duration::from_mins(1))]
106 pub timeout_connection: Duration,
107 #[builder(default = Duration::from_secs(30))]
109 pub timeout_reconciliation: Duration,
110 #[builder(default = Duration::from_secs(10))]
112 pub timeout_portfolio: Duration,
113 #[builder(default = Duration::from_secs(10))]
115 pub timeout_disconnection: Duration,
116 #[builder(default = Duration::from_secs(10))]
118 pub delay_post_stop: Duration,
119 #[builder(default = Duration::from_secs(5))]
121 pub timeout_shutdown: Duration,
122 pub cache: Option<CacheConfig>,
124 pub msgbus: Option<MessageBusConfig>,
126 pub data_engine: Option<DataEngineConfig>,
128 pub risk_engine: Option<RiskEngineConfig>,
130 pub exec_engine: Option<ExecutionEngineConfig>,
132 pub portfolio: Option<PortfolioConfig>,
134 pub streaming: Option<StreamingConfig>,
136}
137
138impl NautilusKernelConfig for KernelConfig {
139 fn environment(&self) -> Environment {
140 self.environment
141 }
142
143 fn trader_id(&self) -> TraderId {
144 self.trader_id
145 }
146
147 fn load_state(&self) -> bool {
148 self.load_state
149 }
150
151 fn save_state(&self) -> bool {
152 self.save_state
153 }
154
155 fn shutdown_on_error(&self) -> bool {
156 self.shutdown_on_error
157 }
158
159 fn logging(&self) -> LoggerConfig {
160 self.logging.clone()
161 }
162
163 fn instance_id(&self) -> Option<UUID4> {
164 self.instance_id
165 }
166
167 fn timeout_connection(&self) -> Duration {
168 self.timeout_connection
169 }
170
171 fn timeout_reconciliation(&self) -> Duration {
172 self.timeout_reconciliation
173 }
174
175 fn timeout_portfolio(&self) -> Duration {
176 self.timeout_portfolio
177 }
178
179 fn timeout_disconnection(&self) -> Duration {
180 self.timeout_disconnection
181 }
182
183 fn delay_post_stop(&self) -> Duration {
184 self.delay_post_stop
185 }
186
187 fn timeout_shutdown(&self) -> Duration {
188 self.timeout_shutdown
189 }
190
191 fn cache(&self) -> Option<CacheConfig> {
192 self.cache.clone()
193 }
194
195 fn msgbus(&self) -> Option<MessageBusConfig> {
196 self.msgbus.clone()
197 }
198
199 fn data_engine(&self) -> Option<DataEngineConfig> {
200 self.data_engine.clone()
201 }
202
203 fn risk_engine(&self) -> Option<RiskEngineConfig> {
204 self.risk_engine.clone()
205 }
206
207 fn exec_engine(&self) -> Option<ExecutionEngineConfig> {
208 self.exec_engine.clone()
209 }
210
211 fn portfolio(&self) -> Option<PortfolioConfig> {
212 self.portfolio
213 }
214
215 fn streaming(&self) -> Option<StreamingConfig> {
216 self.streaming.clone()
217 }
218}
219
220impl Default for KernelConfig {
221 fn default() -> Self {
222 Self::builder().build()
223 }
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228#[serde(rename_all = "snake_case")]
229pub enum RotationConfig {
230 Size {
232 max_size: u64,
234 },
235 Interval {
237 interval_ns: u64,
239 },
240 ScheduledDates {
242 interval_ns: u64,
244 schedule_ns: UnixNanos,
246 },
247 NoRotation,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
253#[builder(finish_fn(name = build_inner, vis = ""))]
254#[serde(deny_unknown_fields)]
255pub struct StreamingConfig {
256 pub catalog_path: String,
258 pub fs_protocol: String,
260 pub flush_interval_ms: u64,
262 pub replace_existing: bool,
264 pub rotation_config: RotationConfig,
266}
267
268impl<S: streaming_config_builder::IsComplete> StreamingConfigBuilder<S> {
269 pub fn build(self) -> ConfigResult<StreamingConfig> {
276 let config = self.build_inner();
277 config.validate()?;
278 Ok(config)
279 }
280}
281
282impl StreamingConfig {
283 #[must_use]
285 pub const fn new(
286 catalog_path: String,
287 fs_protocol: String,
288 flush_interval_ms: u64,
289 replace_existing: bool,
290 rotation_config: RotationConfig,
291 ) -> Self {
292 Self {
293 catalog_path,
294 fs_protocol,
295 flush_interval_ms,
296 replace_existing,
297 rotation_config,
298 }
299 }
300
301 pub fn validate(&self) -> ConfigResult<()> {
308 let mut errors = ConfigErrorCollector::new();
309
310 errors.check(
311 !self.catalog_path.trim().is_empty(),
312 ConfigError::empty_field("catalog_path"),
313 );
314 errors.check(
315 !self.fs_protocol.trim().is_empty(),
316 ConfigError::empty_field("fs_protocol"),
317 );
318
319 let flush_interval_ms = self.flush_interval_ms;
320 errors.check(
321 flush_interval_ms > 0,
322 ConfigError::range(
323 "flush_interval_ms",
324 format!("must be a positive number of milliseconds, was {flush_interval_ms}"),
325 ),
326 );
327
328 errors.into_result()
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use rstest::rstest;
335
336 use super::*;
337
338 #[rstest]
339 fn test_kernel_config_default_connection_timeout() {
340 let config = KernelConfig::default();
341
342 assert_eq!(config.timeout_connection, Duration::from_mins(1));
343 }
344
345 #[rstest]
346 fn test_streaming_config_builder_valid() {
347 let config = StreamingConfig::builder()
348 .catalog_path("/data/catalog".to_string())
349 .fs_protocol("file".to_string())
350 .flush_interval_ms(1_000)
351 .replace_existing(false)
352 .rotation_config(RotationConfig::NoRotation)
353 .build();
354
355 assert!(config.is_ok());
356 }
357
358 #[rstest]
359 fn test_streaming_config_zero_flush_interval_rejected() {
360 let result = StreamingConfig::builder()
361 .catalog_path("/data/catalog".to_string())
362 .fs_protocol("file".to_string())
363 .flush_interval_ms(0)
364 .replace_existing(false)
365 .rotation_config(RotationConfig::NoRotation)
366 .build();
367
368 assert!(
369 matches!(result, Err(ConfigError::Range { field, .. }) if field == "flush_interval_ms")
370 );
371 }
372
373 #[rstest]
374 fn test_streaming_config_empty_catalog_path_rejected() {
375 let result = StreamingConfig::builder()
376 .catalog_path(String::new())
377 .fs_protocol("file".to_string())
378 .flush_interval_ms(1_000)
379 .replace_existing(false)
380 .rotation_config(RotationConfig::NoRotation)
381 .build();
382
383 assert!(
384 matches!(result, Err(ConfigError::EmptyField { field }) if field == "catalog_path")
385 );
386 }
387
388 #[rstest]
389 fn test_streaming_config_toml_round_trip() {
390 let config: StreamingConfig = toml::from_str(
391 r#"
392catalog_path = "/data/catalog"
393fs_protocol = "file"
394flush_interval_ms = 1000
395replace_existing = false
396
397[rotation_config.size]
398max_size = 1048576
399"#,
400 )
401 .unwrap();
402
403 assert_eq!(config.catalog_path, "/data/catalog");
404 assert_eq!(config.fs_protocol, "file");
405 assert_eq!(config.flush_interval_ms, 1000);
406 assert!(!config.replace_existing);
407 assert!(matches!(
408 config.rotation_config,
409 RotationConfig::Size {
410 max_size: 1_048_576
411 }
412 ));
413 }
414
415 #[rstest]
416 fn test_streaming_config_with_no_rotation_toml() {
417 let config: StreamingConfig = toml::from_str(
418 r#"
419catalog_path = "/data/catalog"
420fs_protocol = "file"
421flush_interval_ms = 500
422replace_existing = true
423rotation_config = "no_rotation"
424"#,
425 )
426 .unwrap();
427
428 assert!(matches!(config.rotation_config, RotationConfig::NoRotation));
429 assert!(config.replace_existing);
430 }
431}