1use std::{collections::HashMap, str::FromStr, time::Duration};
19
20use ahash::AHashMap;
21use indexmap::IndexSet;
22use nautilus_common::{
23 cache::CacheConfig,
24 config::{
25 ConfigError, ConfigErrorCollector, ConfigResult, check_non_empty_field, check_range,
26 check_supported_field, check_valid_format,
27 },
28 enums::Environment,
29 logging::logger::LoggerConfig,
30 msgbus::MessageBusConfig,
31 throttler::RateLimit,
32};
33use nautilus_core::{
34 UUID4,
35 datetime::{
36 NANOSECONDS_IN_MILLISECOND, NANOSECONDS_IN_SECOND, checked_mins_to_nanos, secs_to_nanos,
37 },
38};
39use nautilus_data::engine::config::DataEngineConfig;
40use nautilus_execution::{
41 engine::config::ExecutionEngineConfig, order_emulator::config::OrderEmulatorConfig,
42};
43use nautilus_model::{
44 enums::{BarAggregation, BarIntervalType},
45 identifiers::{ClientId, ClientOrderId, InstrumentId, TraderId},
46};
47use nautilus_portfolio::config::PortfolioConfig;
48use nautilus_risk::engine::config::RiskEngineConfig;
49use nautilus_system::{
50 config::{NautilusKernelConfig, StreamingConfig},
51 event_store::EventStoreConfig,
52};
53use nautilus_trading::ImportableControllerConfig;
54use rust_decimal::Decimal;
55use serde::{Deserialize, Serialize};
56
57pub use super::queue::QueueMonitorConfig;
58use crate::execution::manager::ExecutionManagerConfig;
59
60const DEFAULT_ORDER_RATE_LIMIT: &str = "100/00:00:01";
62const RUST_RUNTIME_UNSUPPORTED: &str = "not supported by the Rust live runtime yet";
63const RATE_LIMIT_FORMAT: &str = "expected 'limit/HH:MM:SS'";
64
65pub(crate) fn validate_live_environment(environment: Environment) -> anyhow::Result<()> {
66 match environment {
67 Environment::Sandbox | Environment::Live => Ok(()),
68 Environment::Backtest => {
69 anyhow::bail!("LiveNode cannot be used with Backtest environment")
70 }
71 }
72}
73
74#[cfg_attr(
76 feature = "python",
77 pyo3::pyclass(module = "nautilus_trader.live", from_py_object)
78)]
79#[cfg_attr(
80 feature = "python",
81 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
82)]
83#[expect(
84 clippy::struct_excessive_bools,
85 reason = "config fields mirror the existing Python live data engine surface"
86)]
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
88#[serde(default, deny_unknown_fields)]
89pub struct LiveDataEngineConfig {
90 #[builder(default = true)]
92 pub time_bars_build_with_no_updates: bool,
93 #[builder(default = true)]
96 pub time_bars_timestamp_on_close: bool,
97 #[builder(default)]
99 pub time_bars_skip_first_non_full_bar: bool,
100 #[builder(default = BarIntervalType::LeftOpen)]
102 pub time_bars_interval_type: BarIntervalType,
103 #[builder(default)]
105 pub time_bars_build_delay: u64,
106 #[builder(default)]
110 pub time_bars_origin_offset: HashMap<String, u64>,
111 #[builder(default)]
113 pub validate_data_sequence: bool,
114 #[builder(default)]
116 pub buffer_deltas: bool,
117 #[builder(default)]
119 pub emit_quotes_from_book: bool,
120 #[builder(default)]
122 pub emit_quotes_from_book_depths: bool,
123 pub external_clients: Option<Vec<ClientId>>,
127 #[builder(default)]
129 pub debug: bool,
130 #[builder(default = 100_000)]
135 pub qsize: u32,
136}
137
138impl Default for LiveDataEngineConfig {
139 fn default() -> Self {
140 Self::builder().build()
141 }
142}
143
144impl From<LiveDataEngineConfig> for DataEngineConfig {
145 fn from(config: LiveDataEngineConfig) -> Self {
146 let time_bars_origin_offset = config
147 .time_bars_origin_offset
148 .into_iter()
149 .map(|(agg, nanos)| {
150 let agg = BarAggregation::from_str(&agg)
151 .expect("validate_runtime_support must run before DataEngineConfig conversion");
152 (agg, Duration::from_nanos(nanos))
153 })
154 .collect();
155
156 Self {
157 time_bars_build_with_no_updates: config.time_bars_build_with_no_updates,
158 time_bars_timestamp_on_close: config.time_bars_timestamp_on_close,
159 time_bars_skip_first_non_full_bar: config.time_bars_skip_first_non_full_bar,
160 time_bars_interval_type: config.time_bars_interval_type,
161 time_bars_build_delay: config.time_bars_build_delay,
162 time_bars_origin_offset,
163 validate_data_sequence: config.validate_data_sequence,
164 buffer_deltas: config.buffer_deltas,
165 emit_quotes_from_book: config.emit_quotes_from_book,
166 emit_quotes_from_book_depths: config.emit_quotes_from_book_depths,
167 disable_historical_cache: false,
168 external_clients: config.external_clients,
169 debug: config.debug,
170 }
171 }
172}
173
174#[cfg_attr(
176 feature = "python",
177 pyo3::pyclass(module = "nautilus_trader.live", from_py_object)
178)]
179#[cfg_attr(
180 feature = "python",
181 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
182)]
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
184#[serde(default, deny_unknown_fields)]
185pub struct LiveRiskEngineConfig {
186 #[builder(default)]
188 pub bypass: bool,
189 #[builder(default = DEFAULT_ORDER_RATE_LIMIT.to_string())]
191 pub max_order_submit_rate: String,
192 #[builder(default = DEFAULT_ORDER_RATE_LIMIT.to_string())]
194 pub max_order_modify_rate: String,
195 #[builder(default)]
199 pub max_notional_per_order: HashMap<String, String>,
200 #[builder(default)]
202 pub debug: bool,
203 #[builder(default = 100_000)]
208 pub qsize: u32,
209}
210
211impl Default for LiveRiskEngineConfig {
212 fn default() -> Self {
213 Self::builder().build()
214 }
215}
216
217impl From<LiveRiskEngineConfig> for RiskEngineConfig {
218 fn from(config: LiveRiskEngineConfig) -> Self {
219 let max_notional_per_order = config
220 .max_notional_per_order
221 .into_iter()
222 .map(|(instrument_id, notional)| {
223 let instrument_id = InstrumentId::from_str(&instrument_id)
224 .expect("validate_runtime_support must run before RiskEngineConfig conversion");
225 let notional = Decimal::from_str(¬ional)
226 .expect("validate_runtime_support must run before RiskEngineConfig conversion");
227 (instrument_id, notional)
228 })
229 .collect::<AHashMap<_, _>>();
230
231 Self {
232 bypass: config.bypass,
233 max_order_submit: parse_rate_limit(
234 "LiveRiskEngineConfig.max_order_submit_rate",
235 &config.max_order_submit_rate,
236 )
237 .expect("validate_runtime_support must run before RiskEngineConfig conversion"),
238 max_order_modify: parse_rate_limit(
239 "LiveRiskEngineConfig.max_order_modify_rate",
240 &config.max_order_modify_rate,
241 )
242 .expect("validate_runtime_support must run before RiskEngineConfig conversion"),
243 max_notional_per_order,
244 debug: config.debug,
245 }
246 }
247}
248
249pub(crate) fn parse_rate_limit(field: impl Into<String>, input: &str) -> ConfigResult<RateLimit> {
250 let field = field.into();
251 let (limit, interval) = input
252 .split_once('/')
253 .ok_or_else(|| ConfigError::invalid_format(field.clone(), RATE_LIMIT_FORMAT))?;
254
255 let limit = limit
256 .parse::<usize>()
257 .map_err(|e| ConfigError::invalid_format(field.clone(), format!("limit: {e}")))?;
258
259 let mut parts = interval.split(':');
260 let mut next = |label: &str| -> ConfigResult<u64> {
261 parts
262 .next()
263 .ok_or_else(|| {
264 ConfigError::invalid_format(field.clone(), format!("missing {label} component"))
265 })?
266 .parse::<u64>()
267 .map_err(|e| ConfigError::invalid_format(field.clone(), format!("{label}: {e}")))
268 };
269
270 let hours = next("hours")?;
271 let minutes = next("minutes")?;
272 let seconds = next("seconds")?;
273
274 check_valid_format(field.clone(), parts.next().is_none(), RATE_LIMIT_FORMAT)?;
275
276 let interval_ns = hours
277 .saturating_mul(3_600)
278 .saturating_add(minutes.saturating_mul(60))
279 .saturating_add(seconds)
280 .saturating_mul(NANOSECONDS_IN_SECOND);
281
282 RateLimit::new_checked(limit, interval_ns).map_err(|e| ConfigError::range(field, e.to_string()))
283}
284
285pub(crate) fn validate_max_notional_per_order(
286 field: &str,
287 max_notional_per_order: &HashMap<String, String>,
288) -> ConfigResult<()> {
289 let mut collector = ConfigErrorCollector::new();
290
291 for (instrument_id, notional) in max_notional_per_order {
292 let entry_path = format!("{field}[{instrument_id}]");
293 if let Err(e) = InstrumentId::from_str(instrument_id) {
294 collector.push(ConfigError::invalid_reference(
295 entry_path.clone(),
296 "instrument ID",
297 e.to_string(),
298 ));
299 }
300
301 if let Err(e) = Decimal::from_str(notional) {
302 collector.push(ConfigError::invalid_value(
303 entry_path,
304 format!("invalid notional: {e}"),
305 ));
306 }
307 }
308
309 collector.into_result()
310}
311
312pub(crate) fn validate_instrument_id_strings(field: &str, values: &[String]) -> ConfigResult<()> {
313 let mut collector = ConfigErrorCollector::new();
314
315 for (index, value) in values.iter().enumerate() {
316 if let Err(e) = InstrumentId::from_str(value) {
317 collector.push(ConfigError::invalid_reference(
318 format!("{field}[{index}]"),
319 "instrument ID",
320 e.to_string(),
321 ));
322 }
323 }
324
325 collector.into_result()
326}
327
328pub(crate) fn validate_client_order_id_strings(field: &str, values: &[String]) -> ConfigResult<()> {
329 let mut collector = ConfigErrorCollector::new();
330
331 for (index, value) in values.iter().enumerate() {
332 if let Err(e) = ClientOrderId::new_checked(value) {
333 collector.push(ConfigError::invalid_reference(
334 format!("{field}[{index}]"),
335 "client order ID",
336 e.to_string(),
337 ));
338 }
339 }
340
341 collector.into_result()
342}
343
344pub(crate) fn validate_non_negative_finite_f64(field: &str, value: f64) -> ConfigResult<()> {
345 check_range(
346 field,
347 value.is_finite() && value >= 0.0,
348 format!("{value} (must be a non-negative finite number)"),
349 )
350}
351
352pub(crate) fn validate_positive_interval_secs(field: &str, value: f64) -> ConfigResult<()> {
353 check_range(
354 field,
355 value.is_finite() && value > 0.0,
356 format!("{value} (must be a positive finite number)"),
357 )?;
358 let nanos = secs_to_nanos(value).map_err(|e| ConfigError::range(field, e.to_string()))?;
359 check_range(
360 field,
361 nanos > 0,
362 format!("{value} (must be at least one nanosecond)"),
363 )
364}
365
366#[cfg(feature = "python")]
367pub(crate) fn duration_from_secs_f64(field: &str, value: f64) -> ConfigResult<Duration> {
368 check_range(
369 field,
370 value.is_finite() && (0.0..=86_400.0).contains(&value),
371 format!("{value} (must be finite, non-negative, and <= 86400)"),
372 )?;
373
374 Ok(Duration::from_secs_f64(value))
375}
376
377#[cfg_attr(
379 feature = "python",
380 pyo3::pyclass(module = "nautilus_trader.live", from_py_object)
381)]
382#[cfg_attr(
383 feature = "python",
384 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
385)]
386#[expect(
387 clippy::struct_excessive_bools,
388 reason = "config fields mirror the existing Python live execution engine surface"
389)]
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, bon::Builder)]
391#[serde(default, deny_unknown_fields)]
392pub struct LiveExecEngineConfig {
393 #[builder(default = true)]
395 pub load_cache: bool,
396 #[builder(default)]
402 pub snapshot_orders: bool,
403 #[builder(default)]
409 pub snapshot_positions: bool,
410 pub snapshot_positions_interval_secs: Option<f64>,
413 pub external_clients: Option<Vec<ClientId>>,
418 #[builder(default)]
420 pub debug: bool,
421 #[builder(default = true)]
423 pub reconciliation: bool,
424 #[builder(default = 10.0)]
426 pub reconciliation_startup_delay_secs: f64,
427 pub reconciliation_lookback_mins: Option<u32>,
429 pub reconciliation_instrument_ids: Option<Vec<String>>,
431 #[builder(default)]
433 pub filter_unclaimed_external_orders: bool,
434 #[builder(default)]
436 pub filter_position_reports: bool,
437 pub filtered_client_order_ids: Option<Vec<String>>,
439 #[builder(default = true)]
441 pub generate_missing_orders: bool,
442 #[builder(default = 2_000)]
444 pub inflight_check_interval_ms: u32,
445 #[builder(default = 5_000)]
447 pub inflight_check_threshold_ms: u32,
448 #[builder(default = 5)]
450 pub inflight_check_retries: u32,
451 pub open_check_interval_secs: Option<f64>,
453 pub open_check_lookback_mins: Option<u32>,
456 #[builder(default = 5_000)]
458 pub open_check_threshold_ms: u32,
459 #[builder(default = 5)]
461 pub open_check_missing_retries: u32,
462 #[builder(default = true)]
464 pub open_check_open_only: bool,
465 #[builder(default = 10)]
467 pub max_single_order_queries_per_cycle: u32,
468 #[builder(default = 100)]
470 pub single_order_query_delay_ms: u32,
471 pub position_check_interval_secs: Option<f64>,
473 #[builder(default = 60)]
475 pub position_check_lookback_mins: u32,
476 #[builder(default = 5_000)]
478 pub position_check_threshold_ms: u32,
479 #[builder(default = 3)]
481 pub position_check_retries: u32,
482 pub purge_closed_orders_interval_mins: Option<u32>,
484 pub purge_closed_orders_buffer_mins: Option<u32>,
486 pub purge_closed_positions_interval_mins: Option<u32>,
488 pub purge_closed_positions_buffer_mins: Option<u32>,
490 pub purge_account_events_interval_mins: Option<u32>,
492 pub purge_account_events_lookback_mins: Option<u32>,
494 #[builder(default)]
496 pub purge_from_database: bool,
497 pub own_books_audit_interval_secs: Option<f64>,
499 #[builder(default = 100_000)]
501 pub qsize: u32,
502 #[builder(default)]
505 pub allow_overfills: bool,
506 #[builder(default)]
508 pub manage_own_order_books: bool,
509}
510
511impl Default for LiveExecEngineConfig {
512 fn default() -> Self {
513 Self {
514 open_check_lookback_mins: Some(60),
515 ..Self::builder().build()
516 }
517 }
518}
519
520impl From<LiveExecEngineConfig> for ExecutionEngineConfig {
521 fn from(config: LiveExecEngineConfig) -> Self {
522 Self {
523 load_cache: config.load_cache,
524 manage_own_order_books: config.manage_own_order_books,
525 snapshot_orders: config.snapshot_orders,
526 snapshot_positions: config.snapshot_positions,
527 snapshot_positions_interval_secs: config.snapshot_positions_interval_secs,
528 carry_replay_events_on_reopen: true,
530 allow_overfills: config.allow_overfills,
531 filter_unclaimed_external_orders: config.filter_unclaimed_external_orders,
532 external_clients: config.external_clients,
533 purge_closed_orders_interval_mins: config.purge_closed_orders_interval_mins,
538 purge_closed_orders_buffer_mins: config.purge_closed_orders_buffer_mins,
539 purge_closed_positions_interval_mins: config.purge_closed_positions_interval_mins,
540 purge_closed_positions_buffer_mins: config.purge_closed_positions_buffer_mins,
541 purge_account_events_interval_mins: config.purge_account_events_interval_mins,
542 purge_account_events_lookback_mins: config.purge_account_events_lookback_mins,
543 purge_from_database: config.purge_from_database,
544 debug: config.debug,
545 }
546 }
547}
548
549impl From<&LiveExecEngineConfig> for ExecutionManagerConfig {
550 fn from(config: &LiveExecEngineConfig) -> Self {
551 let filtered_client_order_ids: IndexSet<ClientOrderId> = config
552 .filtered_client_order_ids
553 .clone()
554 .unwrap_or_default()
555 .into_iter()
556 .map(|value| ClientOrderId::from(value.as_str()))
557 .collect();
558
559 let reconciliation_instrument_ids: IndexSet<InstrumentId> = config
560 .reconciliation_instrument_ids
561 .clone()
562 .unwrap_or_default()
563 .into_iter()
564 .map(InstrumentId::from)
565 .collect();
566
567 let open_check_threshold_ns =
568 u64::from(config.open_check_threshold_ms) * NANOSECONDS_IN_MILLISECOND;
569 let position_check_threshold_ns =
570 u64::from(config.position_check_threshold_ms) * NANOSECONDS_IN_MILLISECOND;
571
572 Self {
573 trader_id: TraderId::default(),
574 reconciliation: config.reconciliation,
575 lookback_mins: config.reconciliation_lookback_mins.map(u64::from),
576 reconciliation_instrument_ids,
577 filter_unclaimed_external: config.filter_unclaimed_external_orders,
578 filter_position_reports: config.filter_position_reports,
579 filtered_client_order_ids,
580 generate_missing_orders: config.generate_missing_orders,
581 inflight_check_interval_ms: config.inflight_check_interval_ms,
582 inflight_threshold_ms: u64::from(config.inflight_check_threshold_ms),
583 inflight_max_retries: config.inflight_check_retries,
584 open_check_interval_secs: config.open_check_interval_secs,
585 open_check_lookback_mins: config.open_check_lookback_mins.map(u64::from),
586 open_check_threshold_ns,
587 open_check_missing_retries: config.open_check_missing_retries,
588 open_check_open_only: config.open_check_open_only,
589 max_single_order_queries_per_cycle: config.max_single_order_queries_per_cycle,
590 single_order_query_delay_ms: config.single_order_query_delay_ms,
591 position_check_interval_secs: config.position_check_interval_secs,
592 position_check_lookback_mins: u64::from(config.position_check_lookback_mins),
593 position_check_threshold_ns,
594 position_check_retries: config.position_check_retries,
595 purge_closed_orders_buffer_mins: config.purge_closed_orders_buffer_mins,
596 purge_closed_positions_buffer_mins: config.purge_closed_positions_buffer_mins,
597 purge_account_events_lookback_mins: config.purge_account_events_lookback_mins,
598 purge_from_database: config.purge_from_database,
599 }
600 }
601}
602
603#[cfg_attr(
605 feature = "python",
606 pyo3::pyclass(module = "nautilus_trader.live", from_py_object)
607)]
608#[cfg_attr(
609 feature = "python",
610 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
611)]
612#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, bon::Builder)]
613#[serde(default, deny_unknown_fields)]
614pub struct RoutingConfig {
615 #[builder(default)]
617 pub default: bool,
618 pub venues: Option<Vec<String>>,
620}
621
622#[cfg_attr(
624 feature = "python",
625 pyo3::pyclass(module = "nautilus_trader.live", from_py_object)
626)]
627#[cfg_attr(
628 feature = "python",
629 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
630)]
631#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, bon::Builder)]
632#[serde(default, deny_unknown_fields)]
633pub struct InstrumentProviderConfig {
634 #[builder(default)]
636 pub load_all: bool,
637 pub load_ids: Option<Vec<String>>,
639 #[builder(default)]
641 pub filters: HashMap<String, serde_json::Value>,
642 pub filter_callable: Option<String>,
644 #[builder(default = true)]
646 pub log_warnings: bool,
647}
648
649impl Default for InstrumentProviderConfig {
650 fn default() -> Self {
651 Self::builder().build()
652 }
653}
654
655#[cfg_attr(
657 feature = "python",
658 pyo3::pyclass(module = "nautilus_trader.live", from_py_object)
659)]
660#[cfg_attr(
661 feature = "python",
662 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
663)]
664#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, bon::Builder)]
665#[serde(default, deny_unknown_fields)]
666pub struct LiveDataClientConfig {
667 #[builder(default)]
669 pub handle_revised_bars: bool,
670 #[builder(default)]
672 pub instrument_provider: InstrumentProviderConfig,
673 #[builder(default)]
675 pub routing: RoutingConfig,
676}
677
678#[cfg_attr(
680 feature = "python",
681 pyo3::pyclass(module = "nautilus_trader.live", from_py_object)
682)]
683#[cfg_attr(
684 feature = "python",
685 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
686)]
687#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, bon::Builder)]
688#[serde(default, deny_unknown_fields)]
689pub struct LiveExecClientConfig {
690 #[builder(default)]
692 pub instrument_provider: InstrumentProviderConfig,
693 #[builder(default)]
695 pub routing: RoutingConfig,
696}
697
698#[cfg_attr(
700 feature = "python",
701 pyo3::pyclass(module = "nautilus_trader.live", from_py_object)
702)]
703#[cfg_attr(
704 feature = "python",
705 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
706)]
707#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, bon::Builder)]
708#[serde(default, deny_unknown_fields)]
709pub struct PluginConfig {
710 pub path: String,
712 pub type_name: String,
714 #[builder(default)]
716 pub config: HashMap<String, serde_json::Value>,
717 pub sha256: Option<String>,
719}
720
721impl Default for PluginConfig {
722 fn default() -> Self {
723 Self::builder()
724 .path(String::new())
725 .type_name(String::new())
726 .build()
727 }
728}
729
730#[cfg_attr(
732 feature = "python",
733 pyo3::pyclass(module = "nautilus_trader.live", from_py_object)
734)]
735#[cfg_attr(
736 feature = "python",
737 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
738)]
739#[expect(
740 clippy::struct_excessive_bools,
741 reason = "config fields mirror the existing Python live node surface"
742)]
743#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
744#[serde(default, deny_unknown_fields)]
745pub struct LiveNodeConfig {
746 #[builder(default = Environment::Live)]
748 pub environment: Environment,
749 #[builder(default = TraderId::from("TRADER-001"))]
751 pub trader_id: TraderId,
752 #[builder(default)]
754 pub load_state: bool,
755 #[builder(default)]
757 pub save_state: bool,
758 #[builder(default)]
762 pub shutdown_on_error: bool,
763 #[builder(default)]
765 pub logging: LoggerConfig,
766 pub instance_id: Option<UUID4>,
768 #[builder(default = Duration::from_mins(1))]
770 pub timeout_connection: Duration,
771 #[builder(default = Duration::from_secs(30))]
773 pub timeout_reconciliation: Duration,
774 #[builder(default = Duration::from_secs(10))]
776 pub timeout_portfolio: Duration,
777 #[builder(default = Duration::from_secs(10))]
779 pub timeout_disconnection: Duration,
780 #[builder(default = Duration::from_secs(10))]
782 pub delay_post_stop: Duration,
783 #[builder(default = Duration::from_secs(5))]
785 pub timeout_shutdown: Duration,
786 pub cache: Option<CacheConfig>,
788 pub msgbus: Option<MessageBusConfig>,
790 pub portfolio: Option<PortfolioConfig>,
792 pub emulator: Option<OrderEmulatorConfig>,
794 pub streaming: Option<StreamingConfig>,
796 pub queue_monitor: Option<QueueMonitorConfig>,
798 pub event_store: Option<EventStoreConfig>,
804 #[builder(default)]
806 pub loop_debug: bool,
807 #[builder(default)]
809 pub data_engine: LiveDataEngineConfig,
810 #[builder(default)]
812 pub risk_engine: LiveRiskEngineConfig,
813 #[builder(default)]
815 pub exec_engine: LiveExecEngineConfig,
816 #[builder(default)]
818 pub data_clients: HashMap<String, LiveDataClientConfig>,
819 #[builder(default)]
821 pub exec_clients: HashMap<String, LiveExecClientConfig>,
822 pub controller: Option<ImportableControllerConfig>,
824 #[builder(default)]
826 pub plugins: Vec<PluginConfig>,
827}
828
829impl Default for LiveNodeConfig {
830 fn default() -> Self {
831 Self::builder().build()
832 }
833}
834
835impl LiveNodeConfig {
836 pub(crate) fn validate_runtime_support(&self) -> ConfigResult<()> {
844 let mut collector = ConfigErrorCollector::new();
845
846 collector.collect(check_supported_field(
847 "LiveNodeConfig.streaming",
848 self.streaming.is_none(),
849 RUST_RUNTIME_UNSUPPORTED,
850 ));
851 collector.collect(check_supported_field(
852 "LiveNodeConfig.emulator",
853 self.emulator.is_none(),
854 RUST_RUNTIME_UNSUPPORTED,
855 ));
856 collector.collect(check_supported_field(
857 "LiveNodeConfig.loop_debug",
858 !self.loop_debug,
859 RUST_RUNTIME_UNSUPPORTED,
860 ));
861 collector.collect(self.data_engine.validate_runtime_support());
862 collector.collect(self.risk_engine.validate_runtime_support());
863 collector.collect(self.exec_engine.validate_runtime_support());
864
865 if let Some(queue_monitor) = &self.queue_monitor {
866 collector.collect(queue_monitor.validate());
867 }
868 collector.collect(self.validate_plugin_configs());
869
870 collector.into_result()
871 }
872
873 fn validate_plugin_configs(&self) -> ConfigResult<()> {
874 let mut collector = ConfigErrorCollector::new();
875
876 for (index, plugin) in self.plugins.iter().enumerate() {
877 collector.collect(plugin.validate_runtime_support(index));
878 }
879
880 collector.into_result()
881 }
882}
883
884impl PluginConfig {
885 pub(crate) fn validate_runtime_support(&self, index: usize) -> ConfigResult<()> {
886 let mut collector = ConfigErrorCollector::with_capacity(3);
887
888 collector.collect(check_non_empty_field(
889 format!("LiveNodeConfig.plugins[{index}].path"),
890 &self.path,
891 ));
892 collector.collect(check_non_empty_field(
893 format!("LiveNodeConfig.plugins[{index}].type_name"),
894 &self.type_name,
895 ));
896
897 if let Some(sha256) = &self.sha256 {
898 let valid = sha256.len() == 64 && sha256.bytes().all(|b| b.is_ascii_hexdigit());
899 collector.collect(check_valid_format(
900 format!("LiveNodeConfig.plugins[{index}].sha256"),
901 valid,
902 "must be a 64-character hex digest",
903 ));
904 }
905
906 collector.into_result()
907 }
908}
909
910impl LiveDataEngineConfig {
911 fn validate_runtime_support(&self) -> ConfigResult<()> {
912 let mut collector = ConfigErrorCollector::new();
913
914 for agg_str in self.time_bars_origin_offset.keys() {
915 if let Err(e) = BarAggregation::from_str(agg_str) {
916 collector.push(ConfigError::invalid_reference(
917 format!("LiveDataEngineConfig.time_bars_origin_offset[{agg_str}]"),
918 "bar aggregation",
919 e.to_string(),
920 ));
921 }
922 }
923
924 let default = Self::default();
925 collector.collect(check_supported_field(
926 "LiveDataEngineConfig.qsize",
927 self.qsize == default.qsize,
928 RUST_RUNTIME_UNSUPPORTED,
929 ));
930
931 collector.into_result()
932 }
933}
934
935impl LiveRiskEngineConfig {
936 fn validate_runtime_support(&self) -> ConfigResult<()> {
937 let mut collector = ConfigErrorCollector::new();
938
939 collector.collect(
940 parse_rate_limit(
941 "LiveRiskEngineConfig.max_order_submit_rate",
942 &self.max_order_submit_rate,
943 )
944 .map(|_| ()),
945 );
946 collector.collect(
947 parse_rate_limit(
948 "LiveRiskEngineConfig.max_order_modify_rate",
949 &self.max_order_modify_rate,
950 )
951 .map(|_| ()),
952 );
953 collector.collect(validate_max_notional_per_order(
954 "LiveRiskEngineConfig.max_notional_per_order",
955 &self.max_notional_per_order,
956 ));
957
958 let default = Self::default();
959 collector.collect(check_supported_field(
960 "LiveRiskEngineConfig.qsize",
961 self.qsize == default.qsize,
962 RUST_RUNTIME_UNSUPPORTED,
963 ));
964
965 collector.into_result()
966 }
967}
968
969impl LiveExecEngineConfig {
970 pub(crate) fn validate_runtime_support(&self) -> ConfigResult<()> {
971 let mut collector = ConfigErrorCollector::new();
972
973 collector.collect(validate_non_negative_finite_f64(
977 "LiveExecEngineConfig.reconciliation_startup_delay_secs",
978 self.reconciliation_startup_delay_secs,
979 ));
980
981 for (field, value) in [
982 (
983 "LiveExecEngineConfig.snapshot_positions_interval_secs",
984 self.snapshot_positions_interval_secs,
985 ),
986 (
987 "LiveExecEngineConfig.open_check_interval_secs",
988 self.open_check_interval_secs,
989 ),
990 (
991 "LiveExecEngineConfig.position_check_interval_secs",
992 self.position_check_interval_secs,
993 ),
994 (
995 "LiveExecEngineConfig.own_books_audit_interval_secs",
996 self.own_books_audit_interval_secs,
997 ),
998 ] {
999 if let Some(value) = value {
1000 collector.collect(validate_positive_interval_secs(field, value));
1001 }
1002 }
1003
1004 for (field, value) in [
1005 (
1006 "LiveExecEngineConfig.open_check_lookback_mins",
1007 self.open_check_lookback_mins,
1008 ),
1009 (
1010 "LiveExecEngineConfig.purge_closed_orders_interval_mins",
1011 self.purge_closed_orders_interval_mins,
1012 ),
1013 (
1014 "LiveExecEngineConfig.purge_closed_positions_interval_mins",
1015 self.purge_closed_positions_interval_mins,
1016 ),
1017 (
1018 "LiveExecEngineConfig.purge_account_events_interval_mins",
1019 self.purge_account_events_interval_mins,
1020 ),
1021 (
1022 "LiveExecEngineConfig.purge_closed_orders_buffer_mins",
1023 self.purge_closed_orders_buffer_mins,
1024 ),
1025 (
1026 "LiveExecEngineConfig.purge_closed_positions_buffer_mins",
1027 self.purge_closed_positions_buffer_mins,
1028 ),
1029 (
1030 "LiveExecEngineConfig.purge_account_events_lookback_mins",
1031 self.purge_account_events_lookback_mins,
1032 ),
1033 ] {
1034 if let Some(mins) = value {
1035 collector.collect(check_range(
1036 field,
1037 checked_mins_to_nanos(u64::from(mins)).is_some(),
1038 format!("{mins} minutes (must fit in `u64` nanoseconds)"),
1039 ));
1040 }
1041 }
1042
1043 if let Some(instrument_ids) = &self.reconciliation_instrument_ids {
1044 collector.collect(validate_instrument_id_strings(
1045 "LiveExecEngineConfig.reconciliation_instrument_ids",
1046 instrument_ids,
1047 ));
1048 }
1049
1050 if let Some(client_order_ids) = &self.filtered_client_order_ids {
1051 collector.collect(validate_client_order_id_strings(
1052 "LiveExecEngineConfig.filtered_client_order_ids",
1053 client_order_ids,
1054 ));
1055 }
1056
1057 let default = Self::default();
1058 collector.collect(check_supported_field(
1059 "LiveExecEngineConfig.snapshot_orders",
1060 self.snapshot_orders == default.snapshot_orders,
1061 RUST_RUNTIME_UNSUPPORTED,
1062 ));
1063 collector.collect(check_supported_field(
1064 "LiveExecEngineConfig.snapshot_positions",
1065 self.snapshot_positions == default.snapshot_positions,
1066 RUST_RUNTIME_UNSUPPORTED,
1067 ));
1068 collector.collect(check_supported_field(
1069 "LiveExecEngineConfig.purge_from_database",
1070 self.purge_from_database == default.purge_from_database,
1071 RUST_RUNTIME_UNSUPPORTED,
1072 ));
1073 collector.collect(check_supported_field(
1074 "LiveExecEngineConfig.qsize",
1075 self.qsize == default.qsize,
1076 RUST_RUNTIME_UNSUPPORTED,
1077 ));
1078
1079 collector.into_result()
1080 }
1081}
1082
1083impl NautilusKernelConfig for LiveNodeConfig {
1084 fn environment(&self) -> Environment {
1085 self.environment
1086 }
1087
1088 fn trader_id(&self) -> TraderId {
1089 self.trader_id
1090 }
1091
1092 fn load_state(&self) -> bool {
1093 self.load_state
1094 }
1095
1096 fn save_state(&self) -> bool {
1097 self.save_state
1098 }
1099
1100 fn shutdown_on_error(&self) -> bool {
1101 self.shutdown_on_error
1102 }
1103
1104 fn logging(&self) -> LoggerConfig {
1105 self.logging.clone()
1106 }
1107
1108 fn instance_id(&self) -> Option<UUID4> {
1109 self.instance_id
1110 }
1111
1112 fn timeout_connection(&self) -> Duration {
1113 self.timeout_connection
1114 }
1115
1116 fn timeout_reconciliation(&self) -> Duration {
1117 self.timeout_reconciliation
1118 }
1119
1120 fn timeout_portfolio(&self) -> Duration {
1121 self.timeout_portfolio
1122 }
1123
1124 fn timeout_disconnection(&self) -> Duration {
1125 self.timeout_disconnection
1126 }
1127
1128 fn delay_post_stop(&self) -> Duration {
1129 self.delay_post_stop
1130 }
1131
1132 fn timeout_shutdown(&self) -> Duration {
1133 self.timeout_shutdown
1134 }
1135
1136 fn cache(&self) -> Option<CacheConfig> {
1137 self.cache.clone()
1138 }
1139
1140 fn msgbus(&self) -> Option<MessageBusConfig> {
1141 self.msgbus.clone()
1142 }
1143
1144 fn data_engine(&self) -> Option<DataEngineConfig> {
1145 Some(self.data_engine.clone().into())
1146 }
1147
1148 fn risk_engine(&self) -> Option<RiskEngineConfig> {
1149 Some(self.risk_engine.clone().into())
1150 }
1151
1152 fn exec_engine(&self) -> Option<ExecutionEngineConfig> {
1153 Some(self.exec_engine.clone().into())
1154 }
1155
1156 fn portfolio(&self) -> Option<PortfolioConfig> {
1157 self.portfolio
1158 }
1159
1160 fn streaming(&self) -> Option<StreamingConfig> {
1161 self.streaming.clone()
1162 }
1163}
1164
1165#[cfg(test)]
1166mod tests {
1167 use nautilus_system::config::RotationConfig;
1168 use rstest::rstest;
1169
1170 use super::*;
1171
1172 #[rstest]
1173 fn test_trading_node_config_default() {
1174 let config = LiveNodeConfig::default();
1175
1176 assert_eq!(config.environment, Environment::Live);
1177 assert_eq!(config.trader_id, TraderId::from("TRADER-001"));
1178 assert_eq!(config.data_engine.qsize, 100_000);
1179 assert_eq!(config.risk_engine.qsize, 100_000);
1180 assert_eq!(config.exec_engine.qsize, 100_000);
1181 assert_eq!(config.timeout_connection, Duration::from_mins(1));
1182 assert!(config.exec_engine.reconciliation);
1183 assert!(!config.exec_engine.filter_unclaimed_external_orders);
1184 assert!(config.data_clients.is_empty());
1185 assert!(config.exec_clients.is_empty());
1186 assert!(config.plugins.is_empty());
1187 assert!(config.queue_monitor.is_none());
1188 }
1189
1190 #[rstest]
1191 fn test_live_node_queue_monitor_config_serde_roundtrip() {
1192 let config: LiveNodeConfig = toml::from_str(
1193 "
1194[queue_monitor]
1195queue_depth_trigger = 100
1196queue_depth_clear = 60
1197mean_dispatch_ns_trigger = 1000
1198mean_dispatch_ns_clear = 700
1199",
1200 )
1201 .unwrap();
1202
1203 let expected = Some(
1204 QueueMonitorConfig::builder()
1205 .queue_depth_trigger(100)
1206 .queue_depth_clear(60)
1207 .mean_dispatch_ns_trigger(1_000)
1208 .mean_dispatch_ns_clear(700)
1209 .build(),
1210 );
1211 let json = serde_json::to_string(&config).unwrap();
1212 let restored: LiveNodeConfig = serde_json::from_str(&json).unwrap();
1213
1214 assert_eq!(config.queue_monitor, expected);
1215 assert_eq!(restored.queue_monitor, expected);
1216 }
1217
1218 #[rstest]
1219 #[case(
1220 QueueMonitorConfig {
1221 queue_depth_trigger: 10,
1222 queue_depth_clear: 10,
1223 mean_dispatch_ns_trigger: 100,
1224 mean_dispatch_ns_clear: 50,
1225 },
1226 "invalid LiveNodeConfig.queue_monitor.queue_depth: clear threshold 10 must be lower than trigger threshold 10"
1227 )]
1228 #[case(
1229 QueueMonitorConfig {
1230 queue_depth_trigger: 10,
1231 queue_depth_clear: 5,
1232 mean_dispatch_ns_trigger: 50,
1233 mean_dispatch_ns_clear: 50,
1234 },
1235 "invalid LiveNodeConfig.queue_monitor.mean_dispatch_ns: clear threshold 50 must be lower than trigger threshold 50"
1236 )]
1237 fn test_live_node_queue_monitor_config_validates_hysteresis(
1238 #[case] queue_monitor: QueueMonitorConfig,
1239 #[case] expected: &str,
1240 ) {
1241 let config = LiveNodeConfig {
1242 queue_monitor: Some(queue_monitor),
1243 ..Default::default()
1244 };
1245
1246 assert_eq!(
1247 config.validate_runtime_support().unwrap_err().to_string(),
1248 expected
1249 );
1250 }
1251
1252 #[rstest]
1253 fn test_trading_node_config_as_kernel_config() {
1254 let config = LiveNodeConfig::default();
1255
1256 assert_eq!(config.environment(), Environment::Live);
1257 assert_eq!(config.trader_id(), TraderId::from("TRADER-001"));
1258 assert!(config.data_engine().is_some());
1259 assert!(config.risk_engine().is_some());
1260 assert!(config.exec_engine().is_some());
1261 assert!(!config.load_state());
1262 assert!(!config.save_state());
1263 }
1264
1265 #[rstest]
1266 fn test_validate_runtime_support_with_defaults() {
1267 let config = LiveNodeConfig::default();
1268
1269 assert!(config.validate_runtime_support().is_ok());
1270 }
1271
1272 #[rstest]
1273 fn test_validate_runtime_support_accepts_msgbus_config() {
1274 let config = LiveNodeConfig {
1275 msgbus: Some(MessageBusConfig::default()),
1276 ..Default::default()
1277 };
1278
1279 assert!(config.validate_runtime_support().is_ok());
1280 }
1281
1282 #[rstest]
1283 fn test_validate_runtime_support_accepts_msgbus_external_streams() {
1284 let config = LiveNodeConfig {
1285 msgbus: Some(MessageBusConfig {
1286 external_streams: Some(vec!["stream".to_string()]),
1287 ..Default::default()
1288 }),
1289 ..Default::default()
1290 };
1291
1292 assert!(config.validate_runtime_support().is_ok());
1293 }
1294
1295 #[rstest]
1296 fn test_validate_runtime_support_rejects_streaming_config() {
1297 let config = LiveNodeConfig {
1298 streaming: Some(StreamingConfig::new(
1299 "catalog".to_string(),
1300 "file".to_string(),
1301 1_000,
1302 false,
1303 RotationConfig::NoRotation,
1304 )),
1305 ..Default::default()
1306 };
1307
1308 let error = config.validate_runtime_support().unwrap_err();
1309 assert_eq!(
1310 error.to_string(),
1311 "LiveNodeConfig.streaming is not supported by the Rust live runtime yet"
1312 );
1313 }
1314
1315 #[rstest]
1316 fn test_validate_runtime_support_collects_multiple_errors() {
1317 let config = LiveNodeConfig {
1318 msgbus: Some(MessageBusConfig {
1319 external_streams: Some(vec!["stream".to_string()]),
1320 ..Default::default()
1321 }),
1322 streaming: Some(StreamingConfig::new(
1323 "catalog".to_string(),
1324 "file".to_string(),
1325 1_000,
1326 false,
1327 RotationConfig::NoRotation,
1328 )),
1329 loop_debug: true,
1330 ..Default::default()
1331 };
1332
1333 let error = config.validate_runtime_support().unwrap_err();
1334
1335 match error {
1336 ConfigError::Multiple { errors } => {
1337 assert_eq!(errors.len(), 2);
1338 assert_eq!(
1339 errors[0],
1340 ConfigError::UnsupportedField {
1341 field: "LiveNodeConfig.streaming".to_string(),
1342 reason: RUST_RUNTIME_UNSUPPORTED.to_string(),
1343 },
1344 );
1345 assert_eq!(
1346 errors[1],
1347 ConfigError::UnsupportedField {
1348 field: "LiveNodeConfig.loop_debug".to_string(),
1349 reason: RUST_RUNTIME_UNSUPPORTED.to_string(),
1350 },
1351 );
1352 }
1353 _ => panic!("Expected multiple config errors, received {error:?}"),
1354 }
1355 }
1356
1357 #[rstest]
1358 fn test_validate_runtime_support_rejects_data_engine_qsize() {
1359 let config = LiveNodeConfig {
1360 data_engine: LiveDataEngineConfig {
1361 qsize: 1,
1362 ..Default::default()
1363 },
1364 ..Default::default()
1365 };
1366
1367 let error = config.validate_runtime_support().unwrap_err();
1368 assert_eq!(
1369 error.to_string(),
1370 "LiveDataEngineConfig.qsize is not supported by the Rust live runtime yet"
1371 );
1372 }
1373
1374 #[rstest]
1375 fn test_validate_runtime_support_rejects_risk_engine_qsize() {
1376 let config = LiveNodeConfig {
1377 risk_engine: LiveRiskEngineConfig {
1378 qsize: 1,
1379 ..Default::default()
1380 },
1381 ..Default::default()
1382 };
1383
1384 let error = config.validate_runtime_support().unwrap_err();
1385 assert_eq!(
1386 error.to_string(),
1387 "LiveRiskEngineConfig.qsize is not supported by the Rust live runtime yet"
1388 );
1389 }
1390
1391 #[rstest]
1392 fn test_live_data_engine_config_converts_to_data_engine_config() {
1393 let config = LiveDataEngineConfig {
1394 time_bars_build_with_no_updates: false,
1395 time_bars_timestamp_on_close: false,
1396 time_bars_skip_first_non_full_bar: true,
1397 time_bars_interval_type: BarIntervalType::RightOpen,
1398 time_bars_build_delay: 1_500,
1399 validate_data_sequence: true,
1400 buffer_deltas: true,
1401 external_clients: Some(vec![ClientId::from("EXTERNAL")]),
1402 debug: true,
1403 ..Default::default()
1404 };
1405
1406 let converted: DataEngineConfig = config.into();
1407
1408 assert!(!converted.time_bars_build_with_no_updates);
1409 assert!(!converted.time_bars_timestamp_on_close);
1410 assert!(converted.time_bars_skip_first_non_full_bar);
1411 assert_eq!(
1412 converted.time_bars_interval_type,
1413 BarIntervalType::RightOpen,
1414 );
1415 assert_eq!(converted.time_bars_build_delay, 1_500);
1416 assert!(converted.time_bars_origin_offset.is_empty());
1417 assert!(converted.validate_data_sequence);
1418 assert!(converted.buffer_deltas);
1419 assert!(!converted.emit_quotes_from_book);
1420 assert!(!converted.emit_quotes_from_book_depths);
1421 assert_eq!(
1422 converted.external_clients,
1423 Some(vec![ClientId::from("EXTERNAL")]),
1424 );
1425 assert!(converted.debug);
1426 }
1427
1428 #[rstest]
1429 fn test_live_data_engine_config_converts_time_bars_origin_offset() {
1430 let config = LiveDataEngineConfig {
1431 time_bars_origin_offset: HashMap::from([("Minute".to_string(), 5_000_000_000)]),
1432 emit_quotes_from_book: true,
1433 emit_quotes_from_book_depths: true,
1434 ..Default::default()
1435 };
1436
1437 let converted: DataEngineConfig = config.into();
1438
1439 assert_eq!(converted.time_bars_origin_offset.len(), 1);
1440 assert_eq!(
1441 converted.time_bars_origin_offset[&BarAggregation::Minute],
1442 Duration::from_secs(5),
1443 );
1444 assert!(converted.emit_quotes_from_book);
1445 assert!(converted.emit_quotes_from_book_depths);
1446 }
1447
1448 #[rstest]
1449 fn test_live_exec_engine_config_converts_to_exec_engine_config() {
1450 let config = LiveExecEngineConfig {
1451 load_cache: false,
1452 snapshot_positions_interval_secs: Some(30.0),
1453 filter_unclaimed_external_orders: true,
1454 purge_closed_orders_interval_mins: Some(5),
1455 purge_closed_orders_buffer_mins: Some(1),
1456 purge_closed_positions_interval_mins: Some(10),
1457 purge_closed_positions_buffer_mins: Some(2),
1458 purge_account_events_interval_mins: Some(15),
1459 purge_account_events_lookback_mins: Some(3),
1460 ..Default::default()
1461 };
1462
1463 let converted: ExecutionEngineConfig = config.into();
1464
1465 assert!(!converted.load_cache);
1466 assert_eq!(converted.snapshot_positions_interval_secs, Some(30.0));
1467 assert!(converted.filter_unclaimed_external_orders);
1468 assert_eq!(converted.purge_closed_orders_interval_mins, Some(5));
1469 assert_eq!(converted.purge_closed_orders_buffer_mins, Some(1));
1470 assert_eq!(converted.purge_closed_positions_interval_mins, Some(10));
1471 assert_eq!(converted.purge_closed_positions_buffer_mins, Some(2));
1472 assert_eq!(converted.purge_account_events_interval_mins, Some(15));
1473 assert_eq!(converted.purge_account_events_lookback_mins, Some(3));
1474 assert!(converted.carry_replay_events_on_reopen);
1476 }
1477
1478 #[rstest]
1479 fn test_live_exec_engine_config_converts_to_execution_manager_config() {
1480 let config = LiveExecEngineConfig {
1481 reconciliation: false,
1482 reconciliation_lookback_mins: Some(45),
1483 reconciliation_instrument_ids: Some(vec![
1484 "ETHUSDT.BINANCE".to_string(),
1485 "BTCUSDT.BINANCE".to_string(),
1486 ]),
1487 filter_unclaimed_external_orders: true,
1488 filter_position_reports: true,
1489 filtered_client_order_ids: Some(vec!["O-001".to_string(), "O-002".to_string()]),
1490 generate_missing_orders: false,
1491 inflight_check_interval_ms: 321,
1492 inflight_check_threshold_ms: 654,
1493 inflight_check_retries: 7,
1494 open_check_interval_secs: Some(1.5),
1495 open_check_lookback_mins: Some(9),
1496 open_check_threshold_ms: 234,
1497 open_check_missing_retries: 4,
1498 open_check_open_only: false,
1499 max_single_order_queries_per_cycle: 8,
1500 single_order_query_delay_ms: 76,
1501 position_check_interval_secs: Some(2.5),
1502 position_check_lookback_mins: 11,
1503 position_check_threshold_ms: 345,
1504 position_check_retries: 6,
1505 purge_closed_orders_buffer_mins: Some(12),
1506 purge_closed_positions_buffer_mins: Some(13),
1507 purge_account_events_lookback_mins: Some(14),
1508 purge_from_database: true,
1509 ..Default::default()
1510 };
1511
1512 let converted = ExecutionManagerConfig::from(&config);
1513
1514 assert!(!converted.reconciliation);
1515 assert_eq!(converted.lookback_mins, Some(45));
1516 assert_eq!(converted.reconciliation_instrument_ids.len(), 2);
1517 assert!(
1518 converted
1519 .reconciliation_instrument_ids
1520 .contains(&InstrumentId::from("ETHUSDT.BINANCE"))
1521 );
1522 assert!(
1523 converted
1524 .reconciliation_instrument_ids
1525 .contains(&InstrumentId::from("BTCUSDT.BINANCE"))
1526 );
1527 assert!(converted.filter_unclaimed_external);
1528 assert!(converted.filter_position_reports);
1529 assert_eq!(converted.filtered_client_order_ids.len(), 2);
1530 assert!(
1531 converted
1532 .filtered_client_order_ids
1533 .contains(&ClientOrderId::from("O-001"))
1534 );
1535 assert!(
1536 converted
1537 .filtered_client_order_ids
1538 .contains(&ClientOrderId::from("O-002"))
1539 );
1540 assert!(!converted.generate_missing_orders);
1541 assert_eq!(converted.inflight_check_interval_ms, 321);
1542 assert_eq!(converted.inflight_threshold_ms, 654);
1543 assert_eq!(converted.inflight_max_retries, 7);
1544 assert_eq!(converted.open_check_interval_secs, Some(1.5));
1545 assert_eq!(converted.open_check_lookback_mins, Some(9));
1546 assert_eq!(
1547 converted.open_check_threshold_ns,
1548 234 * NANOSECONDS_IN_MILLISECOND
1549 );
1550 assert_eq!(converted.open_check_missing_retries, 4);
1551 assert!(!converted.open_check_open_only);
1552 assert_eq!(converted.max_single_order_queries_per_cycle, 8);
1553 assert_eq!(converted.single_order_query_delay_ms, 76);
1554 assert_eq!(converted.position_check_interval_secs, Some(2.5));
1555 assert_eq!(converted.position_check_lookback_mins, 11);
1556 assert_eq!(
1557 converted.position_check_threshold_ns,
1558 345 * NANOSECONDS_IN_MILLISECOND
1559 );
1560 assert_eq!(converted.position_check_retries, 6);
1561 assert_eq!(converted.purge_closed_orders_buffer_mins, Some(12));
1562 assert_eq!(converted.purge_closed_positions_buffer_mins, Some(13));
1563 assert_eq!(converted.purge_account_events_lookback_mins, Some(14));
1564 assert!(converted.purge_from_database);
1565 }
1566
1567 #[rstest]
1568 fn test_live_risk_engine_config_converts_to_risk_engine_config() {
1569 let config = LiveRiskEngineConfig {
1570 bypass: true,
1571 max_order_submit_rate: "12/00:00:03".to_string(),
1572 max_order_modify_rate: "7/00:00:05".to_string(),
1573 max_notional_per_order: HashMap::from([(
1574 "ETHUSDT.BINANCE".to_string(),
1575 "1000.5".to_string(),
1576 )]),
1577 debug: true,
1578 ..Default::default()
1579 };
1580
1581 let converted: RiskEngineConfig = config.into();
1582
1583 assert!(converted.bypass);
1584 assert_eq!(
1585 converted.max_order_submit,
1586 RateLimit::new(12, 3_000_000_000)
1587 );
1588 assert_eq!(converted.max_order_modify, RateLimit::new(7, 5_000_000_000));
1589 assert_eq!(
1590 converted.max_notional_per_order[&"ETHUSDT.BINANCE".parse::<InstrumentId>().unwrap()],
1591 Decimal::from_str("1000.5").unwrap(),
1592 );
1593 assert!(converted.debug);
1594 }
1595
1596 #[rstest]
1597 fn test_validate_runtime_support_rejects_exec_engine_snapshot_orders() {
1598 let config = LiveNodeConfig {
1599 exec_engine: LiveExecEngineConfig {
1600 snapshot_orders: true,
1601 ..Default::default()
1602 },
1603 ..Default::default()
1604 };
1605
1606 let error = config.validate_runtime_support().unwrap_err();
1607 assert_eq!(
1608 error.to_string(),
1609 "LiveExecEngineConfig.snapshot_orders is not supported by the Rust live runtime yet"
1610 );
1611 }
1612
1613 #[rstest]
1614 fn test_validate_runtime_support_rejects_overflowing_minute_fields() {
1615 let config = LiveNodeConfig {
1616 exec_engine: LiveExecEngineConfig {
1617 open_check_lookback_mins: Some(u32::MAX),
1618 purge_closed_orders_interval_mins: Some(u32::MAX),
1619 purge_closed_positions_interval_mins: Some(u32::MAX),
1620 purge_account_events_interval_mins: Some(u32::MAX),
1621 ..Default::default()
1622 },
1623 ..Default::default()
1624 };
1625
1626 let error = config.validate_runtime_support().unwrap_err();
1627 assert_eq!(
1628 error,
1629 ConfigError::Multiple {
1630 errors: vec![
1631 ConfigError::range(
1632 "LiveExecEngineConfig.open_check_lookback_mins",
1633 "4294967295 minutes (must fit in `u64` nanoseconds)",
1634 ),
1635 ConfigError::range(
1636 "LiveExecEngineConfig.purge_closed_orders_interval_mins",
1637 "4294967295 minutes (must fit in `u64` nanoseconds)",
1638 ),
1639 ConfigError::range(
1640 "LiveExecEngineConfig.purge_closed_positions_interval_mins",
1641 "4294967295 minutes (must fit in `u64` nanoseconds)",
1642 ),
1643 ConfigError::range(
1644 "LiveExecEngineConfig.purge_account_events_interval_mins",
1645 "4294967295 minutes (must fit in `u64` nanoseconds)",
1646 ),
1647 ],
1648 }
1649 );
1650 }
1651
1652 #[rstest]
1653 #[case(0)]
1654 #[case(307_445_734)]
1655 fn test_validate_runtime_support_accepts_purge_retention_boundaries(#[case] mins: u32) {
1656 let config = LiveNodeConfig {
1657 exec_engine: LiveExecEngineConfig {
1658 purge_closed_orders_buffer_mins: Some(mins),
1659 purge_closed_positions_buffer_mins: Some(mins),
1660 purge_account_events_lookback_mins: Some(mins),
1661 ..Default::default()
1662 },
1663 ..Default::default()
1664 };
1665
1666 assert!(config.validate_runtime_support().is_ok());
1667 }
1668
1669 #[rstest]
1670 fn test_validate_runtime_support_rejects_overflowing_purge_retention_minutes() {
1671 let config = LiveNodeConfig {
1672 exec_engine: LiveExecEngineConfig {
1673 purge_closed_orders_buffer_mins: Some(307_445_735),
1674 purge_closed_positions_buffer_mins: Some(307_445_735),
1675 purge_account_events_lookback_mins: Some(307_445_735),
1676 ..Default::default()
1677 },
1678 ..Default::default()
1679 };
1680
1681 let error = config.validate_runtime_support().unwrap_err();
1682 let ConfigError::Multiple { errors } = error else {
1683 panic!("Expected multiple config errors, received {error:?}");
1684 };
1685 assert_eq!(errors.len(), 3);
1686
1687 for field in [
1688 "LiveExecEngineConfig.purge_closed_orders_buffer_mins",
1689 "LiveExecEngineConfig.purge_closed_positions_buffer_mins",
1690 "LiveExecEngineConfig.purge_account_events_lookback_mins",
1691 ] {
1692 assert!(errors.iter().any(
1693 |e| matches!(e, ConfigError::Range { field: error_field, .. } if error_field == field)
1694 ));
1695 }
1696 }
1697
1698 #[rstest]
1699 fn test_validate_runtime_support_rejects_invalid_rate_limit() {
1700 let config = LiveNodeConfig {
1701 risk_engine: LiveRiskEngineConfig {
1702 max_order_submit_rate: "bad-rate".to_string(),
1703 ..Default::default()
1704 },
1705 ..Default::default()
1706 };
1707
1708 let error = config.validate_runtime_support().unwrap_err().to_string();
1709 assert!(error.contains("LiveRiskEngineConfig.max_order_submit_rate"));
1710 }
1711
1712 #[rstest]
1713 fn test_parse_rate_limit_rejects_invalid_format_with_field_path() {
1714 let error =
1715 parse_rate_limit("LiveRiskEngineConfig.max_order_submit_rate", "bad-rate").unwrap_err();
1716
1717 assert_eq!(
1718 error,
1719 ConfigError::InvalidFormat {
1720 field: "LiveRiskEngineConfig.max_order_submit_rate".to_string(),
1721 expected: RATE_LIMIT_FORMAT.to_string(),
1722 },
1723 );
1724 }
1725
1726 #[rstest]
1727 fn test_validate_max_notional_per_order_collects_entry_errors() {
1728 let error = validate_max_notional_per_order(
1729 "LiveRiskEngineConfig.max_notional_per_order",
1730 &HashMap::from([("INVALID".to_string(), "not-a-decimal".to_string())]),
1731 )
1732 .unwrap_err();
1733
1734 match error {
1735 ConfigError::Multiple { errors } => {
1736 assert_eq!(errors.len(), 2);
1737 assert!(matches!(
1738 &errors[0],
1739 ConfigError::InvalidReference {
1740 field,
1741 reference,
1742 ..
1743 } if field == "LiveRiskEngineConfig.max_notional_per_order[INVALID]"
1744 && reference == "instrument ID"
1745 ));
1746 assert!(matches!(
1747 &errors[1],
1748 ConfigError::InvalidValue { field, reason }
1749 if field == "LiveRiskEngineConfig.max_notional_per_order[INVALID]"
1750 && reason.contains("invalid notional")
1751 ));
1752 }
1753 _ => panic!("Expected multiple config errors, received {error:?}"),
1754 }
1755 }
1756
1757 #[rstest]
1758 #[case(-1.0)]
1759 #[case(f64::NAN)]
1760 #[case(f64::INFINITY)]
1761 #[case(f64::NEG_INFINITY)]
1762 fn test_validate_runtime_support_rejects_hostile_startup_delay(#[case] value: f64) {
1763 let config = LiveNodeConfig {
1764 exec_engine: LiveExecEngineConfig {
1765 reconciliation_startup_delay_secs: value,
1766 ..Default::default()
1767 },
1768 ..Default::default()
1769 };
1770
1771 let error = config.validate_runtime_support().unwrap_err().to_string();
1772 assert!(error.contains("reconciliation_startup_delay_secs"));
1773 }
1774
1775 #[rstest]
1776 #[case(0.0)]
1777 #[case(0.5e-9)]
1778 #[case(-1.0)]
1779 #[case(f64::NAN)]
1780 #[case(f64::INFINITY)]
1781 #[case(f64::NEG_INFINITY)]
1782 #[case(f64::MAX)]
1783 fn test_validate_runtime_support_rejects_invalid_exec_intervals(#[case] value: f64) {
1784 let configs = [
1785 (
1786 "LiveExecEngineConfig.snapshot_positions_interval_secs",
1787 LiveExecEngineConfig {
1788 snapshot_positions_interval_secs: Some(value),
1789 ..Default::default()
1790 },
1791 ),
1792 (
1793 "LiveExecEngineConfig.open_check_interval_secs",
1794 LiveExecEngineConfig {
1795 open_check_interval_secs: Some(value),
1796 ..Default::default()
1797 },
1798 ),
1799 (
1800 "LiveExecEngineConfig.position_check_interval_secs",
1801 LiveExecEngineConfig {
1802 position_check_interval_secs: Some(value),
1803 ..Default::default()
1804 },
1805 ),
1806 (
1807 "LiveExecEngineConfig.own_books_audit_interval_secs",
1808 LiveExecEngineConfig {
1809 own_books_audit_interval_secs: Some(value),
1810 ..Default::default()
1811 },
1812 ),
1813 ];
1814
1815 for (expected_field, config) in configs {
1816 let error = config.validate_runtime_support().unwrap_err();
1817
1818 assert!(matches!(
1819 error,
1820 ConfigError::Range { field, .. } if field == expected_field
1821 ));
1822 }
1823 }
1824
1825 #[rstest]
1826 fn test_validate_runtime_support_accepts_valid_exec_intervals() {
1827 let config = LiveExecEngineConfig {
1828 snapshot_positions_interval_secs: Some(1.25),
1829 open_check_interval_secs: Some(2.5),
1830 position_check_interval_secs: Some(3.75),
1831 own_books_audit_interval_secs: Some(4.5),
1832 ..Default::default()
1833 };
1834
1835 assert!(config.validate_runtime_support().is_ok());
1836 }
1837
1838 #[cfg(feature = "python")]
1839 #[rstest]
1840 fn test_duration_from_secs_f64_accepts_valid_value() {
1841 let duration = duration_from_secs_f64("LiveNodeConfig.timeout_connection", 1.5).unwrap();
1842
1843 assert_eq!(duration, Duration::from_millis(1_500));
1844 }
1845
1846 #[cfg(feature = "python")]
1847 #[rstest]
1848 #[case(-1.0)]
1849 #[case(f64::NAN)]
1850 #[case(f64::INFINITY)]
1851 #[case(86_400.1)]
1852 fn test_duration_from_secs_f64_rejects_invalid_values(#[case] value: f64) {
1853 let error = duration_from_secs_f64("LiveNodeConfig.timeout_connection", value).unwrap_err();
1854
1855 match error {
1856 ConfigError::Range { field, reason } => {
1857 assert_eq!(field, "LiveNodeConfig.timeout_connection");
1858 assert!(reason.contains("must be finite, non-negative, and <= 86400"));
1859 }
1860 _ => panic!("Expected range config error, received {error:?}"),
1861 }
1862 }
1863
1864 #[rstest]
1865 fn test_validate_runtime_support_rejects_invalid_reconciliation_instrument_id() {
1866 let config = LiveNodeConfig {
1867 exec_engine: LiveExecEngineConfig {
1868 reconciliation_instrument_ids: Some(vec!["INVALID".to_string()]),
1869 ..Default::default()
1870 },
1871 ..Default::default()
1872 };
1873
1874 let error = config.validate_runtime_support().unwrap_err().to_string();
1875 assert!(error.contains("reconciliation_instrument_ids"));
1876 }
1877
1878 #[rstest]
1879 fn test_parse_rate_limit_happy_path() {
1880 let limit = parse_rate_limit("test.rate_limit", "150/00:00:02").unwrap();
1881 assert_eq!(limit, RateLimit::new(150, 2_000_000_000));
1882 }
1883
1884 #[rstest]
1885 fn test_parse_rate_limit_rejects_trailing_component() {
1886 let err = parse_rate_limit("test.rate_limit", "10/00:00:01:99")
1887 .unwrap_err()
1888 .to_string();
1889 assert!(err.contains("expected 'limit/HH:MM:SS'"));
1890 }
1891
1892 #[rstest]
1893 fn test_parse_rate_limit_rejects_zero_limit() {
1894 let err = parse_rate_limit("test.rate_limit", "0/00:00:01")
1895 .unwrap_err()
1896 .to_string();
1897 assert!(err.contains("Invalid limit"));
1898 assert!(err.contains("must be non-zero"));
1899 }
1900
1901 #[rstest]
1902 fn test_parse_rate_limit_rejects_zero_interval() {
1903 let err = parse_rate_limit("test.rate_limit", "100/00:00:00")
1904 .unwrap_err()
1905 .to_string();
1906 assert!(err.contains("Invalid interval_ns"));
1907 assert!(err.contains("must be non-zero"));
1908 }
1909
1910 #[rstest]
1911 fn test_validate_runtime_support_rejects_exec_engine_qsize() {
1912 let config = LiveNodeConfig {
1913 exec_engine: LiveExecEngineConfig {
1914 qsize: 1,
1915 ..Default::default()
1916 },
1917 ..Default::default()
1918 };
1919
1920 let error = config.validate_runtime_support().unwrap_err();
1921 assert_eq!(
1922 error.to_string(),
1923 "LiveExecEngineConfig.qsize is not supported by the Rust live runtime yet"
1924 );
1925 }
1926
1927 #[rstest]
1928 fn test_validate_runtime_support_rejects_emulator() {
1929 let config = LiveNodeConfig {
1930 emulator: Some(OrderEmulatorConfig::default()),
1931 ..Default::default()
1932 };
1933
1934 let error = config.validate_runtime_support().unwrap_err().to_string();
1935 assert!(error.contains("emulator"));
1936 }
1937
1938 #[rstest]
1939 fn test_validate_runtime_support_rejects_loop_debug() {
1940 let config = LiveNodeConfig {
1941 loop_debug: true,
1942 ..Default::default()
1943 };
1944
1945 let error = config.validate_runtime_support().unwrap_err().to_string();
1946 assert!(error.contains("loop_debug"));
1947 }
1948
1949 #[rstest]
1950 fn test_validate_runtime_support_accepts_file_config() {
1951 use nautilus_common::logging::writer::FileWriterConfig;
1952
1953 let config = LiveNodeConfig {
1954 logging: LoggerConfig {
1955 file_config: Some(FileWriterConfig::default()),
1956 ..Default::default()
1957 },
1958 ..Default::default()
1959 };
1960
1961 assert!(config.validate_runtime_support().is_ok());
1962 }
1963
1964 #[rstest]
1965 fn test_validate_runtime_support_accepts_clear_log_file() {
1966 let config = LiveNodeConfig {
1967 logging: LoggerConfig {
1968 clear_log_file: true,
1969 ..Default::default()
1970 },
1971 ..Default::default()
1972 };
1973
1974 assert!(config.validate_runtime_support().is_ok());
1975 }
1976
1977 #[rstest]
1978 fn test_validate_runtime_support_rejects_invalid_time_bars_origin_offset_key() {
1979 let config = LiveNodeConfig {
1980 data_engine: LiveDataEngineConfig {
1981 time_bars_origin_offset: HashMap::from([("INVALID".to_string(), 1_000)]),
1982 ..Default::default()
1983 },
1984 ..Default::default()
1985 };
1986
1987 let error = config.validate_runtime_support().unwrap_err().to_string();
1988 assert!(error.contains("time_bars_origin_offset"));
1989 }
1990
1991 #[rstest]
1992 fn test_validate_runtime_support_rejects_empty_plugin_path() {
1993 let config = LiveNodeConfig {
1994 plugins: vec![PluginConfig {
1995 type_name: "ExampleActor".to_string(),
1996 ..Default::default()
1997 }],
1998 ..Default::default()
1999 };
2000
2001 let error = config.validate_runtime_support().unwrap_err().to_string();
2002 assert!(error.contains("plugins[0].path"));
2003 }
2004
2005 #[rstest]
2006 fn test_validate_runtime_support_rejects_empty_plugin_type_name() {
2007 let config = LiveNodeConfig {
2008 plugins: vec![PluginConfig {
2009 path: "./libexample.so".to_string(),
2010 ..Default::default()
2011 }],
2012 ..Default::default()
2013 };
2014
2015 let error = config.validate_runtime_support().unwrap_err().to_string();
2016 assert!(error.contains("plugins[0].type_name"));
2017 }
2018
2019 #[rstest]
2020 fn test_validate_runtime_support_rejects_invalid_plugin_sha256() {
2021 let config = LiveNodeConfig {
2022 plugins: vec![PluginConfig {
2023 path: "./libexample.so".to_string(),
2024 type_name: "ExampleActor".to_string(),
2025 sha256: Some("not-a-digest".to_string()),
2026 ..Default::default()
2027 }],
2028 ..Default::default()
2029 };
2030
2031 let error = config.validate_runtime_support().unwrap_err().to_string();
2032 assert!(error.contains("sha256"));
2033 }
2034
2035 #[rstest]
2036 #[allow(
2038 clippy::float_cmp,
2039 reason = "asserts the exact configured default with no arithmetic involved"
2040 )]
2041 fn test_live_exec_engine_config_defaults() {
2042 let config = LiveExecEngineConfig::default();
2043
2044 assert!(config.load_cache);
2045 assert!(!config.snapshot_orders);
2046 assert!(!config.snapshot_positions);
2047 assert_eq!(config.snapshot_positions_interval_secs, None);
2048 assert_eq!(config.external_clients, None);
2049 assert!(!config.debug);
2050 assert!(!config.manage_own_order_books);
2051 assert!(!config.allow_overfills);
2052 assert!(config.reconciliation);
2053 assert_eq!(config.reconciliation_startup_delay_secs, 10.0);
2054 assert_eq!(config.reconciliation_lookback_mins, None);
2055 assert_eq!(config.reconciliation_instrument_ids, None);
2056 assert_eq!(config.filtered_client_order_ids, None);
2057 assert!(!config.filter_unclaimed_external_orders);
2058 assert!(!config.filter_position_reports);
2059 assert!(config.generate_missing_orders);
2060 assert_eq!(config.inflight_check_interval_ms, 2_000);
2061 assert_eq!(config.inflight_check_threshold_ms, 5_000);
2062 assert_eq!(config.inflight_check_retries, 5);
2063 assert_eq!(config.open_check_threshold_ms, 5_000);
2064 assert_eq!(config.open_check_lookback_mins, Some(60));
2065 assert_eq!(config.open_check_missing_retries, 5);
2066 assert!(config.open_check_open_only);
2067 assert_eq!(config.max_single_order_queries_per_cycle, 10);
2068 assert_eq!(config.position_check_threshold_ms, 5_000);
2069 assert_eq!(config.position_check_retries, 3);
2070 assert!(!config.purge_from_database);
2071 assert_eq!(config.qsize, 100_000);
2072 }
2073
2074 #[rstest]
2075 fn test_live_data_engine_config_defaults() {
2076 let config = LiveDataEngineConfig::default();
2077
2078 assert!(config.time_bars_build_with_no_updates);
2079 assert!(config.time_bars_timestamp_on_close);
2080 assert!(!config.time_bars_skip_first_non_full_bar);
2081 assert_eq!(config.time_bars_interval_type, BarIntervalType::LeftOpen);
2082 assert_eq!(config.time_bars_build_delay, 0);
2083 assert!(config.time_bars_origin_offset.is_empty());
2084 assert!(!config.validate_data_sequence);
2085 assert!(!config.buffer_deltas);
2086 assert!(!config.emit_quotes_from_book);
2087 assert!(!config.emit_quotes_from_book_depths);
2088 assert_eq!(config.external_clients, None);
2089 assert!(!config.debug);
2090 assert_eq!(config.qsize, 100_000);
2091 }
2092
2093 #[rstest]
2094 fn test_live_risk_engine_config_defaults() {
2095 let config = LiveRiskEngineConfig::default();
2096
2097 assert!(!config.bypass);
2098 assert_eq!(config.max_order_submit_rate, DEFAULT_ORDER_RATE_LIMIT);
2099 assert_eq!(config.max_order_modify_rate, DEFAULT_ORDER_RATE_LIMIT);
2100 assert!(config.max_notional_per_order.is_empty());
2101 assert!(!config.debug);
2102 assert_eq!(config.qsize, 100_000);
2103 }
2104
2105 #[rstest]
2106 fn test_routing_config_default() {
2107 let config = RoutingConfig::default();
2108
2109 assert!(!config.default);
2110 assert_eq!(config.venues, None);
2111 }
2112
2113 #[rstest]
2114 fn test_live_data_client_config_default() {
2115 let config = LiveDataClientConfig::default();
2116
2117 assert!(!config.handle_revised_bars);
2118 assert!(!config.instrument_provider.load_all);
2119 assert!(config.instrument_provider.load_ids.is_none());
2120 assert!(config.instrument_provider.filters.is_empty());
2121 assert!(config.instrument_provider.filter_callable.is_none());
2122 assert!(config.instrument_provider.log_warnings);
2123 assert!(!config.routing.default);
2124 }
2125
2126 #[rstest]
2127 fn test_live_data_client_config_rejects_unknown_field() {
2128 let error = serde_json::from_str::<LiveDataClientConfig>(
2129 r#"{"handle_revised_bars":true,"unexpected":true}"#,
2130 )
2131 .unwrap_err();
2132
2133 assert!(error.to_string().contains("unknown field `unexpected`"));
2134 }
2135
2136 #[rstest]
2137 fn test_live_data_client_config_rejects_unknown_nested_field() {
2138 let error = serde_json::from_str::<LiveDataClientConfig>(
2139 r#"{"instrument_provider":{"load_all":true,"instrument_provider":{"load_all":false}}}"#,
2140 )
2141 .unwrap_err();
2142
2143 assert!(
2144 error
2145 .to_string()
2146 .contains("unknown field `instrument_provider`")
2147 );
2148 }
2149
2150 #[rstest]
2151 fn test_live_node_config_toml_minimal() {
2152 let config: LiveNodeConfig = toml::from_str(
2153 r#"
2154environment = "Live"
2155trader_id = "TRADER-042"
2156
2157[data_engine]
2158debug = true
2159
2160[risk_engine]
2161bypass = false
2162
2163[exec_engine]
2164reconciliation = false
2165
2166[data_clients.hyperliquid]
2167handle_revised_bars = true
2168
2169[exec_clients.hyperliquid]
2170routing = { default = true, venues = ["HYPERLIQUID"] }
2171instrument_provider = { load_all = true }
2172
2173[[plugins]]
2174path = "./target/debug/examples/libcustom_data_plugin.so"
2175type_name = "ExampleStrategy"
2176config = { strategy_id = "ExampleStrategy-001", threshold = 10 }
2177"#,
2178 )
2179 .unwrap();
2180
2181 assert_eq!(config.environment, Environment::Live);
2182 assert_eq!(config.trader_id, TraderId::from("TRADER-042"));
2183 assert!(config.data_engine.debug);
2184 assert!(!config.risk_engine.bypass);
2185 assert!(!config.exec_engine.reconciliation);
2186 assert!(config.data_clients["hyperliquid"].handle_revised_bars);
2187 let exec_client = &config.exec_clients["hyperliquid"];
2188 assert!(exec_client.routing.default);
2189 assert_eq!(
2190 exec_client.routing.venues,
2191 Some(vec!["HYPERLIQUID".to_string()]),
2192 );
2193 assert!(exec_client.instrument_provider.load_all);
2194 assert_eq!(config.plugins.len(), 1);
2195 assert_eq!(
2196 config.plugins[0].path,
2197 "./target/debug/examples/libcustom_data_plugin.so"
2198 );
2199 assert_eq!(config.plugins[0].type_name, "ExampleStrategy");
2200 assert_eq!(
2201 config.plugins[0].config["strategy_id"],
2202 serde_json::json!("ExampleStrategy-001")
2203 );
2204 assert_eq!(config.plugins[0].config["threshold"], serde_json::json!(10));
2205 }
2206
2207 #[rstest]
2208 fn live_node_config_serde_roundtrip_with_event_store() {
2209 let config = LiveNodeConfig {
2210 event_store: Some(EventStoreConfig {
2211 channel_capacity: 5_000,
2212 ..Default::default()
2213 }),
2214 ..Default::default()
2215 };
2216 let json = serde_json::to_string(&config).expect("serialize");
2217 let restored: LiveNodeConfig = serde_json::from_str(&json).expect("deserialize");
2218
2219 let restored_event_store = restored.event_store.expect("event_store present");
2220 assert_eq!(restored_event_store.channel_capacity, 5_000);
2221 }
2222
2223 #[rstest]
2224 fn live_node_config_default_has_no_event_store() {
2225 let config = LiveNodeConfig::default();
2226 assert!(config.event_store.is_none());
2227 }
2228}