1use std::{collections::HashMap, str::FromStr, time::Duration};
19
20use ahash::AHashMap;
21use nautilus_common::{
22 cache::CacheConfig, enums::Environment, logging::logger::LoggerConfig,
23 msgbus::database::MessageBusConfig, throttler::RateLimit,
24};
25use nautilus_core::{UUID4, datetime::NANOSECONDS_IN_SECOND};
26use nautilus_data::engine::config::DataEngineConfig;
27use nautilus_execution::{
28 engine::config::ExecutionEngineConfig, order_emulator::config::OrderEmulatorConfig,
29};
30use nautilus_model::{
31 enums::{BarAggregation, BarIntervalType},
32 identifiers::{ClientId, ClientOrderId, InstrumentId, TraderId},
33};
34use nautilus_portfolio::config::PortfolioConfig;
35use nautilus_risk::engine::config::RiskEngineConfig;
36use nautilus_system::config::{NautilusKernelConfig, StreamingConfig};
37use rust_decimal::Decimal;
38use serde::{Deserialize, Serialize};
39
40const DEFAULT_ORDER_RATE_LIMIT: &str = "100/00:00:01";
42
43#[cfg_attr(
45 feature = "python",
46 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.live", from_py_object)
47)]
48#[cfg_attr(
49 feature = "python",
50 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
51)]
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
53#[serde(deny_unknown_fields)]
54pub struct LiveDataEngineConfig {
55 #[builder(default = true)]
57 pub time_bars_build_with_no_updates: bool,
58 #[builder(default = true)]
61 pub time_bars_timestamp_on_close: bool,
62 #[builder(default)]
64 pub time_bars_skip_first_non_full_bar: bool,
65 #[builder(default = BarIntervalType::LeftOpen)]
67 pub time_bars_interval_type: BarIntervalType,
68 #[builder(default)]
70 pub time_bars_build_delay: u64,
71 #[builder(default)]
75 pub time_bars_origins: HashMap<String, u64>,
76 #[builder(default)]
78 pub validate_data_sequence: bool,
79 #[builder(default)]
81 pub buffer_deltas: bool,
82 #[builder(default)]
84 pub emit_quotes_from_book: bool,
85 #[builder(default)]
87 pub emit_quotes_from_book_depths: bool,
88 pub external_clients: Option<Vec<ClientId>>,
92 #[builder(default)]
94 pub debug: bool,
95 #[builder(default)]
97 pub graceful_shutdown_on_error: bool,
98 #[builder(default = 100_000)]
103 pub qsize: u32,
104}
105
106impl Default for LiveDataEngineConfig {
107 fn default() -> Self {
108 Self::builder().build()
109 }
110}
111
112impl From<LiveDataEngineConfig> for DataEngineConfig {
113 fn from(config: LiveDataEngineConfig) -> Self {
114 let time_bars_origins = config
115 .time_bars_origins
116 .into_iter()
117 .map(|(agg, nanos)| {
118 let agg = BarAggregation::from_str(&agg)
119 .expect("validate_runtime_support must run before DataEngineConfig conversion");
120 (agg, Duration::from_nanos(nanos))
121 })
122 .collect();
123
124 Self {
125 time_bars_build_with_no_updates: config.time_bars_build_with_no_updates,
126 time_bars_timestamp_on_close: config.time_bars_timestamp_on_close,
127 time_bars_skip_first_non_full_bar: config.time_bars_skip_first_non_full_bar,
128 time_bars_interval_type: config.time_bars_interval_type,
129 time_bars_build_delay: config.time_bars_build_delay,
130 time_bars_origins,
131 validate_data_sequence: config.validate_data_sequence,
132 buffer_deltas: config.buffer_deltas,
133 emit_quotes_from_book: config.emit_quotes_from_book,
134 emit_quotes_from_book_depths: config.emit_quotes_from_book_depths,
135 external_clients: config.external_clients,
136 debug: config.debug,
137 }
138 }
139}
140
141#[cfg_attr(
143 feature = "python",
144 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.live", from_py_object)
145)]
146#[cfg_attr(
147 feature = "python",
148 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
149)]
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
151#[serde(deny_unknown_fields)]
152pub struct LiveRiskEngineConfig {
153 #[builder(default)]
155 pub bypass: bool,
156 #[builder(default = DEFAULT_ORDER_RATE_LIMIT.to_string())]
158 pub max_order_submit_rate: String,
159 #[builder(default = DEFAULT_ORDER_RATE_LIMIT.to_string())]
161 pub max_order_modify_rate: String,
162 #[builder(default)]
166 pub max_notional_per_order: HashMap<String, String>,
167 #[builder(default)]
169 pub debug: bool,
170 #[builder(default)]
172 pub graceful_shutdown_on_error: bool,
173 #[builder(default = 100_000)]
178 pub qsize: u32,
179}
180
181impl Default for LiveRiskEngineConfig {
182 fn default() -> Self {
183 Self::builder().build()
184 }
185}
186
187impl From<LiveRiskEngineConfig> for RiskEngineConfig {
188 fn from(config: LiveRiskEngineConfig) -> Self {
189 let max_notional_per_order = config
190 .max_notional_per_order
191 .into_iter()
192 .map(|(instrument_id, notional)| {
193 let instrument_id = InstrumentId::from_str(&instrument_id)
194 .expect("validate_runtime_support must run before RiskEngineConfig conversion");
195 let notional = Decimal::from_str(¬ional)
196 .expect("validate_runtime_support must run before RiskEngineConfig conversion");
197 (instrument_id, notional)
198 })
199 .collect::<AHashMap<_, _>>();
200
201 Self {
202 bypass: config.bypass,
203 max_order_submit: parse_rate_limit(&config.max_order_submit_rate)
204 .expect("validate_runtime_support must run before RiskEngineConfig conversion"),
205 max_order_modify: parse_rate_limit(&config.max_order_modify_rate)
206 .expect("validate_runtime_support must run before RiskEngineConfig conversion"),
207 max_notional_per_order,
208 debug: config.debug,
209 }
210 }
211}
212
213fn parse_rate_limit(input: &str) -> anyhow::Result<RateLimit> {
214 let (limit, interval) = input.split_once('/').ok_or_else(|| {
215 anyhow::anyhow!("invalid rate limit '{input}': expected 'limit/HH:MM:SS'")
216 })?;
217
218 let limit = limit
219 .parse::<usize>()
220 .map_err(|e| anyhow::anyhow!("invalid rate limit '{input}': {e}"))?;
221
222 if limit == 0 {
223 anyhow::bail!("invalid rate limit '{input}': limit must be greater than zero");
224 }
225
226 let mut parts = interval.split(':');
227 let mut next = |label: &str| -> anyhow::Result<u64> {
228 parts
229 .next()
230 .ok_or_else(|| {
231 anyhow::anyhow!("invalid rate limit '{input}': missing {label} component")
232 })?
233 .parse::<u64>()
234 .map_err(|e| anyhow::anyhow!("invalid rate limit '{input}': {label}: {e}"))
235 };
236
237 let hours = next("hours")?;
238 let minutes = next("minutes")?;
239 let seconds = next("seconds")?;
240
241 if parts.next().is_some() {
242 anyhow::bail!("invalid rate limit '{input}': expected 'limit/HH:MM:SS'");
243 }
244
245 let interval_ns = hours
246 .saturating_mul(3_600)
247 .saturating_add(minutes.saturating_mul(60))
248 .saturating_add(seconds)
249 .saturating_mul(NANOSECONDS_IN_SECOND);
250
251 if interval_ns == 0 {
252 anyhow::bail!("invalid rate limit '{input}': interval must be greater than zero");
253 }
254
255 Ok(RateLimit::new(limit, interval_ns))
256}
257
258#[cfg_attr(
260 feature = "python",
261 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.live", from_py_object)
262)]
263#[cfg_attr(
264 feature = "python",
265 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
266)]
267#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, bon::Builder)]
268#[serde(deny_unknown_fields)]
269pub struct LiveExecEngineConfig {
270 #[builder(default = true)]
272 pub load_cache: bool,
273 #[builder(default)]
279 pub snapshot_orders: bool,
280 #[builder(default)]
286 pub snapshot_positions: bool,
287 pub snapshot_positions_interval_secs: Option<f64>,
290 pub external_clients: Option<Vec<ClientId>>,
295 #[builder(default)]
297 pub debug: bool,
298 #[builder(default = true)]
300 pub reconciliation: bool,
301 #[builder(default = 10.0)]
303 pub reconciliation_startup_delay_secs: f64,
304 pub reconciliation_lookback_mins: Option<u32>,
306 pub reconciliation_instrument_ids: Option<Vec<String>>,
308 #[builder(default)]
310 pub filter_unclaimed_external_orders: bool,
311 #[builder(default)]
313 pub filter_position_reports: bool,
314 pub filtered_client_order_ids: Option<Vec<String>>,
316 #[builder(default = true)]
318 pub generate_missing_orders: bool,
319 #[builder(default = 2_000)]
321 pub inflight_check_interval_ms: u32,
322 #[builder(default = 5_000)]
324 pub inflight_check_threshold_ms: u32,
325 #[builder(default = 5)]
327 pub inflight_check_retries: u32,
328 pub open_check_interval_secs: Option<f64>,
330 pub open_check_lookback_mins: Option<u32>,
333 #[builder(default = 5_000)]
335 pub open_check_threshold_ms: u32,
336 #[builder(default = 5)]
338 pub open_check_missing_retries: u32,
339 #[builder(default = true)]
341 pub open_check_open_only: bool,
342 #[builder(default = 10)]
344 pub max_single_order_queries_per_cycle: u32,
345 #[builder(default = 100)]
347 pub single_order_query_delay_ms: u32,
348 pub position_check_interval_secs: Option<f64>,
350 #[builder(default = 60)]
352 pub position_check_lookback_mins: u32,
353 #[builder(default = 5_000)]
355 pub position_check_threshold_ms: u32,
356 #[builder(default = 3)]
358 pub position_check_retries: u32,
359 pub purge_closed_orders_interval_mins: Option<u32>,
361 pub purge_closed_orders_buffer_mins: Option<u32>,
363 pub purge_closed_positions_interval_mins: Option<u32>,
365 pub purge_closed_positions_buffer_mins: Option<u32>,
367 pub purge_account_events_interval_mins: Option<u32>,
369 pub purge_account_events_lookback_mins: Option<u32>,
371 #[builder(default)]
373 pub purge_from_database: bool,
374 pub own_books_audit_interval_secs: Option<f64>,
376 #[builder(default)]
378 pub graceful_shutdown_on_error: bool,
379 #[builder(default = 100_000)]
381 pub qsize: u32,
382 #[builder(default)]
385 pub allow_overfills: bool,
386 #[builder(default)]
388 pub manage_own_order_books: bool,
389}
390
391impl Default for LiveExecEngineConfig {
392 fn default() -> Self {
393 Self {
394 open_check_lookback_mins: Some(60),
395 ..Self::builder().build()
396 }
397 }
398}
399
400impl From<LiveExecEngineConfig> for ExecutionEngineConfig {
401 fn from(config: LiveExecEngineConfig) -> Self {
402 Self {
403 load_cache: config.load_cache,
404 manage_own_order_books: config.manage_own_order_books,
405 snapshot_orders: config.snapshot_orders,
406 snapshot_positions: config.snapshot_positions,
407 snapshot_positions_interval_secs: config.snapshot_positions_interval_secs,
408 allow_overfills: config.allow_overfills,
409 external_clients: config.external_clients,
410 purge_closed_orders_interval_mins: config.purge_closed_orders_interval_mins,
411 purge_closed_orders_buffer_mins: config.purge_closed_orders_buffer_mins,
412 purge_closed_positions_interval_mins: config.purge_closed_positions_interval_mins,
413 purge_closed_positions_buffer_mins: config.purge_closed_positions_buffer_mins,
414 purge_account_events_interval_mins: config.purge_account_events_interval_mins,
415 purge_account_events_lookback_mins: config.purge_account_events_lookback_mins,
416 purge_from_database: config.purge_from_database,
417 debug: config.debug,
418 }
419 }
420}
421
422#[cfg_attr(
424 feature = "python",
425 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.live", from_py_object)
426)]
427#[cfg_attr(
428 feature = "python",
429 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
430)]
431#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, bon::Builder)]
432#[serde(deny_unknown_fields)]
433pub struct RoutingConfig {
434 #[builder(default)]
436 pub default: bool,
437 pub venues: Option<Vec<String>>,
439}
440
441#[cfg_attr(
443 feature = "python",
444 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.live", from_py_object)
445)]
446#[cfg_attr(
447 feature = "python",
448 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
449)]
450#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, bon::Builder)]
451#[serde(deny_unknown_fields)]
452pub struct InstrumentProviderConfig {
453 #[builder(default)]
455 pub load_all: bool,
456 pub load_ids: Option<Vec<String>>,
458 #[builder(default)]
460 pub filters: HashMap<String, serde_json::Value>,
461 pub filter_callable: Option<String>,
463 #[builder(default = true)]
465 pub log_warnings: bool,
466}
467
468impl Default for InstrumentProviderConfig {
469 fn default() -> Self {
470 Self::builder().build()
471 }
472}
473
474#[cfg_attr(
476 feature = "python",
477 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.live", from_py_object)
478)]
479#[cfg_attr(
480 feature = "python",
481 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
482)]
483#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, bon::Builder)]
484#[serde(deny_unknown_fields)]
485pub struct LiveDataClientConfig {
486 #[builder(default)]
488 pub handle_revised_bars: bool,
489 #[builder(default)]
491 pub instrument_provider: InstrumentProviderConfig,
492 #[builder(default)]
494 pub routing: RoutingConfig,
495}
496
497#[cfg_attr(
499 feature = "python",
500 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.live", from_py_object)
501)]
502#[cfg_attr(
503 feature = "python",
504 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
505)]
506#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, bon::Builder)]
507#[serde(deny_unknown_fields)]
508pub struct LiveExecClientConfig {
509 #[builder(default)]
511 pub instrument_provider: InstrumentProviderConfig,
512 #[builder(default)]
514 pub routing: RoutingConfig,
515}
516
517#[cfg_attr(
519 feature = "python",
520 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.live", from_py_object)
521)]
522#[cfg_attr(
523 feature = "python",
524 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
525)]
526#[derive(Debug, Clone, bon::Builder)]
527pub struct LiveNodeConfig {
528 #[builder(default = Environment::Live)]
530 pub environment: Environment,
531 #[builder(default = TraderId::from("TRADER-001"))]
533 pub trader_id: TraderId,
534 #[builder(default)]
536 pub load_state: bool,
537 #[builder(default)]
539 pub save_state: bool,
540 #[builder(default)]
542 pub logging: LoggerConfig,
543 pub instance_id: Option<UUID4>,
545 #[builder(default = Duration::from_secs(120))]
547 pub timeout_connection: Duration,
548 #[builder(default = Duration::from_secs(30))]
550 pub timeout_reconciliation: Duration,
551 #[builder(default = Duration::from_secs(10))]
553 pub timeout_portfolio: Duration,
554 #[builder(default = Duration::from_secs(10))]
556 pub timeout_disconnection: Duration,
557 #[builder(default = Duration::from_secs(10))]
559 pub delay_post_stop: Duration,
560 #[builder(default = Duration::from_secs(5))]
562 pub timeout_shutdown: Duration,
563 pub cache: Option<CacheConfig>,
565 pub msgbus: Option<MessageBusConfig>,
567 pub portfolio: Option<PortfolioConfig>,
569 pub emulator: Option<OrderEmulatorConfig>,
571 pub streaming: Option<StreamingConfig>,
573 #[builder(default)]
575 pub loop_debug: bool,
576 #[builder(default)]
578 pub data_engine: LiveDataEngineConfig,
579 #[builder(default)]
581 pub risk_engine: LiveRiskEngineConfig,
582 #[builder(default)]
584 pub exec_engine: LiveExecEngineConfig,
585 #[builder(default)]
587 pub data_clients: HashMap<String, LiveDataClientConfig>,
588 #[builder(default)]
590 pub exec_clients: HashMap<String, LiveExecClientConfig>,
591}
592
593impl Default for LiveNodeConfig {
594 fn default() -> Self {
595 Self::builder().build()
596 }
597}
598
599impl LiveNodeConfig {
600 pub(crate) fn validate_runtime_support(&self) -> anyhow::Result<()> {
608 if self.msgbus.is_some() {
609 anyhow::bail!("LiveNodeConfig.msgbus is not supported by the Rust live runtime yet");
610 }
611
612 if self.streaming.is_some() {
613 anyhow::bail!("LiveNodeConfig.streaming is not supported by the Rust live runtime yet");
614 }
615
616 if self.emulator.is_some() {
617 anyhow::bail!("LiveNodeConfig.emulator is not supported by the Rust live runtime yet");
618 }
619
620 if self.loop_debug {
621 anyhow::bail!(
622 "LiveNodeConfig.loop_debug is not supported by the Rust live runtime yet"
623 );
624 }
625
626 if self.logging.file_config.is_some() {
627 anyhow::bail!(
628 "LoggerConfig.file_config is not supported by the Rust live runtime yet (use py_init_logging)"
629 );
630 }
631
632 if self.logging.clear_log_file {
633 anyhow::bail!(
634 "LoggerConfig.clear_log_file is not supported by the Rust live runtime yet"
635 );
636 }
637
638 self.data_engine.validate_runtime_support()?;
639 self.risk_engine.validate_runtime_support()?;
640 self.exec_engine.validate_runtime_support()?;
641
642 Ok(())
643 }
644}
645
646impl LiveDataEngineConfig {
647 fn validate_runtime_support(&self) -> anyhow::Result<()> {
648 for agg_str in self.time_bars_origins.keys() {
649 BarAggregation::from_str(agg_str).map_err(|e| {
650 anyhow::anyhow!(
651 "invalid LiveDataEngineConfig.time_bars_origins key {agg_str:?}: {e}"
652 )
653 })?;
654 }
655
656 let default = Self::default();
657
658 if self.graceful_shutdown_on_error != default.graceful_shutdown_on_error {
659 anyhow::bail!(
660 "LiveDataEngineConfig.graceful_shutdown_on_error is not supported by the Rust live runtime yet"
661 );
662 }
663
664 if self.qsize != default.qsize {
665 anyhow::bail!(
666 "LiveDataEngineConfig.qsize is not supported by the Rust live runtime yet"
667 );
668 }
669
670 Ok(())
671 }
672}
673
674impl LiveRiskEngineConfig {
675 fn validate_runtime_support(&self) -> anyhow::Result<()> {
676 parse_rate_limit(&self.max_order_submit_rate).map_err(|e| {
677 anyhow::anyhow!("invalid LiveRiskEngineConfig.max_order_submit_rate: {e}")
678 })?;
679 parse_rate_limit(&self.max_order_modify_rate).map_err(|e| {
680 anyhow::anyhow!("invalid LiveRiskEngineConfig.max_order_modify_rate: {e}")
681 })?;
682
683 for (instrument_id, notional) in &self.max_notional_per_order {
684 InstrumentId::from_str(instrument_id).map_err(|e| {
685 anyhow::anyhow!(
686 "invalid LiveRiskEngineConfig.max_notional_per_order instrument ID {instrument_id:?}: {e}"
687 )
688 })?;
689 Decimal::from_str(notional).map_err(|e| {
690 anyhow::anyhow!(
691 "invalid LiveRiskEngineConfig.max_notional_per_order notional {notional:?}: {e}"
692 )
693 })?;
694 }
695
696 let default = Self::default();
697
698 if self.graceful_shutdown_on_error != default.graceful_shutdown_on_error {
699 anyhow::bail!(
700 "LiveRiskEngineConfig.graceful_shutdown_on_error is not supported by the Rust live runtime yet"
701 );
702 }
703
704 if self.qsize != default.qsize {
705 anyhow::bail!(
706 "LiveRiskEngineConfig.qsize is not supported by the Rust live runtime yet"
707 );
708 }
709
710 Ok(())
711 }
712}
713
714impl LiveExecEngineConfig {
715 fn validate_runtime_support(&self) -> anyhow::Result<()> {
716 if !self.reconciliation_startup_delay_secs.is_finite()
720 || self.reconciliation_startup_delay_secs < 0.0
721 {
722 anyhow::bail!(
723 "invalid LiveExecEngineConfig.reconciliation_startup_delay_secs: {} (must be a non-negative finite number)",
724 self.reconciliation_startup_delay_secs
725 );
726 }
727
728 if let Some(instrument_ids) = &self.reconciliation_instrument_ids {
729 for instrument_id in instrument_ids {
730 InstrumentId::from_str(instrument_id).map_err(|e| {
731 anyhow::anyhow!(
732 "invalid LiveExecEngineConfig.reconciliation_instrument_ids entry {instrument_id:?}: {e}"
733 )
734 })?;
735 }
736 }
737
738 if let Some(client_order_ids) = &self.filtered_client_order_ids {
739 for client_order_id in client_order_ids {
740 ClientOrderId::new_checked(client_order_id).map_err(|e| {
741 anyhow::anyhow!(
742 "invalid LiveExecEngineConfig.filtered_client_order_ids entry {client_order_id:?}: {e}"
743 )
744 })?;
745 }
746 }
747
748 let default = Self::default();
749
750 if self.snapshot_orders != default.snapshot_orders {
751 anyhow::bail!(
752 "LiveExecEngineConfig.snapshot_orders is not supported by the Rust live runtime yet"
753 );
754 }
755
756 if self.snapshot_positions != default.snapshot_positions {
757 anyhow::bail!(
758 "LiveExecEngineConfig.snapshot_positions is not supported by the Rust live runtime yet"
759 );
760 }
761
762 if self.purge_from_database != default.purge_from_database {
763 anyhow::bail!(
764 "LiveExecEngineConfig.purge_from_database is not supported by the Rust live runtime yet"
765 );
766 }
767
768 if self.graceful_shutdown_on_error != default.graceful_shutdown_on_error {
769 anyhow::bail!(
770 "LiveExecEngineConfig.graceful_shutdown_on_error is not supported by the Rust live runtime yet"
771 );
772 }
773
774 if self.qsize != default.qsize {
775 anyhow::bail!(
776 "LiveExecEngineConfig.qsize is not supported by the Rust live runtime yet"
777 );
778 }
779
780 Ok(())
781 }
782}
783
784impl NautilusKernelConfig for LiveNodeConfig {
785 fn environment(&self) -> Environment {
786 self.environment
787 }
788
789 fn trader_id(&self) -> TraderId {
790 self.trader_id
791 }
792
793 fn load_state(&self) -> bool {
794 self.load_state
795 }
796
797 fn save_state(&self) -> bool {
798 self.save_state
799 }
800
801 fn logging(&self) -> LoggerConfig {
802 self.logging.clone()
803 }
804
805 fn instance_id(&self) -> Option<UUID4> {
806 self.instance_id
807 }
808
809 fn timeout_connection(&self) -> Duration {
810 self.timeout_connection
811 }
812
813 fn timeout_reconciliation(&self) -> Duration {
814 self.timeout_reconciliation
815 }
816
817 fn timeout_portfolio(&self) -> Duration {
818 self.timeout_portfolio
819 }
820
821 fn timeout_disconnection(&self) -> Duration {
822 self.timeout_disconnection
823 }
824
825 fn delay_post_stop(&self) -> Duration {
826 self.delay_post_stop
827 }
828
829 fn timeout_shutdown(&self) -> Duration {
830 self.timeout_shutdown
831 }
832
833 fn cache(&self) -> Option<CacheConfig> {
834 self.cache.clone()
835 }
836
837 fn msgbus(&self) -> Option<MessageBusConfig> {
838 self.msgbus.clone()
839 }
840
841 fn data_engine(&self) -> Option<DataEngineConfig> {
842 Some(self.data_engine.clone().into())
843 }
844
845 fn risk_engine(&self) -> Option<RiskEngineConfig> {
846 Some(self.risk_engine.clone().into())
847 }
848
849 fn exec_engine(&self) -> Option<ExecutionEngineConfig> {
850 Some(self.exec_engine.clone().into())
851 }
852
853 fn portfolio(&self) -> Option<PortfolioConfig> {
854 self.portfolio
855 }
856
857 fn streaming(&self) -> Option<StreamingConfig> {
858 self.streaming.clone()
859 }
860}
861
862#[cfg(test)]
863mod tests {
864 use nautilus_system::config::RotationConfig;
865 use rstest::rstest;
866
867 use super::*;
868
869 #[rstest]
870 fn test_trading_node_config_default() {
871 let config = LiveNodeConfig::default();
872
873 assert_eq!(config.environment, Environment::Live);
874 assert_eq!(config.trader_id, TraderId::from("TRADER-001"));
875 assert_eq!(config.data_engine.qsize, 100_000);
876 assert_eq!(config.risk_engine.qsize, 100_000);
877 assert_eq!(config.exec_engine.qsize, 100_000);
878 assert!(config.exec_engine.reconciliation);
879 assert!(!config.exec_engine.filter_unclaimed_external_orders);
880 assert!(config.data_clients.is_empty());
881 assert!(config.exec_clients.is_empty());
882 }
883
884 #[rstest]
885 fn test_trading_node_config_as_kernel_config() {
886 let config = LiveNodeConfig::default();
887
888 assert_eq!(config.environment(), Environment::Live);
889 assert_eq!(config.trader_id(), TraderId::from("TRADER-001"));
890 assert!(config.data_engine().is_some());
891 assert!(config.risk_engine().is_some());
892 assert!(config.exec_engine().is_some());
893 assert!(!config.load_state());
894 assert!(!config.save_state());
895 }
896
897 #[rstest]
898 fn test_validate_runtime_support_with_defaults() {
899 let config = LiveNodeConfig::default();
900
901 assert!(config.validate_runtime_support().is_ok());
902 }
903
904 #[rstest]
905 fn test_validate_runtime_support_rejects_msgbus_config() {
906 let config = LiveNodeConfig {
907 msgbus: Some(MessageBusConfig::default()),
908 ..Default::default()
909 };
910
911 let error = config.validate_runtime_support().unwrap_err();
912 assert_eq!(
913 error.to_string(),
914 "LiveNodeConfig.msgbus is not supported by the Rust live runtime yet"
915 );
916 }
917
918 #[rstest]
919 fn test_validate_runtime_support_rejects_streaming_config() {
920 let config = LiveNodeConfig {
921 streaming: Some(StreamingConfig::new(
922 "catalog".to_string(),
923 "file".to_string(),
924 1_000,
925 false,
926 RotationConfig::NoRotation,
927 )),
928 ..Default::default()
929 };
930
931 let error = config.validate_runtime_support().unwrap_err();
932 assert_eq!(
933 error.to_string(),
934 "LiveNodeConfig.streaming is not supported by the Rust live runtime yet"
935 );
936 }
937
938 #[rstest]
939 fn test_validate_runtime_support_rejects_data_engine_qsize() {
940 let config = LiveNodeConfig {
941 data_engine: LiveDataEngineConfig {
942 qsize: 1,
943 ..Default::default()
944 },
945 ..Default::default()
946 };
947
948 let error = config.validate_runtime_support().unwrap_err();
949 assert_eq!(
950 error.to_string(),
951 "LiveDataEngineConfig.qsize is not supported by the Rust live runtime yet"
952 );
953 }
954
955 #[rstest]
956 fn test_validate_runtime_support_rejects_risk_engine_qsize() {
957 let config = LiveNodeConfig {
958 risk_engine: LiveRiskEngineConfig {
959 qsize: 1,
960 ..Default::default()
961 },
962 ..Default::default()
963 };
964
965 let error = config.validate_runtime_support().unwrap_err();
966 assert_eq!(
967 error.to_string(),
968 "LiveRiskEngineConfig.qsize is not supported by the Rust live runtime yet"
969 );
970 }
971
972 #[rstest]
973 fn test_live_data_engine_config_converts_to_data_engine_config() {
974 let config = LiveDataEngineConfig {
975 time_bars_build_with_no_updates: false,
976 time_bars_timestamp_on_close: false,
977 time_bars_skip_first_non_full_bar: true,
978 time_bars_interval_type: BarIntervalType::RightOpen,
979 time_bars_build_delay: 1_500,
980 validate_data_sequence: true,
981 buffer_deltas: true,
982 external_clients: Some(vec![ClientId::from("EXTERNAL")]),
983 debug: true,
984 ..Default::default()
985 };
986
987 let converted: DataEngineConfig = config.into();
988
989 assert!(!converted.time_bars_build_with_no_updates);
990 assert!(!converted.time_bars_timestamp_on_close);
991 assert!(converted.time_bars_skip_first_non_full_bar);
992 assert_eq!(
993 converted.time_bars_interval_type,
994 BarIntervalType::RightOpen,
995 );
996 assert_eq!(converted.time_bars_build_delay, 1_500);
997 assert!(converted.time_bars_origins.is_empty());
998 assert!(converted.validate_data_sequence);
999 assert!(converted.buffer_deltas);
1000 assert!(!converted.emit_quotes_from_book);
1001 assert!(!converted.emit_quotes_from_book_depths);
1002 assert_eq!(
1003 converted.external_clients,
1004 Some(vec![ClientId::from("EXTERNAL")]),
1005 );
1006 assert!(converted.debug);
1007 }
1008
1009 #[rstest]
1010 fn test_live_data_engine_config_converts_time_bars_origins() {
1011 let config = LiveDataEngineConfig {
1012 time_bars_origins: HashMap::from([("Minute".to_string(), 5_000_000_000)]),
1013 emit_quotes_from_book: true,
1014 emit_quotes_from_book_depths: true,
1015 ..Default::default()
1016 };
1017
1018 let converted: DataEngineConfig = config.into();
1019
1020 assert_eq!(converted.time_bars_origins.len(), 1);
1021 assert_eq!(
1022 converted.time_bars_origins[&BarAggregation::Minute],
1023 Duration::from_nanos(5_000_000_000),
1024 );
1025 assert!(converted.emit_quotes_from_book);
1026 assert!(converted.emit_quotes_from_book_depths);
1027 }
1028
1029 #[rstest]
1030 fn test_live_exec_engine_config_converts_to_exec_engine_config() {
1031 let config = LiveExecEngineConfig {
1032 load_cache: false,
1033 snapshot_positions_interval_secs: Some(30.0),
1034 ..Default::default()
1035 };
1036
1037 let converted: ExecutionEngineConfig = config.into();
1038
1039 assert!(!converted.load_cache);
1040 assert_eq!(converted.snapshot_positions_interval_secs, Some(30.0));
1041 }
1042
1043 #[rstest]
1044 fn test_live_risk_engine_config_converts_to_risk_engine_config() {
1045 let config = LiveRiskEngineConfig {
1046 bypass: true,
1047 max_order_submit_rate: "12/00:00:03".to_string(),
1048 max_order_modify_rate: "7/00:00:05".to_string(),
1049 max_notional_per_order: HashMap::from([(
1050 "ETHUSDT.BINANCE".to_string(),
1051 "1000.5".to_string(),
1052 )]),
1053 debug: true,
1054 ..Default::default()
1055 };
1056
1057 let converted: RiskEngineConfig = config.into();
1058
1059 assert!(converted.bypass);
1060 assert_eq!(
1061 converted.max_order_submit,
1062 RateLimit::new(12, 3_000_000_000)
1063 );
1064 assert_eq!(converted.max_order_modify, RateLimit::new(7, 5_000_000_000));
1065 assert_eq!(
1066 converted.max_notional_per_order[&"ETHUSDT.BINANCE".parse::<InstrumentId>().unwrap()],
1067 Decimal::from_str("1000.5").unwrap(),
1068 );
1069 assert!(converted.debug);
1070 }
1071
1072 #[rstest]
1073 fn test_validate_runtime_support_rejects_exec_engine_snapshot_orders() {
1074 let config = LiveNodeConfig {
1075 exec_engine: LiveExecEngineConfig {
1076 snapshot_orders: true,
1077 ..Default::default()
1078 },
1079 ..Default::default()
1080 };
1081
1082 let error = config.validate_runtime_support().unwrap_err();
1083 assert_eq!(
1084 error.to_string(),
1085 "LiveExecEngineConfig.snapshot_orders is not supported by the Rust live runtime yet"
1086 );
1087 }
1088
1089 #[rstest]
1090 fn test_validate_runtime_support_rejects_invalid_rate_limit() {
1091 let config = LiveNodeConfig {
1092 risk_engine: LiveRiskEngineConfig {
1093 max_order_submit_rate: "bad-rate".to_string(),
1094 ..Default::default()
1095 },
1096 ..Default::default()
1097 };
1098
1099 let error = config.validate_runtime_support().unwrap_err().to_string();
1100 assert!(error.contains("LiveRiskEngineConfig.max_order_submit_rate"));
1101 }
1102
1103 #[rstest]
1104 #[case(-1.0)]
1105 #[case(f64::NAN)]
1106 #[case(f64::INFINITY)]
1107 #[case(f64::NEG_INFINITY)]
1108 fn test_validate_runtime_support_rejects_hostile_startup_delay(#[case] value: f64) {
1109 let config = LiveNodeConfig {
1110 exec_engine: LiveExecEngineConfig {
1111 reconciliation_startup_delay_secs: value,
1112 ..Default::default()
1113 },
1114 ..Default::default()
1115 };
1116
1117 let error = config.validate_runtime_support().unwrap_err().to_string();
1118 assert!(error.contains("reconciliation_startup_delay_secs"));
1119 }
1120
1121 #[rstest]
1122 fn test_validate_runtime_support_rejects_invalid_reconciliation_instrument_id() {
1123 let config = LiveNodeConfig {
1124 exec_engine: LiveExecEngineConfig {
1125 reconciliation_instrument_ids: Some(vec!["INVALID".to_string()]),
1126 ..Default::default()
1127 },
1128 ..Default::default()
1129 };
1130
1131 let error = config.validate_runtime_support().unwrap_err().to_string();
1132 assert!(error.contains("reconciliation_instrument_ids"));
1133 }
1134
1135 #[rstest]
1136 fn test_parse_rate_limit_happy_path() {
1137 let limit = parse_rate_limit("150/00:00:02").unwrap();
1138 assert_eq!(limit, RateLimit::new(150, 2_000_000_000));
1139 }
1140
1141 #[rstest]
1142 fn test_parse_rate_limit_rejects_trailing_component() {
1143 let err = parse_rate_limit("10/00:00:01:99").unwrap_err().to_string();
1144 assert!(err.contains("expected 'limit/HH:MM:SS'"));
1145 }
1146
1147 #[rstest]
1148 fn test_parse_rate_limit_rejects_zero_limit() {
1149 let err = parse_rate_limit("0/00:00:01").unwrap_err().to_string();
1150 assert!(err.contains("limit must be greater than zero"));
1151 }
1152
1153 #[rstest]
1154 fn test_parse_rate_limit_rejects_zero_interval() {
1155 let err = parse_rate_limit("100/00:00:00").unwrap_err().to_string();
1156 assert!(err.contains("interval must be greater than zero"));
1157 }
1158
1159 #[rstest]
1160 fn test_validate_runtime_support_rejects_exec_engine_qsize() {
1161 let config = LiveNodeConfig {
1162 exec_engine: LiveExecEngineConfig {
1163 qsize: 1,
1164 ..Default::default()
1165 },
1166 ..Default::default()
1167 };
1168
1169 let error = config.validate_runtime_support().unwrap_err();
1170 assert_eq!(
1171 error.to_string(),
1172 "LiveExecEngineConfig.qsize is not supported by the Rust live runtime yet"
1173 );
1174 }
1175
1176 #[rstest]
1177 fn test_validate_runtime_support_rejects_data_engine_graceful_shutdown() {
1178 let config = LiveNodeConfig {
1179 data_engine: LiveDataEngineConfig {
1180 graceful_shutdown_on_error: true,
1181 ..Default::default()
1182 },
1183 ..Default::default()
1184 };
1185
1186 let error = config.validate_runtime_support().unwrap_err().to_string();
1187 assert!(error.contains("graceful_shutdown_on_error"));
1188 }
1189
1190 #[rstest]
1191 fn test_validate_runtime_support_rejects_risk_engine_graceful_shutdown() {
1192 let config = LiveNodeConfig {
1193 risk_engine: LiveRiskEngineConfig {
1194 graceful_shutdown_on_error: true,
1195 ..Default::default()
1196 },
1197 ..Default::default()
1198 };
1199
1200 let error = config.validate_runtime_support().unwrap_err().to_string();
1201 assert!(error.contains("graceful_shutdown_on_error"));
1202 }
1203
1204 #[rstest]
1205 fn test_validate_runtime_support_rejects_emulator() {
1206 let config = LiveNodeConfig {
1207 emulator: Some(OrderEmulatorConfig::default()),
1208 ..Default::default()
1209 };
1210
1211 let error = config.validate_runtime_support().unwrap_err().to_string();
1212 assert!(error.contains("emulator"));
1213 }
1214
1215 #[rstest]
1216 fn test_validate_runtime_support_rejects_loop_debug() {
1217 let config = LiveNodeConfig {
1218 loop_debug: true,
1219 ..Default::default()
1220 };
1221
1222 let error = config.validate_runtime_support().unwrap_err().to_string();
1223 assert!(error.contains("loop_debug"));
1224 }
1225
1226 #[rstest]
1227 fn test_validate_runtime_support_rejects_file_config() {
1228 use nautilus_common::logging::writer::FileWriterConfig;
1229
1230 let config = LiveNodeConfig {
1231 logging: LoggerConfig {
1232 file_config: Some(FileWriterConfig::default()),
1233 ..Default::default()
1234 },
1235 ..Default::default()
1236 };
1237
1238 let error = config.validate_runtime_support().unwrap_err().to_string();
1239 assert!(error.contains("file_config"));
1240 }
1241
1242 #[rstest]
1243 fn test_validate_runtime_support_rejects_clear_log_file() {
1244 let config = LiveNodeConfig {
1245 logging: LoggerConfig {
1246 clear_log_file: true,
1247 ..Default::default()
1248 },
1249 ..Default::default()
1250 };
1251
1252 let error = config.validate_runtime_support().unwrap_err().to_string();
1253 assert!(error.contains("clear_log_file"));
1254 }
1255
1256 #[rstest]
1257 fn test_validate_runtime_support_rejects_invalid_time_bars_origins_key() {
1258 let config = LiveNodeConfig {
1259 data_engine: LiveDataEngineConfig {
1260 time_bars_origins: HashMap::from([("INVALID".to_string(), 1_000)]),
1261 ..Default::default()
1262 },
1263 ..Default::default()
1264 };
1265
1266 let error = config.validate_runtime_support().unwrap_err().to_string();
1267 assert!(error.contains("time_bars_origins"));
1268 }
1269
1270 #[rstest]
1271 fn test_live_exec_engine_config_defaults() {
1272 let config = LiveExecEngineConfig::default();
1273
1274 assert!(config.load_cache);
1275 assert!(!config.snapshot_orders);
1276 assert!(!config.snapshot_positions);
1277 assert_eq!(config.snapshot_positions_interval_secs, None);
1278 assert_eq!(config.external_clients, None);
1279 assert!(!config.debug);
1280 assert!(!config.manage_own_order_books);
1281 assert!(!config.allow_overfills);
1282 assert!(config.reconciliation);
1283 assert_eq!(config.reconciliation_startup_delay_secs, 10.0);
1284 assert_eq!(config.reconciliation_lookback_mins, None);
1285 assert_eq!(config.reconciliation_instrument_ids, None);
1286 assert_eq!(config.filtered_client_order_ids, None);
1287 assert!(!config.filter_unclaimed_external_orders);
1288 assert!(!config.filter_position_reports);
1289 assert!(config.generate_missing_orders);
1290 assert_eq!(config.inflight_check_interval_ms, 2_000);
1291 assert_eq!(config.inflight_check_threshold_ms, 5_000);
1292 assert_eq!(config.inflight_check_retries, 5);
1293 assert_eq!(config.open_check_threshold_ms, 5_000);
1294 assert_eq!(config.open_check_lookback_mins, Some(60));
1295 assert_eq!(config.open_check_missing_retries, 5);
1296 assert!(config.open_check_open_only);
1297 assert_eq!(config.max_single_order_queries_per_cycle, 10);
1298 assert_eq!(config.position_check_threshold_ms, 5_000);
1299 assert_eq!(config.position_check_retries, 3);
1300 assert!(!config.purge_from_database);
1301 assert!(!config.graceful_shutdown_on_error);
1302 assert_eq!(config.qsize, 100_000);
1303 }
1304
1305 #[rstest]
1306 fn test_live_data_engine_config_defaults() {
1307 let config = LiveDataEngineConfig::default();
1308
1309 assert!(config.time_bars_build_with_no_updates);
1310 assert!(config.time_bars_timestamp_on_close);
1311 assert!(!config.time_bars_skip_first_non_full_bar);
1312 assert_eq!(config.time_bars_interval_type, BarIntervalType::LeftOpen);
1313 assert_eq!(config.time_bars_build_delay, 0);
1314 assert!(config.time_bars_origins.is_empty());
1315 assert!(!config.validate_data_sequence);
1316 assert!(!config.buffer_deltas);
1317 assert!(!config.emit_quotes_from_book);
1318 assert!(!config.emit_quotes_from_book_depths);
1319 assert_eq!(config.external_clients, None);
1320 assert!(!config.debug);
1321 assert!(!config.graceful_shutdown_on_error);
1322 assert_eq!(config.qsize, 100_000);
1323 }
1324
1325 #[rstest]
1326 fn test_live_risk_engine_config_defaults() {
1327 let config = LiveRiskEngineConfig::default();
1328
1329 assert!(!config.bypass);
1330 assert_eq!(config.max_order_submit_rate, DEFAULT_ORDER_RATE_LIMIT);
1331 assert_eq!(config.max_order_modify_rate, DEFAULT_ORDER_RATE_LIMIT);
1332 assert!(config.max_notional_per_order.is_empty());
1333 assert!(!config.debug);
1334 assert!(!config.graceful_shutdown_on_error);
1335 assert_eq!(config.qsize, 100_000);
1336 }
1337
1338 #[rstest]
1339 fn test_routing_config_default() {
1340 let config = RoutingConfig::default();
1341
1342 assert!(!config.default);
1343 assert_eq!(config.venues, None);
1344 }
1345
1346 #[rstest]
1347 fn test_live_data_client_config_default() {
1348 let config = LiveDataClientConfig::default();
1349
1350 assert!(!config.handle_revised_bars);
1351 assert!(!config.instrument_provider.load_all);
1352 assert!(config.instrument_provider.load_ids.is_none());
1353 assert!(config.instrument_provider.filters.is_empty());
1354 assert!(config.instrument_provider.filter_callable.is_none());
1355 assert!(config.instrument_provider.log_warnings);
1356 assert!(!config.routing.default);
1357 }
1358
1359 #[rstest]
1360 fn test_live_data_client_config_rejects_unknown_field() {
1361 let error = serde_json::from_str::<LiveDataClientConfig>(
1362 r#"{"handle_revised_bars":true,"unexpected":true}"#,
1363 )
1364 .unwrap_err();
1365
1366 assert!(error.to_string().contains("unknown field `unexpected`"));
1367 }
1368
1369 #[rstest]
1370 fn test_live_data_client_config_rejects_unknown_nested_field() {
1371 let error = serde_json::from_str::<LiveDataClientConfig>(
1372 r#"{"instrument_provider":{"load_all":true,"instrument_provider":{"load_all":false}}}"#,
1373 )
1374 .unwrap_err();
1375
1376 assert!(
1377 error
1378 .to_string()
1379 .contains("unknown field `instrument_provider`")
1380 );
1381 }
1382}