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