Skip to main content

nautilus_live/
config.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Configuration types for live Nautilus system nodes.
17
18use 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
40/// The default rate limit string used for order submission and modification.
41const DEFAULT_ORDER_RATE_LIMIT: &str = "100/00:00:01";
42
43/// Configuration for live data engines.
44#[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    /// If time bar aggregators will build and emit bars with no new market updates.
56    #[builder(default = true)]
57    pub time_bars_build_with_no_updates: bool,
58    /// If time bar aggregators will timestamp `ts_event` on bar close.
59    /// If false, the aggregator will timestamp on bar open.
60    #[builder(default = true)]
61    pub time_bars_timestamp_on_close: bool,
62    /// If time bar aggregators will skip emitting a bar when aggregation starts mid-interval.
63    #[builder(default)]
64    pub time_bars_skip_first_non_full_bar: bool,
65    /// The interval semantics used for time aggregation.
66    #[builder(default = BarIntervalType::LeftOpen)]
67    pub time_bars_interval_type: BarIntervalType,
68    /// The build delay (microseconds) before a time bar is emitted.
69    #[builder(default)]
70    pub time_bars_build_delay: u64,
71    /// A mapping of time bar aggregation types to their origin time offsets (nanoseconds).
72    ///
73    /// Keys are `BarAggregation` variant names, values are offset durations in nanoseconds.
74    #[builder(default)]
75    pub time_bars_origins: HashMap<String, u64>,
76    /// If data timestamp sequencing should be validated and handled.
77    #[builder(default)]
78    pub validate_data_sequence: bool,
79    /// If order book deltas should be buffered until the `F_LAST` flag is set for a delta.
80    #[builder(default)]
81    pub buffer_deltas: bool,
82    /// If quotes should be emitted on order book updates.
83    #[builder(default)]
84    pub emit_quotes_from_book: bool,
85    /// If quotes should be emitted on order book depth updates.
86    #[builder(default)]
87    pub emit_quotes_from_book_depths: bool,
88    /// Client IDs declared for external stream processing.
89    ///
90    /// The data engine will not attempt to send data commands to these client IDs.
91    pub external_clients: Option<Vec<ClientId>>,
92    /// If debug mode is active (will provide extra debug logging).
93    #[builder(default)]
94    pub debug: bool,
95    /// If the engine should gracefully shut down when queue processing encounters unexpected errors.
96    #[builder(default)]
97    pub graceful_shutdown_on_error: bool,
98    /// The queue size for the engine's internal queue buffers.
99    ///
100    /// Not implemented on the current live runtime; `validate_runtime_support` rejects
101    /// any value other than the default.
102    #[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/// Configuration for live risk engines.
142#[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    /// If all pre-trade risk checks should be bypassed.
154    #[builder(default)]
155    pub bypass: bool,
156    /// The maximum submit order rate as `limit/HH:MM:SS`.
157    #[builder(default = DEFAULT_ORDER_RATE_LIMIT.to_string())]
158    pub max_order_submit_rate: String,
159    /// The maximum modify order rate as `limit/HH:MM:SS`.
160    #[builder(default = DEFAULT_ORDER_RATE_LIMIT.to_string())]
161    pub max_order_modify_rate: String,
162    /// The maximum notional per order keyed by instrument ID.
163    ///
164    /// Entries map instrument ID strings to decimal notional strings.
165    #[builder(default)]
166    pub max_notional_per_order: HashMap<String, String>,
167    /// If debug mode is active (will provide extra debug logging).
168    #[builder(default)]
169    pub debug: bool,
170    /// If the engine should gracefully shut down when queue processing encounters unexpected errors.
171    #[builder(default)]
172    pub graceful_shutdown_on_error: bool,
173    /// The queue size for the engine's internal queue buffers.
174    ///
175    /// Not implemented on the current live runtime; `validate_runtime_support` rejects
176    /// any value other than the default.
177    #[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(&notional)
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/// Configuration for live execution engines.
259#[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    /// If the cache should be loaded on initialization.
271    #[builder(default = true)]
272    pub load_cache: bool,
273    /// If order state snapshot lists should be persisted to a backing database.
274    ///
275    /// Not implemented on the current live runtime; `validate_runtime_support` rejects
276    /// any value other than the default because the live kernel does not yet wire a
277    /// cache database adapter.
278    #[builder(default)]
279    pub snapshot_orders: bool,
280    /// If position state snapshot lists should be persisted to a backing database.
281    ///
282    /// Not implemented on the current live runtime; `validate_runtime_support` rejects
283    /// any value other than the default because the live kernel does not yet wire a
284    /// cache database adapter.
285    #[builder(default)]
286    pub snapshot_positions: bool,
287    /// The interval (seconds) at which additional position state snapshots are persisted.
288    /// If `None` then no additional snapshots will be taken.
289    pub snapshot_positions_interval_secs: Option<f64>,
290    /// Client IDs declared for external stream processing.
291    ///
292    /// The execution engine will not attempt to send trading commands to these client
293    /// IDs, assuming an external process consumes them from the bus.
294    pub external_clients: Option<Vec<ClientId>>,
295    /// If debug mode is active (will provide extra debug logging).
296    #[builder(default)]
297    pub debug: bool,
298    /// If reconciliation is active at start-up.
299    #[builder(default = true)]
300    pub reconciliation: bool,
301    /// The delay (seconds) before starting reconciliation at startup.
302    #[builder(default = 10.0)]
303    pub reconciliation_startup_delay_secs: f64,
304    /// The maximum lookback minutes to reconcile state for.
305    pub reconciliation_lookback_mins: Option<u32>,
306    /// Specific instrument IDs to reconcile (if None, reconciles all).
307    pub reconciliation_instrument_ids: Option<Vec<String>>,
308    /// If unclaimed order events with an EXTERNAL strategy ID should be filtered/dropped.
309    #[builder(default)]
310    pub filter_unclaimed_external_orders: bool,
311    /// If position status reports are filtered from reconciliation.
312    #[builder(default)]
313    pub filter_position_reports: bool,
314    /// Client order IDs to filter from reconciliation.
315    pub filtered_client_order_ids: Option<Vec<String>>,
316    /// If MARKET order events will be generated during reconciliation to align discrepancies.
317    #[builder(default = true)]
318    pub generate_missing_orders: bool,
319    /// The interval (milliseconds) between checking whether in-flight orders have exceeded their threshold.
320    #[builder(default = 2_000)]
321    pub inflight_check_interval_ms: u32,
322    /// The threshold (milliseconds) beyond which an in-flight order's status is checked with the venue.
323    #[builder(default = 5_000)]
324    pub inflight_check_threshold_ms: u32,
325    /// The number of retry attempts for verifying in-flight order status.
326    #[builder(default = 5)]
327    pub inflight_check_retries: u32,
328    /// The interval (seconds) between checks for open orders at the venue.
329    pub open_check_interval_secs: Option<f64>,
330    /// The lookback minutes for open order checks.
331    /// When `None`, the check is unbounded (no time filter).
332    pub open_check_lookback_mins: Option<u32>,
333    /// The minimum elapsed time (milliseconds) since an order update before acting on discrepancies.
334    #[builder(default = 5_000)]
335    pub open_check_threshold_ms: u32,
336    /// The number of retries for missing open orders.
337    #[builder(default = 5)]
338    pub open_check_missing_retries: u32,
339    /// If the `check_open_orders` requests only currently open orders from the venue.
340    #[builder(default = true)]
341    pub open_check_open_only: bool,
342    /// The maximum number of single-order queries per consistency check cycle.
343    #[builder(default = 10)]
344    pub max_single_order_queries_per_cycle: u32,
345    /// The delay (milliseconds) between consecutive single-order queries.
346    #[builder(default = 100)]
347    pub single_order_query_delay_ms: u32,
348    /// The interval (seconds) between checks for open positions at the venue.
349    pub position_check_interval_secs: Option<f64>,
350    /// The lookback minutes for position consistency checks.
351    #[builder(default = 60)]
352    pub position_check_lookback_mins: u32,
353    /// The minimum elapsed time (milliseconds) since a position update before acting on discrepancies.
354    #[builder(default = 5_000)]
355    pub position_check_threshold_ms: u32,
356    /// The maximum number of reconciliation attempts for a position discrepancy.
357    #[builder(default = 3)]
358    pub position_check_retries: u32,
359    /// The interval (minutes) between purging closed orders from the in-memory cache.
360    pub purge_closed_orders_interval_mins: Option<u32>,
361    /// The time buffer (minutes) before closed orders can be purged.
362    pub purge_closed_orders_buffer_mins: Option<u32>,
363    /// The interval (minutes) between purging closed positions from the in-memory cache.
364    pub purge_closed_positions_interval_mins: Option<u32>,
365    /// The time buffer (minutes) before closed positions can be purged.
366    pub purge_closed_positions_buffer_mins: Option<u32>,
367    /// The interval (minutes) between purging account events from the in-memory cache.
368    pub purge_account_events_interval_mins: Option<u32>,
369    /// The time buffer (minutes) before account events can be purged.
370    pub purge_account_events_lookback_mins: Option<u32>,
371    /// If purge operations should also delete from the backing database.
372    #[builder(default)]
373    pub purge_from_database: bool,
374    /// The interval (seconds) between auditing own books against public order books.
375    pub own_books_audit_interval_secs: Option<f64>,
376    /// If the engine should gracefully shutdown when queue processing encounters unexpected errors.
377    #[builder(default)]
378    pub graceful_shutdown_on_error: bool,
379    /// The queue size for the engine's internal queue buffers.
380    #[builder(default = 100_000)]
381    pub qsize: u32,
382    /// If order fills exceeding order quantity are allowed (logs warning instead of raising).
383    /// Useful when position reconciliation races with exchange fill events.
384    #[builder(default)]
385    pub allow_overfills: bool,
386    /// If the execution engine should maintain own/user order books based on commands and events.
387    #[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/// Configuration for live client message routing.
423#[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    /// If the client should be registered as the default routing client.
435    #[builder(default)]
436    pub default: bool,
437    /// The venues to register for routing.
438    pub venues: Option<Vec<String>>,
439}
440
441/// Configuration for instrument providers.
442#[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    /// Whether to load all instruments on startup.
454    #[builder(default)]
455    pub load_all: bool,
456    /// Specific instrument IDs to load on startup (if `load_all` is false).
457    pub load_ids: Option<Vec<String>>,
458    /// Venue-specific instrument loading filters.
459    #[builder(default)]
460    pub filters: HashMap<String, serde_json::Value>,
461    /// A fully qualified path to a callable for custom instrument filtering.
462    pub filter_callable: Option<String>,
463    /// If parser warnings should be logged.
464    #[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/// Configuration for live data clients.
475#[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    /// If `DataClient` will emit bar updates when a new bar opens.
487    #[builder(default)]
488    pub handle_revised_bars: bool,
489    /// The client's instrument provider configuration.
490    #[builder(default)]
491    pub instrument_provider: InstrumentProviderConfig,
492    /// The client's message routing configuration.
493    #[builder(default)]
494    pub routing: RoutingConfig,
495}
496
497/// Configuration for live execution clients.
498#[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    /// The client's instrument provider configuration.
510    #[builder(default)]
511    pub instrument_provider: InstrumentProviderConfig,
512    /// The client's message routing configuration.
513    #[builder(default)]
514    pub routing: RoutingConfig,
515}
516
517/// Configuration for live Nautilus system nodes.
518#[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    /// The trading environment.
529    #[builder(default = Environment::Live)]
530    pub environment: Environment,
531    /// The trader ID for the node.
532    #[builder(default = TraderId::from("TRADER-001"))]
533    pub trader_id: TraderId,
534    /// If trading strategy state should be loaded from the database on start.
535    #[builder(default)]
536    pub load_state: bool,
537    /// If trading strategy state should be saved to the database on stop.
538    #[builder(default)]
539    pub save_state: bool,
540    /// The logging configuration for the kernel.
541    #[builder(default)]
542    pub logging: LoggerConfig,
543    /// The unique instance identifier for the kernel
544    pub instance_id: Option<UUID4>,
545    /// The timeout for all clients to connect and initialize.
546    #[builder(default = Duration::from_secs(120))]
547    pub timeout_connection: Duration,
548    /// The timeout for execution state to reconcile.
549    #[builder(default = Duration::from_secs(30))]
550    pub timeout_reconciliation: Duration,
551    /// The timeout for portfolio to initialize margins and unrealized pnls.
552    #[builder(default = Duration::from_secs(10))]
553    pub timeout_portfolio: Duration,
554    /// The timeout for all engine clients to disconnect.
555    #[builder(default = Duration::from_secs(10))]
556    pub timeout_disconnection: Duration,
557    /// The delay after stopping the node to await residual events before final shutdown.
558    #[builder(default = Duration::from_secs(10))]
559    pub delay_post_stop: Duration,
560    /// The timeout to await pending tasks cancellation during shutdown.
561    #[builder(default = Duration::from_secs(5))]
562    pub timeout_shutdown: Duration,
563    /// The cache configuration.
564    pub cache: Option<CacheConfig>,
565    /// The message bus configuration.
566    pub msgbus: Option<MessageBusConfig>,
567    /// The portfolio configuration.
568    pub portfolio: Option<PortfolioConfig>,
569    /// The order emulator configuration.
570    pub emulator: Option<OrderEmulatorConfig>,
571    /// The configuration for streaming to feather files.
572    pub streaming: Option<StreamingConfig>,
573    /// If the asyncio event loop should run in debug mode.
574    #[builder(default)]
575    pub loop_debug: bool,
576    /// The live data engine configuration.
577    #[builder(default)]
578    pub data_engine: LiveDataEngineConfig,
579    /// The live risk engine configuration.
580    #[builder(default)]
581    pub risk_engine: LiveRiskEngineConfig,
582    /// The live execution engine configuration.
583    #[builder(default)]
584    pub exec_engine: LiveExecEngineConfig,
585    /// The data client configurations.
586    #[builder(default)]
587    pub data_clients: HashMap<String, LiveDataClientConfig>,
588    /// The execution client configurations.
589    #[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    /// Validates config fields that the Rust live runtime does not support yet, and checks
601    /// that supported fields hold values the downstream engine conversions can parse.
602    ///
603    /// # Errors
604    ///
605    /// Returns an error when a config field would otherwise be ignored at runtime, or when a
606    /// supported field holds a value that cannot be converted to its engine-side representation.
607    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        // `Duration::from_secs_f64` panics on negative, NaN, or infinite input, and the
717        // `run()` path feeds this value straight in when reconciliation is enabled. Match
718        // the legacy Python `PositiveFloat` semantics and reject hostile values at build.
719        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}