Skip to main content

nautilus_live/python/
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//! Exposes Rust live-node, engine, and client configuration types through PyO3.
17//!
18//! Implements Python constructors and property access, converting Python inputs into Rust
19//! configuration values and reporting invalid inputs as Python exceptions. Includes routing,
20//! instrument-provider, plugin, and queue-monitor settings used when constructing a live node.
21
22use std::{collections::HashMap, hash::BuildHasher, time::Duration};
23
24use nautilus_common::{
25    cache::CacheConfig, enums::Environment, logging::logger::LoggerConfig,
26    msgbus::MessageBusConfig, python::config_error_to_pyvalue_err,
27};
28use nautilus_core::{UUID4, python::to_pyvalue_err};
29use nautilus_model::{
30    enums::BarIntervalType,
31    identifiers::{ClientId, TraderId, Venue},
32};
33use nautilus_persistence::config::{DataCatalogConfig, StreamingConfig};
34use nautilus_portfolio::config::PortfolioConfig;
35use nautilus_trading::ImportableControllerConfig;
36use pyo3::{
37    Bound, IntoPyObject, Py, PyAny, PyResult, Python, pymethods,
38    types::{PyAnyMethods, PyBytes, PyDict, PyDictMethods, PyTuple},
39};
40
41use crate::config::{
42    DataClientConfig, ExecutionClientConfig, InstrumentProviderConfig, LiveDataEngineConfig,
43    LiveExecutionEngineConfig, LiveNodeConfig, LiveRiskEngineConfig, PluginConfig,
44    QueueMonitorConfig, RoutingConfig, SubmissionRecoveryPolicy, duration_from_secs_f64,
45    parse_rate_limit, validate_max_notional_per_order,
46};
47
48// Coerces a PyO3 input into `BarIntervalType`, accepting both the enum (modern Rust
49// surface) and the legacy Python v1 string form (`"left-open"` / `"right-open"`).
50fn coerce_bar_interval_type(value: &Py<PyAny>) -> PyResult<BarIntervalType> {
51    Python::attach(|py| {
52        let bound = value.bind(py);
53        if let Ok(variant) = bound.extract::<BarIntervalType>() {
54            return Ok(variant);
55        }
56
57        let raw = bound.extract::<String>().map_err(|_| {
58            to_pyvalue_err("`time_bars_interval_type` must be a string or BarIntervalType")
59        })?;
60
61        match raw.to_ascii_uppercase().replace('-', "_").as_str() {
62            "LEFT_OPEN" => Ok(BarIntervalType::LeftOpen),
63            "RIGHT_OPEN" => Ok(BarIntervalType::RightOpen),
64            _ => Err(to_pyvalue_err(format!(
65                "invalid `time_bars_interval_type`: {raw:?} (expected 'left-open' or 'right-open')"
66            ))),
67        }
68    })
69}
70
71/// Converts a Python value into a [`serde_json::Value`].
72fn py_to_json_value(bound: &pyo3::Bound<'_, PyAny>) -> PyResult<serde_json::Value> {
73    // Check bool before int since Python `bool` is a subclass of `int`
74    if let Ok(b) = bound.extract::<bool>() {
75        Ok(serde_json::Value::Bool(b))
76    } else if let Ok(s) = bound.extract::<String>() {
77        Ok(serde_json::Value::String(s))
78    } else if let Ok(i) = bound.extract::<i64>() {
79        Ok(serde_json::Value::Number(serde_json::Number::from(i)))
80    } else if let Ok(f) = bound.extract::<f64>() {
81        Ok(serde_json::Number::from_f64(f)
82            .map_or(serde_json::Value::Null, serde_json::Value::Number))
83    } else if let Ok(dict) = bound.cast::<PyDict>() {
84        let mut obj = serde_json::Map::with_capacity(dict.len());
85        for (key, value) in dict.iter() {
86            obj.insert(key.extract::<String>()?, py_to_json_value(&value)?);
87        }
88
89        Ok(serde_json::Value::Object(obj))
90    } else if let Ok(items) = bound.extract::<Vec<Py<PyAny>>>() {
91        // Handle list/tuple/set
92        let py = bound.py();
93        let arr: Vec<serde_json::Value> = items
94            .iter()
95            .map(|item| py_to_json_value(item.bind(py)))
96            .collect::<PyResult<_>>()?;
97        Ok(serde_json::Value::Array(arr))
98    } else {
99        // Fall back to string representation
100        let s: String = bound.str()?.extract()?;
101        Ok(serde_json::Value::String(s))
102    }
103}
104
105/// Converts a JSON configuration value into a Python object.
106///
107/// # Errors
108///
109/// Returns an error if Python object construction fails.
110pub fn json_value_to_py(py: Python<'_>, value: &serde_json::Value) -> PyResult<Py<PyAny>> {
111    match value {
112        serde_json::Value::Null => Ok(py.None()),
113        serde_json::Value::Bool(b) => Ok((*b).into_pyobject(py)?.to_owned().into_any().unbind()),
114        serde_json::Value::Number(n) => {
115            if let Some(i) = n.as_i64() {
116                Ok(i.into_pyobject(py)?.into_any().unbind())
117            } else if let Some(u) = n.as_u64() {
118                Ok(u.into_pyobject(py)?.into_any().unbind())
119            } else if let Some(f) = n.as_f64() {
120                Ok(f.into_pyobject(py)?.into_any().unbind())
121            } else {
122                Ok(n.to_string().into_pyobject(py)?.into_any().unbind())
123            }
124        }
125        serde_json::Value::String(s) => Ok(s.into_pyobject(py)?.into_any().unbind()),
126        serde_json::Value::Array(arr) => {
127            let items: Vec<Py<PyAny>> = arr
128                .iter()
129                .map(|v| json_value_to_py(py, v))
130                .collect::<PyResult<_>>()?;
131            Ok(pyo3::types::PyList::new(py, items)?.into_any().unbind())
132        }
133        serde_json::Value::Object(obj) => {
134            let dict = pyo3::types::PyDict::new(py);
135            for (k, v) in obj {
136                dict.set_item(k, json_value_to_py(py, v)?)?;
137            }
138
139            Ok(dict.into_any().unbind())
140        }
141    }
142}
143
144/// Converts Python mapping values into JSON values.
145///
146/// # Errors
147///
148/// Returns an error if a Python value cannot be converted.
149pub fn coerce_json_config<S: BuildHasher>(
150    raw: HashMap<String, Py<PyAny>, S>,
151) -> PyResult<HashMap<String, serde_json::Value>> {
152    Python::attach(|py| -> PyResult<HashMap<String, serde_json::Value>> {
153        let mut result = HashMap::with_capacity(raw.len());
154        for (key, value) in raw {
155            let json_value = py_to_json_value(value.bind(py))?;
156            result.insert(key, json_value);
157        }
158
159        Ok(result)
160    })
161}
162
163// Normalizes a Python `max_notional_per_order` dict (values can be `int`, `float`,
164// `str`, or `Decimal`, matching the legacy Python v1 config contract) into the
165// string-keyed map stored on `LiveRiskEngineConfig`.
166fn coerce_max_notional_per_order(
167    raw: HashMap<String, Py<PyAny>>,
168) -> PyResult<HashMap<String, String>> {
169    Python::attach(|py| -> PyResult<HashMap<String, String>> {
170        let mut result = HashMap::with_capacity(raw.len());
171        for (instrument_id, value) in raw {
172            let value_str: String = value.bind(py).str()?.extract()?;
173            result.insert(instrument_id, value_str);
174        }
175
176        Ok(result)
177    })
178}
179
180#[pyo3_stub_gen::derive::gen_stub_pymethods]
181#[pymethods]
182impl LiveDataEngineConfig {
183    /// Configuration for live data engines.
184    #[new]
185    #[expect(clippy::too_many_arguments)]
186    #[allow(
187        clippy::needless_pass_by_value,
188        reason = "PyO3 #[new] requires owned params"
189    )]
190    #[pyo3(signature = (time_bars_build_with_no_updates=None, time_bars_timestamp_on_close=None, time_bars_skip_first_non_full_bar=None, time_bars_interval_type=None, time_bars_build_delay=None, time_bars_origin_offset=None, validate_data_sequence=None, buffer_deltas=None, emit_quotes_from_book=None, emit_quotes_from_book_depths=None, external_clients=None, debug=None))]
191    fn py_new(
192        time_bars_build_with_no_updates: Option<bool>,
193        time_bars_timestamp_on_close: Option<bool>,
194        time_bars_skip_first_non_full_bar: Option<bool>,
195        time_bars_interval_type: Option<Py<PyAny>>,
196        time_bars_build_delay: Option<u64>,
197        time_bars_origin_offset: Option<HashMap<String, u64>>,
198        validate_data_sequence: Option<bool>,
199        buffer_deltas: Option<bool>,
200        emit_quotes_from_book: Option<bool>,
201        emit_quotes_from_book_depths: Option<bool>,
202        external_clients: Option<Vec<ClientId>>,
203        debug: Option<bool>,
204    ) -> PyResult<Self> {
205        let default = Self::default();
206
207        let time_bars_interval_type = match time_bars_interval_type {
208            Some(ref obj) => coerce_bar_interval_type(obj)?,
209            None => default.time_bars_interval_type,
210        };
211
212        Ok(Self {
213            time_bars_build_with_no_updates: time_bars_build_with_no_updates
214                .unwrap_or(default.time_bars_build_with_no_updates),
215            time_bars_timestamp_on_close: time_bars_timestamp_on_close
216                .unwrap_or(default.time_bars_timestamp_on_close),
217            time_bars_skip_first_non_full_bar: time_bars_skip_first_non_full_bar
218                .unwrap_or(default.time_bars_skip_first_non_full_bar),
219            time_bars_interval_type,
220            time_bars_build_delay: time_bars_build_delay.unwrap_or(default.time_bars_build_delay),
221            time_bars_origin_offset: time_bars_origin_offset.unwrap_or_default(),
222            validate_data_sequence: validate_data_sequence
223                .unwrap_or(default.validate_data_sequence),
224            buffer_deltas: buffer_deltas.unwrap_or(default.buffer_deltas),
225            emit_quotes_from_book: emit_quotes_from_book.unwrap_or(default.emit_quotes_from_book),
226            emit_quotes_from_book_depths: emit_quotes_from_book_depths
227                .unwrap_or(default.emit_quotes_from_book_depths),
228            external_clients,
229            debug: debug.unwrap_or(default.debug),
230            qsize: default.qsize,
231        })
232    }
233
234    #[getter]
235    #[pyo3(name = "time_bars_build_with_no_updates")]
236    const fn py_time_bars_build_with_no_updates(&self) -> bool {
237        self.time_bars_build_with_no_updates
238    }
239
240    #[getter]
241    #[pyo3(name = "time_bars_timestamp_on_close")]
242    const fn py_time_bars_timestamp_on_close(&self) -> bool {
243        self.time_bars_timestamp_on_close
244    }
245
246    #[getter]
247    #[pyo3(name = "time_bars_skip_first_non_full_bar")]
248    const fn py_time_bars_skip_first_non_full_bar(&self) -> bool {
249        self.time_bars_skip_first_non_full_bar
250    }
251
252    #[getter]
253    #[pyo3(name = "time_bars_interval_type")]
254    const fn py_time_bars_interval_type(&self) -> BarIntervalType {
255        self.time_bars_interval_type
256    }
257
258    #[getter]
259    #[pyo3(name = "time_bars_build_delay")]
260    const fn py_time_bars_build_delay(&self) -> u64 {
261        self.time_bars_build_delay
262    }
263
264    #[getter]
265    #[pyo3(name = "time_bars_origin_offset")]
266    fn py_time_bars_origin_offset(&self) -> HashMap<String, u64> {
267        self.time_bars_origin_offset.clone()
268    }
269
270    #[getter]
271    #[pyo3(name = "validate_data_sequence")]
272    const fn py_validate_data_sequence(&self) -> bool {
273        self.validate_data_sequence
274    }
275
276    #[getter]
277    #[pyo3(name = "buffer_deltas")]
278    const fn py_buffer_deltas(&self) -> bool {
279        self.buffer_deltas
280    }
281
282    #[getter]
283    #[pyo3(name = "emit_quotes_from_book")]
284    const fn py_emit_quotes_from_book(&self) -> bool {
285        self.emit_quotes_from_book
286    }
287
288    #[getter]
289    #[pyo3(name = "emit_quotes_from_book_depths")]
290    const fn py_emit_quotes_from_book_depths(&self) -> bool {
291        self.emit_quotes_from_book_depths
292    }
293
294    #[getter]
295    #[pyo3(name = "external_clients")]
296    fn py_external_clients(&self) -> Option<Vec<ClientId>> {
297        self.external_clients.clone()
298    }
299
300    #[getter]
301    #[pyo3(name = "debug")]
302    const fn py_debug(&self) -> bool {
303        self.debug
304    }
305
306    fn __repr__(&self) -> String {
307        format!("{self:?}")
308    }
309
310    fn __str__(&self) -> String {
311        format!("{self:?}")
312    }
313}
314
315#[pyo3_stub_gen::derive::gen_stub_pymethods]
316#[pymethods]
317impl LiveRiskEngineConfig {
318    /// Configuration for live risk engines.
319    #[new]
320    #[pyo3(signature = (bypass=None, max_order_submit_rate=None, max_order_modify_rate=None, max_notional_per_order=None, full_position_exit_venues=None, debug=None))]
321    fn py_new(
322        bypass: Option<bool>,
323        max_order_submit_rate: Option<String>,
324        max_order_modify_rate: Option<String>,
325        max_notional_per_order: Option<HashMap<String, Py<PyAny>>>,
326        full_position_exit_venues: Option<Vec<Venue>>,
327        debug: Option<bool>,
328    ) -> PyResult<Self> {
329        let default = Self::default();
330        let max_order_submit_rate =
331            max_order_submit_rate.unwrap_or_else(|| default.max_order_submit_rate.clone());
332        let max_order_modify_rate =
333            max_order_modify_rate.unwrap_or_else(|| default.max_order_modify_rate.clone());
334
335        let max_notional_per_order = match max_notional_per_order {
336            Some(raw) => coerce_max_notional_per_order(raw)?,
337            None => HashMap::new(),
338        };
339
340        let full_position_exit_venues = full_position_exit_venues.unwrap_or_default();
341
342        parse_rate_limit(
343            "LiveRiskEngineConfig.max_order_submit_rate",
344            &max_order_submit_rate,
345        )
346        .map_err(config_error_to_pyvalue_err)?;
347        parse_rate_limit(
348            "LiveRiskEngineConfig.max_order_modify_rate",
349            &max_order_modify_rate,
350        )
351        .map_err(config_error_to_pyvalue_err)?;
352        validate_max_notional_per_order(
353            "LiveRiskEngineConfig.max_notional_per_order",
354            &max_notional_per_order,
355        )
356        .map_err(config_error_to_pyvalue_err)?;
357
358        Ok(Self {
359            bypass: bypass.unwrap_or(default.bypass),
360            max_order_submit_rate,
361            max_order_modify_rate,
362            max_notional_per_order,
363            full_position_exit_venues,
364            debug: debug.unwrap_or(default.debug),
365            qsize: default.qsize,
366        })
367    }
368
369    #[getter]
370    #[pyo3(name = "bypass")]
371    const fn py_bypass(&self) -> bool {
372        self.bypass
373    }
374
375    #[getter]
376    #[pyo3(name = "max_order_submit_rate")]
377    fn py_max_order_submit_rate(&self) -> &str {
378        &self.max_order_submit_rate
379    }
380
381    #[getter]
382    #[pyo3(name = "max_order_modify_rate")]
383    fn py_max_order_modify_rate(&self) -> &str {
384        &self.max_order_modify_rate
385    }
386
387    #[getter]
388    #[pyo3(name = "max_notional_per_order")]
389    fn py_max_notional_per_order(&self) -> HashMap<String, String> {
390        self.max_notional_per_order.clone()
391    }
392
393    #[getter]
394    #[pyo3(name = "full_position_exit_venues")]
395    fn py_full_position_exit_venues(&self) -> Vec<Venue> {
396        self.full_position_exit_venues.clone()
397    }
398
399    #[getter]
400    #[pyo3(name = "debug")]
401    const fn py_debug(&self) -> bool {
402        self.debug
403    }
404
405    fn __repr__(&self) -> String {
406        format!("{self:?}")
407    }
408
409    fn __str__(&self) -> String {
410        format!("{self:?}")
411    }
412}
413
414#[pyo3_stub_gen::derive::gen_stub_pymethods]
415#[pymethods]
416impl LiveExecutionEngineConfig {
417    /// Configuration for live execution engines.
418    #[new]
419    #[expect(clippy::too_many_arguments)]
420    #[pyo3(signature = (load_cache=None, manage_own_order_books=None, snapshot_positions_interval_secs=None, external_clients=None, allow_overfills=None, reconciliation=None, reconciliation_startup_delay_secs=None, reconciliation_lookback_mins=None, reconciliation_instrument_ids=None, filter_unclaimed_external_orders=None, filter_position_reports=None, filtered_client_order_ids=None, generate_missing_orders=None, inflight_check_interval_ms=None, inflight_check_threshold_ms=None, inflight_check_retries=None, open_check_interval_secs=None, open_check_lookback_mins=None, open_check_threshold_ms=None, open_check_missing_retries=None, open_check_open_only=None, max_single_order_queries_per_cycle=None, single_order_query_delay_ms=None, position_check_interval_secs=None, position_check_lookback_mins=None, position_check_threshold_ms=None, position_check_retries=None, purge_closed_orders_interval_mins=None, purge_closed_orders_buffer_mins=None, purge_closed_positions_interval_mins=None, purge_closed_positions_buffer_mins=None, purge_account_events_interval_mins=None, purge_account_events_lookback_mins=None, own_books_audit_interval_secs=None, debug=None, snapshot_orders=None, snapshot_positions=None, submission_recovery_policy=None))]
421    fn py_new(
422        load_cache: Option<bool>,
423        manage_own_order_books: Option<bool>,
424        snapshot_positions_interval_secs: Option<f64>,
425        external_clients: Option<Vec<ClientId>>,
426        allow_overfills: Option<bool>,
427        reconciliation: Option<bool>,
428        reconciliation_startup_delay_secs: Option<f64>,
429        reconciliation_lookback_mins: Option<u32>,
430        reconciliation_instrument_ids: Option<Vec<String>>,
431        filter_unclaimed_external_orders: Option<bool>,
432        filter_position_reports: Option<bool>,
433        filtered_client_order_ids: Option<Vec<String>>,
434        generate_missing_orders: Option<bool>,
435        inflight_check_interval_ms: Option<u32>,
436        inflight_check_threshold_ms: Option<u32>,
437        inflight_check_retries: Option<u32>,
438        open_check_interval_secs: Option<f64>,
439        open_check_lookback_mins: Option<u32>,
440        open_check_threshold_ms: Option<u32>,
441        open_check_missing_retries: Option<u32>,
442        open_check_open_only: Option<bool>,
443        max_single_order_queries_per_cycle: Option<u32>,
444        single_order_query_delay_ms: Option<u32>,
445        position_check_interval_secs: Option<f64>,
446        position_check_lookback_mins: Option<u32>,
447        position_check_threshold_ms: Option<u32>,
448        position_check_retries: Option<u32>,
449        purge_closed_orders_interval_mins: Option<u32>,
450        purge_closed_orders_buffer_mins: Option<u32>,
451        purge_closed_positions_interval_mins: Option<u32>,
452        purge_closed_positions_buffer_mins: Option<u32>,
453        purge_account_events_interval_mins: Option<u32>,
454        purge_account_events_lookback_mins: Option<u32>,
455        own_books_audit_interval_secs: Option<f64>,
456        debug: Option<bool>,
457        snapshot_orders: Option<bool>,
458        snapshot_positions: Option<bool>,
459        submission_recovery_policy: Option<SubmissionRecoveryPolicy>,
460    ) -> PyResult<Self> {
461        let default = Self::default();
462
463        let config = Self {
464            load_cache: load_cache.unwrap_or(default.load_cache),
465            manage_own_order_books: manage_own_order_books
466                .unwrap_or(default.manage_own_order_books),
467            snapshot_orders: snapshot_orders.unwrap_or(default.snapshot_orders),
468            snapshot_positions: snapshot_positions.unwrap_or(default.snapshot_positions),
469            snapshot_positions_interval_secs,
470            external_clients,
471            allow_overfills: allow_overfills.unwrap_or(default.allow_overfills),
472            reconciliation: reconciliation.unwrap_or(default.reconciliation),
473            reconciliation_startup_delay_secs: reconciliation_startup_delay_secs
474                .unwrap_or(default.reconciliation_startup_delay_secs),
475            reconciliation_lookback_mins,
476            reconciliation_instrument_ids,
477            filter_unclaimed_external_orders: filter_unclaimed_external_orders
478                .unwrap_or(default.filter_unclaimed_external_orders),
479            filter_position_reports: filter_position_reports
480                .unwrap_or(default.filter_position_reports),
481            filtered_client_order_ids,
482            generate_missing_orders: generate_missing_orders
483                .unwrap_or(default.generate_missing_orders),
484            inflight_check_interval_ms: inflight_check_interval_ms
485                .unwrap_or(default.inflight_check_interval_ms),
486            inflight_check_threshold_ms: inflight_check_threshold_ms
487                .unwrap_or(default.inflight_check_threshold_ms),
488            inflight_check_retries: inflight_check_retries
489                .unwrap_or(default.inflight_check_retries),
490            submission_recovery_policy: submission_recovery_policy
491                .unwrap_or(default.submission_recovery_policy),
492            open_check_interval_secs,
493            open_check_lookback_mins: open_check_lookback_mins.or(default.open_check_lookback_mins),
494            open_check_threshold_ms: open_check_threshold_ms
495                .unwrap_or(default.open_check_threshold_ms),
496            open_check_missing_retries: open_check_missing_retries
497                .unwrap_or(default.open_check_missing_retries),
498            open_check_open_only: open_check_open_only.unwrap_or(default.open_check_open_only),
499            max_single_order_queries_per_cycle: max_single_order_queries_per_cycle
500                .unwrap_or(default.max_single_order_queries_per_cycle),
501            single_order_query_delay_ms: single_order_query_delay_ms
502                .unwrap_or(default.single_order_query_delay_ms),
503            position_check_interval_secs,
504            position_check_lookback_mins: position_check_lookback_mins
505                .unwrap_or(default.position_check_lookback_mins),
506            position_check_threshold_ms: position_check_threshold_ms
507                .unwrap_or(default.position_check_threshold_ms),
508            position_check_retries: position_check_retries
509                .unwrap_or(default.position_check_retries),
510            purge_closed_orders_interval_mins,
511            purge_closed_orders_buffer_mins,
512            purge_closed_positions_interval_mins,
513            purge_closed_positions_buffer_mins,
514            purge_account_events_interval_mins,
515            purge_account_events_lookback_mins,
516            purge_from_database: default.purge_from_database,
517            debug: debug.unwrap_or(default.debug),
518            own_books_audit_interval_secs,
519            qsize: default.qsize,
520        };
521
522        config
523            .validate_runtime_support()
524            .map_err(config_error_to_pyvalue_err)?;
525        Ok(config)
526    }
527
528    #[getter]
529    #[pyo3(name = "load_cache")]
530    const fn py_load_cache(&self) -> bool {
531        self.load_cache
532    }
533
534    #[getter]
535    #[pyo3(name = "manage_own_order_books")]
536    const fn py_manage_own_order_books(&self) -> bool {
537        self.manage_own_order_books
538    }
539
540    #[getter]
541    #[pyo3(name = "snapshot_orders")]
542    const fn py_snapshot_orders(&self) -> bool {
543        self.snapshot_orders
544    }
545
546    #[getter]
547    #[pyo3(name = "snapshot_positions")]
548    const fn py_snapshot_positions(&self) -> bool {
549        self.snapshot_positions
550    }
551
552    #[getter]
553    #[pyo3(name = "snapshot_positions_interval_secs")]
554    const fn py_snapshot_positions_interval_secs(&self) -> Option<f64> {
555        self.snapshot_positions_interval_secs
556    }
557
558    #[getter]
559    #[pyo3(name = "external_clients")]
560    fn py_external_clients(&self) -> Option<Vec<ClientId>> {
561        self.external_clients.clone()
562    }
563
564    #[getter]
565    #[pyo3(name = "allow_overfills")]
566    const fn py_allow_overfills(&self) -> bool {
567        self.allow_overfills
568    }
569
570    #[getter]
571    #[pyo3(name = "reconciliation")]
572    const fn py_reconciliation(&self) -> bool {
573        self.reconciliation
574    }
575
576    #[getter]
577    #[pyo3(name = "reconciliation_startup_delay_secs")]
578    const fn py_reconciliation_startup_delay_secs(&self) -> f64 {
579        self.reconciliation_startup_delay_secs
580    }
581
582    #[getter]
583    #[pyo3(name = "reconciliation_lookback_mins")]
584    const fn py_reconciliation_lookback_mins(&self) -> Option<u32> {
585        self.reconciliation_lookback_mins
586    }
587
588    #[getter]
589    #[pyo3(name = "reconciliation_instrument_ids")]
590    fn py_reconciliation_instrument_ids(&self) -> Option<Vec<String>> {
591        self.reconciliation_instrument_ids.clone()
592    }
593
594    #[getter]
595    #[pyo3(name = "filter_unclaimed_external_orders")]
596    const fn py_filter_unclaimed_external_orders(&self) -> bool {
597        self.filter_unclaimed_external_orders
598    }
599
600    #[getter]
601    #[pyo3(name = "filter_position_reports")]
602    const fn py_filter_position_reports(&self) -> bool {
603        self.filter_position_reports
604    }
605
606    #[getter]
607    #[pyo3(name = "filtered_client_order_ids")]
608    fn py_filtered_client_order_ids(&self) -> Option<Vec<String>> {
609        self.filtered_client_order_ids.clone()
610    }
611
612    #[getter]
613    #[pyo3(name = "generate_missing_orders")]
614    const fn py_generate_missing_orders(&self) -> bool {
615        self.generate_missing_orders
616    }
617
618    #[getter]
619    #[pyo3(name = "inflight_check_interval_ms")]
620    const fn py_inflight_check_interval_ms(&self) -> u32 {
621        self.inflight_check_interval_ms
622    }
623
624    #[getter]
625    #[pyo3(name = "inflight_check_threshold_ms")]
626    const fn py_inflight_check_threshold_ms(&self) -> u32 {
627        self.inflight_check_threshold_ms
628    }
629
630    #[getter]
631    #[pyo3(name = "submission_recovery_policy")]
632    const fn py_submission_recovery_policy(&self) -> SubmissionRecoveryPolicy {
633        self.submission_recovery_policy
634    }
635
636    #[getter]
637    #[pyo3(name = "inflight_check_retries")]
638    const fn py_inflight_check_retries(&self) -> u32 {
639        self.inflight_check_retries
640    }
641
642    #[getter]
643    #[pyo3(name = "open_check_interval_secs")]
644    const fn py_open_check_interval_secs(&self) -> Option<f64> {
645        self.open_check_interval_secs
646    }
647
648    #[getter]
649    #[pyo3(name = "open_check_lookback_mins")]
650    const fn py_open_check_lookback_mins(&self) -> Option<u32> {
651        self.open_check_lookback_mins
652    }
653
654    #[getter]
655    #[pyo3(name = "open_check_threshold_ms")]
656    const fn py_open_check_threshold_ms(&self) -> u32 {
657        self.open_check_threshold_ms
658    }
659
660    #[getter]
661    #[pyo3(name = "open_check_missing_retries")]
662    const fn py_open_check_missing_retries(&self) -> u32 {
663        self.open_check_missing_retries
664    }
665
666    #[getter]
667    #[pyo3(name = "open_check_open_only")]
668    const fn py_open_check_open_only(&self) -> bool {
669        self.open_check_open_only
670    }
671
672    #[getter]
673    #[pyo3(name = "max_single_order_queries_per_cycle")]
674    const fn py_max_single_order_queries_per_cycle(&self) -> u32 {
675        self.max_single_order_queries_per_cycle
676    }
677
678    #[getter]
679    #[pyo3(name = "single_order_query_delay_ms")]
680    const fn py_single_order_query_delay_ms(&self) -> u32 {
681        self.single_order_query_delay_ms
682    }
683
684    #[getter]
685    #[pyo3(name = "position_check_interval_secs")]
686    const fn py_position_check_interval_secs(&self) -> Option<f64> {
687        self.position_check_interval_secs
688    }
689
690    #[getter]
691    #[pyo3(name = "position_check_lookback_mins")]
692    const fn py_position_check_lookback_mins(&self) -> u32 {
693        self.position_check_lookback_mins
694    }
695
696    #[getter]
697    #[pyo3(name = "position_check_threshold_ms")]
698    const fn py_position_check_threshold_ms(&self) -> u32 {
699        self.position_check_threshold_ms
700    }
701
702    #[getter]
703    #[pyo3(name = "position_check_retries")]
704    const fn py_position_check_retries(&self) -> u32 {
705        self.position_check_retries
706    }
707
708    #[getter]
709    #[pyo3(name = "purge_closed_orders_interval_mins")]
710    const fn py_purge_closed_orders_interval_mins(&self) -> Option<u32> {
711        self.purge_closed_orders_interval_mins
712    }
713
714    #[getter]
715    #[pyo3(name = "purge_closed_orders_buffer_mins")]
716    const fn py_purge_closed_orders_buffer_mins(&self) -> Option<u32> {
717        self.purge_closed_orders_buffer_mins
718    }
719
720    #[getter]
721    #[pyo3(name = "purge_closed_positions_interval_mins")]
722    const fn py_purge_closed_positions_interval_mins(&self) -> Option<u32> {
723        self.purge_closed_positions_interval_mins
724    }
725
726    #[getter]
727    #[pyo3(name = "purge_closed_positions_buffer_mins")]
728    const fn py_purge_closed_positions_buffer_mins(&self) -> Option<u32> {
729        self.purge_closed_positions_buffer_mins
730    }
731
732    #[getter]
733    #[pyo3(name = "purge_account_events_interval_mins")]
734    const fn py_purge_account_events_interval_mins(&self) -> Option<u32> {
735        self.purge_account_events_interval_mins
736    }
737
738    #[getter]
739    #[pyo3(name = "purge_account_events_lookback_mins")]
740    const fn py_purge_account_events_lookback_mins(&self) -> Option<u32> {
741        self.purge_account_events_lookback_mins
742    }
743
744    #[getter]
745    #[pyo3(name = "own_books_audit_interval_secs")]
746    const fn py_own_books_audit_interval_secs(&self) -> Option<f64> {
747        self.own_books_audit_interval_secs
748    }
749
750    #[getter]
751    #[pyo3(name = "debug")]
752    const fn py_debug(&self) -> bool {
753        self.debug
754    }
755
756    fn __repr__(&self) -> String {
757        format!("{self:?}")
758    }
759
760    fn __str__(&self) -> String {
761        format!("{self:?}")
762    }
763}
764
765#[pyo3_stub_gen::derive::gen_stub_pymethods]
766#[pymethods]
767impl RoutingConfig {
768    /// Configuration for live client message routing.
769    #[new]
770    #[pyo3(signature = (default=None, venues=None))]
771    fn py_new(default: Option<bool>, venues: Option<Vec<String>>) -> Self {
772        Self {
773            default: default.unwrap_or(false),
774            venues,
775        }
776    }
777
778    fn __repr__(&self) -> String {
779        format!("{self:?}")
780    }
781
782    fn __str__(&self) -> String {
783        format!("{self:?}")
784    }
785
786    #[getter]
787    fn default(&self) -> bool {
788        self.default
789    }
790
791    #[getter]
792    fn venues(&self) -> Option<Vec<String>> {
793        self.venues.clone()
794    }
795}
796
797#[pyo3_stub_gen::derive::gen_stub_pymethods]
798#[pymethods]
799impl InstrumentProviderConfig {
800    /// Configuration for instrument providers.
801    #[new]
802    #[allow(
803        clippy::needless_pass_by_value,
804        reason = "PyO3 #[new] requires owned params"
805    )]
806    #[pyo3(signature = (load_all=None, load_ids=None, filters=None, filter_callable=None, log_warnings=None))]
807    fn py_new(
808        load_all: Option<bool>,
809        load_ids: Option<Vec<String>>,
810        filters: Option<HashMap<String, Py<PyAny>>>,
811        filter_callable: Option<String>,
812        log_warnings: Option<bool>,
813    ) -> PyResult<Self> {
814        let default = Self::default();
815
816        let filters = match filters {
817            Some(raw) => coerce_json_config(raw)?,
818            None => HashMap::new(),
819        };
820
821        Ok(Self {
822            load_all: load_all.unwrap_or(default.load_all),
823            load_ids,
824            filters,
825            filter_callable,
826            log_warnings: log_warnings.unwrap_or(default.log_warnings),
827        })
828    }
829
830    fn __repr__(&self) -> String {
831        format!("{self:?}")
832    }
833
834    fn __str__(&self) -> String {
835        format!("{self:?}")
836    }
837
838    #[getter]
839    fn load_all(&self) -> bool {
840        self.load_all
841    }
842
843    #[getter]
844    fn load_ids(&self) -> Option<Vec<String>> {
845        self.load_ids.clone()
846    }
847
848    #[getter]
849    fn filters(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
850        let dict = pyo3::types::PyDict::new(py);
851        for (k, v) in &self.filters {
852            let py_val = json_value_to_py(py, v)?;
853            dict.set_item(k, py_val)?;
854        }
855
856        Ok(dict.into_any().unbind())
857    }
858
859    #[getter]
860    fn filter_callable(&self) -> Option<String> {
861        self.filter_callable.clone()
862    }
863
864    #[getter]
865    fn log_warnings(&self) -> bool {
866        self.log_warnings
867    }
868}
869
870#[pyo3_stub_gen::derive::gen_stub_pymethods]
871#[pymethods]
872impl DataClientConfig {
873    /// Shared configuration for data clients registered with a live node.
874    #[new]
875    #[gen_stub(override_return_type(type_repr = "typing.Self", imports = ("typing",)))]
876    #[pyo3(signature = (handle_revised_bars=None, instrument_provider=None, routing=None, **_kwargs))]
877    fn py_new(
878        handle_revised_bars: Option<bool>,
879        instrument_provider: Option<InstrumentProviderConfig>,
880        routing: Option<RoutingConfig>,
881        _kwargs: Option<&Bound<'_, PyDict>>,
882    ) -> Self {
883        Self {
884            handle_revised_bars: handle_revised_bars.unwrap_or(false),
885            instrument_provider: instrument_provider.unwrap_or_default(),
886            routing: routing.unwrap_or_default(),
887        }
888    }
889
890    #[pyo3(signature = ())]
891    fn dict(slf: &Bound<'_, Self>) -> PyResult<Py<PyDict>> {
892        Ok(slf
893            .py()
894            .import("nautilus_trader.live.config")?
895            .getattr("config_values")?
896            .call1((slf,))?
897            .cast::<PyDict>()?
898            .clone()
899            .unbind())
900    }
901
902    #[pyo3(signature = ())]
903    fn json(slf: &Bound<'_, Self>) -> PyResult<Py<PyBytes>> {
904        Ok(slf
905            .py()
906            .import("nautilus_trader.live.config")?
907            .getattr("config_json")?
908            .call1((slf,))?
909            .cast::<PyBytes>()?
910            .clone()
911            .unbind())
912    }
913
914    #[pyo3(signature = (factory=None))]
915    fn to_importable(slf: &Bound<'_, Self>, factory: Option<Py<PyAny>>) -> PyResult<Py<PyAny>> {
916        Ok(slf
917            .py()
918            .import("nautilus_trader.live.config")?
919            .getattr("config_importable")?
920            .call1((slf, factory))?
921            .unbind())
922    }
923
924    fn __repr__(&self) -> String {
925        format!("{self:?}")
926    }
927
928    fn __str__(&self) -> String {
929        format!("{self:?}")
930    }
931
932    #[getter]
933    fn handle_revised_bars(&self) -> bool {
934        self.handle_revised_bars
935    }
936
937    #[getter]
938    fn instrument_provider(&self) -> InstrumentProviderConfig {
939        self.instrument_provider.clone()
940    }
941
942    #[getter]
943    fn routing(&self) -> RoutingConfig {
944        self.routing.clone()
945    }
946}
947
948#[pyo3_stub_gen::derive::gen_stub_pymethods]
949#[pymethods]
950impl ExecutionClientConfig {
951    /// Shared configuration for execution clients registered with a live node.
952    #[new]
953    #[gen_stub(override_return_type(type_repr = "typing.Self", imports = ("typing",)))]
954    #[pyo3(signature = (instrument_provider=None, routing=None, **_kwargs))]
955    fn py_new(
956        instrument_provider: Option<InstrumentProviderConfig>,
957        routing: Option<RoutingConfig>,
958        _kwargs: Option<Bound<'_, PyDict>>,
959    ) -> Self {
960        Self {
961            instrument_provider: instrument_provider.unwrap_or_default(),
962            routing: routing.unwrap_or_default(),
963        }
964    }
965
966    #[pyo3(signature = ())]
967    fn dict(slf: &Bound<'_, Self>) -> PyResult<Py<PyDict>> {
968        Ok(slf
969            .py()
970            .import("nautilus_trader.live.config")?
971            .getattr("config_values")?
972            .call1((slf,))?
973            .cast::<PyDict>()?
974            .clone()
975            .unbind())
976    }
977
978    #[pyo3(signature = ())]
979    fn json(slf: &Bound<'_, Self>) -> PyResult<Py<PyBytes>> {
980        Ok(slf
981            .py()
982            .import("nautilus_trader.live.config")?
983            .getattr("config_json")?
984            .call1((slf,))?
985            .cast::<PyBytes>()?
986            .clone()
987            .unbind())
988    }
989
990    #[pyo3(signature = (factory=None))]
991    fn to_importable(slf: &Bound<'_, Self>, factory: Option<Py<PyAny>>) -> PyResult<Py<PyAny>> {
992        Ok(slf
993            .py()
994            .import("nautilus_trader.live.config")?
995            .getattr("config_importable")?
996            .call1((slf, factory))?
997            .unbind())
998    }
999
1000    fn __repr__(&self) -> String {
1001        format!("{self:?}")
1002    }
1003
1004    fn __str__(&self) -> String {
1005        format!("{self:?}")
1006    }
1007
1008    #[getter]
1009    fn instrument_provider(&self) -> InstrumentProviderConfig {
1010        self.instrument_provider.clone()
1011    }
1012
1013    #[getter]
1014    fn routing(&self) -> RoutingConfig {
1015        self.routing.clone()
1016    }
1017}
1018
1019#[pyo3_stub_gen::derive::gen_stub_pymethods]
1020#[pymethods]
1021impl PluginConfig {
1022    /// Configuration for one Rust-native plug-in instance loaded by a live node.
1023    #[new]
1024    #[pyo3(signature = (path, type_name, config=None, sha256=None))]
1025    fn py_new(
1026        path: String,
1027        type_name: String,
1028        config: Option<HashMap<String, Py<PyAny>>>,
1029        sha256: Option<String>,
1030    ) -> PyResult<Self> {
1031        let config = match config {
1032            Some(config) => coerce_json_config(config)?,
1033            None => HashMap::new(),
1034        };
1035
1036        Ok(Self {
1037            path,
1038            type_name,
1039            config,
1040            sha256,
1041        })
1042    }
1043
1044    #[getter]
1045    fn path(&self) -> &str {
1046        &self.path
1047    }
1048
1049    #[getter]
1050    fn type_name(&self) -> &str {
1051        &self.type_name
1052    }
1053
1054    #[getter]
1055    fn config(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
1056        let dict = PyDict::new(py);
1057        for (key, value) in &self.config {
1058            dict.set_item(key, json_value_to_py(py, value)?)?;
1059        }
1060
1061        Ok(dict.unbind())
1062    }
1063
1064    #[getter]
1065    fn sha256(&self) -> Option<&str> {
1066        self.sha256.as_deref()
1067    }
1068}
1069
1070#[pyo3_stub_gen::derive::gen_stub_pymethods]
1071#[pymethods]
1072impl QueueMonitorConfig {
1073    /// Configuration for runner queue pressure monitoring.
1074    #[new]
1075    const fn py_new(
1076        queue_depth_trigger: usize,
1077        queue_depth_clear: usize,
1078        mean_dispatch_ns_trigger: u64,
1079        mean_dispatch_ns_clear: u64,
1080    ) -> Self {
1081        Self {
1082            queue_depth_trigger,
1083            queue_depth_clear,
1084            mean_dispatch_ns_trigger,
1085            mean_dispatch_ns_clear,
1086        }
1087    }
1088
1089    fn __repr__(&self) -> String {
1090        format!("{self:?}")
1091    }
1092
1093    fn __str__(&self) -> String {
1094        format!("{self:?}")
1095    }
1096
1097    #[getter]
1098    const fn queue_depth_trigger(&self) -> usize {
1099        self.queue_depth_trigger
1100    }
1101
1102    #[getter]
1103    const fn queue_depth_clear(&self) -> usize {
1104        self.queue_depth_clear
1105    }
1106
1107    #[getter]
1108    const fn mean_dispatch_ns_trigger(&self) -> u64 {
1109        self.mean_dispatch_ns_trigger
1110    }
1111
1112    #[getter]
1113    const fn mean_dispatch_ns_clear(&self) -> u64 {
1114        self.mean_dispatch_ns_clear
1115    }
1116}
1117
1118#[pyo3_stub_gen::derive::gen_stub_pymethods]
1119#[pymethods]
1120impl LiveNodeConfig {
1121    /// Configuration for live Nautilus system nodes.
1122    #[new]
1123    #[expect(clippy::too_many_arguments)]
1124    #[pyo3(signature = (environment=None, trader_id=None, load_state=None, save_state=None, shutdown_on_error=None, logging=None, instance_id=None, timeout_connection_secs=None, timeout_reconciliation_secs=None, timeout_portfolio_secs=None, timeout_disconnection_secs=None, delay_post_stop_secs=None, timeout_shutdown_secs=None, cache=None, msgbus=None, portfolio=None, queue_monitor=None, loop_debug=None, data_engine=None, risk_engine=None, exec_engine=None, controller=None, plugins=None, streaming=None, catalogs=None, *, data_clients=None, exec_clients=None))]
1125    fn py_new(
1126        environment: Option<Environment>,
1127        trader_id: Option<TraderId>,
1128        load_state: Option<bool>,
1129        save_state: Option<bool>,
1130        shutdown_on_error: Option<bool>,
1131        logging: Option<LoggerConfig>,
1132        instance_id: Option<UUID4>,
1133        timeout_connection_secs: Option<f64>,
1134        timeout_reconciliation_secs: Option<f64>,
1135        timeout_portfolio_secs: Option<f64>,
1136        timeout_disconnection_secs: Option<f64>,
1137        delay_post_stop_secs: Option<f64>,
1138        timeout_shutdown_secs: Option<f64>,
1139        cache: Option<CacheConfig>,
1140        msgbus: Option<MessageBusConfig>,
1141        portfolio: Option<PortfolioConfig>,
1142        queue_monitor: Option<QueueMonitorConfig>,
1143        loop_debug: Option<bool>,
1144        data_engine: Option<LiveDataEngineConfig>,
1145        risk_engine: Option<LiveRiskEngineConfig>,
1146        exec_engine: Option<LiveExecutionEngineConfig>,
1147        controller: Option<ImportableControllerConfig>,
1148        plugins: Option<Vec<PluginConfig>>,
1149        streaming: Option<StreamingConfig>,
1150        catalogs: Option<Vec<DataCatalogConfig>>,
1151        data_clients: Option<Bound<'_, PyDict>>,
1152        exec_clients: Option<Bound<'_, PyDict>>,
1153    ) -> PyResult<Self> {
1154        let _ = (data_clients, exec_clients);
1155        let default = Self::default();
1156
1157        let to_duration = |value: f64, name: &str| -> PyResult<Duration> {
1158            duration_from_secs_f64(name, value).map_err(config_error_to_pyvalue_err)
1159        };
1160
1161        Ok(Self {
1162            environment: environment.unwrap_or(default.environment),
1163            trader_id: trader_id.unwrap_or(default.trader_id),
1164            load_state: load_state.unwrap_or(default.load_state),
1165            save_state: save_state.unwrap_or(default.save_state),
1166            shutdown_on_error: shutdown_on_error.unwrap_or(default.shutdown_on_error),
1167            logging: logging.unwrap_or(default.logging),
1168            instance_id,
1169            timeout_connection: to_duration(
1170                timeout_connection_secs.unwrap_or(default.timeout_connection.as_secs_f64()),
1171                "timeout_connection_secs",
1172            )?,
1173            timeout_reconciliation: to_duration(
1174                timeout_reconciliation_secs.unwrap_or(default.timeout_reconciliation.as_secs_f64()),
1175                "timeout_reconciliation_secs",
1176            )?,
1177            timeout_portfolio: to_duration(
1178                timeout_portfolio_secs.unwrap_or(default.timeout_portfolio.as_secs_f64()),
1179                "timeout_portfolio_secs",
1180            )?,
1181            timeout_disconnection: to_duration(
1182                timeout_disconnection_secs.unwrap_or(default.timeout_disconnection.as_secs_f64()),
1183                "timeout_disconnection_secs",
1184            )?,
1185            delay_post_stop: to_duration(
1186                delay_post_stop_secs.unwrap_or(default.delay_post_stop.as_secs_f64()),
1187                "delay_post_stop_secs",
1188            )?,
1189            timeout_shutdown: to_duration(
1190                timeout_shutdown_secs.unwrap_or(default.timeout_shutdown.as_secs_f64()),
1191                "timeout_shutdown_secs",
1192            )?,
1193            cache,
1194            msgbus,
1195            portfolio,
1196            emulator: None,
1197            streaming,
1198            catalogs: catalogs.unwrap_or_default(),
1199            queue_monitor,
1200            event_store: None,
1201            loop_debug: loop_debug.unwrap_or(false),
1202            data_engine: data_engine.unwrap_or_default(),
1203            risk_engine: risk_engine.unwrap_or_default(),
1204            exec_engine: exec_engine.unwrap_or_default(),
1205            data_clients: HashMap::new(),
1206            exec_clients: HashMap::new(),
1207            controller,
1208            plugins: plugins.unwrap_or_default(),
1209        })
1210    }
1211
1212    #[gen_stub(skip)]
1213    #[pyo3(signature = (*_args, **kwargs))]
1214    fn __init__(
1215        slf: &Bound<'_, Self>,
1216        _args: &Bound<'_, PyTuple>,
1217        kwargs: Option<&Bound<'_, PyDict>>,
1218    ) -> PyResult<()> {
1219        for name in ["data_clients", "exec_clients"] {
1220            let values = kwargs
1221                .map(|kwargs| kwargs.get_item(name))
1222                .transpose()?
1223                .flatten();
1224
1225            let values = match values {
1226                Some(values) if !values.is_none() => values.cast::<PyDict>()?.copy()?,
1227                _ => PyDict::new(slf.py()),
1228            };
1229
1230            slf.setattr(format!("_{name}"), values)?;
1231        }
1232
1233        Ok(())
1234    }
1235
1236    #[getter]
1237    fn data_clients(slf: &Bound<'_, Self>) -> PyResult<Py<PyDict>> {
1238        let values = slf.getattr("__dict__")?;
1239        let values = values.cast::<PyDict>()?;
1240        match values.get_item("_data_clients")? {
1241            Some(values) => Ok(values.cast::<PyDict>()?.copy()?.unbind()),
1242            None => Ok(slf
1243                .borrow()
1244                .data_clients
1245                .clone()
1246                .into_pyobject(slf.py())?
1247                .unbind()),
1248        }
1249    }
1250
1251    #[getter]
1252    fn exec_clients(slf: &Bound<'_, Self>) -> PyResult<Py<PyDict>> {
1253        let values = slf.getattr("__dict__")?;
1254        let values = values.cast::<PyDict>()?;
1255        match values.get_item("_exec_clients")? {
1256            Some(values) => Ok(values.cast::<PyDict>()?.copy()?.unbind()),
1257            None => Ok(slf
1258                .borrow()
1259                .exec_clients
1260                .clone()
1261                .into_pyobject(slf.py())?
1262                .unbind()),
1263        }
1264    }
1265
1266    fn __repr__(&self) -> String {
1267        format!("{self:?}")
1268    }
1269
1270    fn __str__(&self) -> String {
1271        format!("{self:?}")
1272    }
1273
1274    #[getter]
1275    #[pyo3(name = "streaming")]
1276    fn py_streaming(&self) -> Option<StreamingConfig> {
1277        self.streaming.clone()
1278    }
1279
1280    #[getter]
1281    #[pyo3(name = "catalogs")]
1282    fn py_catalogs(&self) -> Vec<DataCatalogConfig> {
1283        self.catalogs.clone()
1284    }
1285
1286    #[getter]
1287    fn environment(&self) -> Environment {
1288        self.environment
1289    }
1290
1291    #[getter]
1292    fn trader_id(&self) -> TraderId {
1293        self.trader_id
1294    }
1295
1296    #[getter]
1297    fn load_state(&self) -> bool {
1298        self.load_state
1299    }
1300
1301    #[getter]
1302    fn save_state(&self) -> bool {
1303        self.save_state
1304    }
1305
1306    #[getter]
1307    fn shutdown_on_error(&self) -> bool {
1308        self.shutdown_on_error
1309    }
1310
1311    #[getter]
1312    fn timeout_connection_secs(&self) -> f64 {
1313        self.timeout_connection.as_secs_f64()
1314    }
1315
1316    #[getter]
1317    fn timeout_reconciliation_secs(&self) -> f64 {
1318        self.timeout_reconciliation.as_secs_f64()
1319    }
1320
1321    #[getter]
1322    fn timeout_portfolio_secs(&self) -> f64 {
1323        self.timeout_portfolio.as_secs_f64()
1324    }
1325
1326    #[getter]
1327    fn timeout_disconnection_secs(&self) -> f64 {
1328        self.timeout_disconnection.as_secs_f64()
1329    }
1330
1331    #[getter]
1332    fn delay_post_stop_secs(&self) -> f64 {
1333        self.delay_post_stop.as_secs_f64()
1334    }
1335
1336    #[getter]
1337    fn timeout_shutdown_secs(&self) -> f64 {
1338        self.timeout_shutdown.as_secs_f64()
1339    }
1340
1341    #[getter]
1342    #[pyo3(name = "logging")]
1343    fn py_logging(&self) -> LoggerConfig {
1344        self.logging.clone()
1345    }
1346
1347    #[getter]
1348    #[pyo3(name = "instance_id")]
1349    const fn py_instance_id(&self) -> Option<UUID4> {
1350        self.instance_id
1351    }
1352
1353    #[getter]
1354    #[pyo3(name = "cache")]
1355    fn py_cache(&self) -> Option<CacheConfig> {
1356        self.cache.clone()
1357    }
1358
1359    #[getter]
1360    #[pyo3(name = "msgbus")]
1361    fn py_msgbus(&self) -> Option<MessageBusConfig> {
1362        self.msgbus.clone()
1363    }
1364
1365    #[getter]
1366    #[pyo3(name = "portfolio")]
1367    fn py_portfolio(&self) -> Option<PortfolioConfig> {
1368        self.portfolio
1369    }
1370
1371    #[getter]
1372    #[pyo3(name = "queue_monitor")]
1373    fn py_queue_monitor(&self) -> Option<QueueMonitorConfig> {
1374        self.queue_monitor.clone()
1375    }
1376
1377    #[getter]
1378    #[pyo3(name = "loop_debug")]
1379    const fn py_loop_debug(&self) -> bool {
1380        self.loop_debug
1381    }
1382
1383    #[getter]
1384    #[pyo3(name = "data_engine")]
1385    fn py_data_engine(&self) -> LiveDataEngineConfig {
1386        self.data_engine.clone()
1387    }
1388
1389    #[getter]
1390    #[pyo3(name = "risk_engine")]
1391    fn py_risk_engine(&self) -> LiveRiskEngineConfig {
1392        self.risk_engine.clone()
1393    }
1394
1395    #[getter]
1396    #[pyo3(name = "exec_engine")]
1397    fn py_exec_engine(&self) -> LiveExecutionEngineConfig {
1398        self.exec_engine.clone()
1399    }
1400
1401    #[getter]
1402    fn plugins(&self) -> Vec<PluginConfig> {
1403        self.plugins.clone()
1404    }
1405
1406    #[getter]
1407    fn controller(&self) -> Option<ImportableControllerConfig> {
1408        self.controller.clone()
1409    }
1410}
1411
1412#[cfg(test)]
1413mod tests {
1414    use rstest::rstest;
1415
1416    use super::*;
1417
1418    #[rstest]
1419    fn json_value_to_py_preserves_unsigned_integer() {
1420        Python::initialize();
1421        Python::attach(|py| {
1422            let expected = u64::MAX;
1423            let value = serde_json::Value::from(expected);
1424            let result = json_value_to_py(py, &value).expect("JSON value must convert to Python");
1425
1426            assert_eq!(
1427                result
1428                    .extract::<u64>(py)
1429                    .expect("Python value must remain an unsigned integer"),
1430                expected
1431            );
1432        });
1433    }
1434}