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
16use std::{collections::HashMap, time::Duration};
17
18use nautilus_common::{
19    cache::CacheConfig, enums::Environment, logging::logger::LoggerConfig,
20    msgbus::MessageBusConfig, python::config_error_to_pyvalue_err,
21};
22use nautilus_core::{UUID4, python::to_pyvalue_err};
23use nautilus_model::{
24    enums::BarIntervalType,
25    identifiers::{ClientId, TraderId},
26};
27use nautilus_portfolio::config::PortfolioConfig;
28use pyo3::{
29    IntoPyObject, Py, PyAny, PyResult, Python, pymethods,
30    types::{PyAnyMethods, PyDict, PyDictMethods},
31};
32
33use crate::config::{
34    InstrumentProviderConfig, LiveDataClientConfig, LiveDataEngineConfig, LiveExecClientConfig,
35    LiveExecEngineConfig, LiveNodeConfig, LiveRiskEngineConfig, PluginConfig, RoutingConfig,
36    duration_from_secs_f64, parse_rate_limit, validate_client_order_id_strings,
37    validate_instrument_id_strings, validate_max_notional_per_order,
38    validate_non_negative_finite_f64,
39};
40
41// Coerces a PyO3 input into `BarIntervalType`, accepting both the enum (modern Rust
42// surface) and the legacy Python v1 string form (`"left-open"` / `"right-open"`).
43fn coerce_bar_interval_type(value: &Py<PyAny>) -> PyResult<BarIntervalType> {
44    Python::attach(|py| {
45        let bound = value.bind(py);
46        if let Ok(variant) = bound.extract::<BarIntervalType>() {
47            return Ok(variant);
48        }
49
50        let raw = bound.extract::<String>().map_err(|_| {
51            to_pyvalue_err("`time_bars_interval_type` must be a string or BarIntervalType")
52        })?;
53
54        match raw.to_ascii_uppercase().replace('-', "_").as_str() {
55            "LEFT_OPEN" => Ok(BarIntervalType::LeftOpen),
56            "RIGHT_OPEN" => Ok(BarIntervalType::RightOpen),
57            _ => Err(to_pyvalue_err(format!(
58                "invalid `time_bars_interval_type`: {raw:?} (expected 'left-open' or 'right-open')"
59            ))),
60        }
61    })
62}
63
64/// Converts a Python value into a [`serde_json::Value`].
65fn py_to_json_value(bound: &pyo3::Bound<'_, PyAny>) -> PyResult<serde_json::Value> {
66    // Check bool before int since Python `bool` is a subclass of `int`
67    if let Ok(b) = bound.extract::<bool>() {
68        Ok(serde_json::Value::Bool(b))
69    } else if let Ok(s) = bound.extract::<String>() {
70        Ok(serde_json::Value::String(s))
71    } else if let Ok(i) = bound.extract::<i64>() {
72        Ok(serde_json::Value::Number(serde_json::Number::from(i)))
73    } else if let Ok(f) = bound.extract::<f64>() {
74        Ok(serde_json::Number::from_f64(f)
75            .map_or(serde_json::Value::Null, serde_json::Value::Number))
76    } else if let Ok(dict) = bound.cast::<PyDict>() {
77        let mut obj = serde_json::Map::with_capacity(dict.len());
78        for (key, value) in dict.iter() {
79            obj.insert(key.extract::<String>()?, py_to_json_value(&value)?);
80        }
81        Ok(serde_json::Value::Object(obj))
82    } else if let Ok(items) = bound.extract::<Vec<Py<PyAny>>>() {
83        // Handle list/tuple/set
84        let py = bound.py();
85        let arr: Vec<serde_json::Value> = items
86            .iter()
87            .map(|item| py_to_json_value(item.bind(py)))
88            .collect::<PyResult<_>>()?;
89        Ok(serde_json::Value::Array(arr))
90    } else {
91        // Fall back to string representation
92        let s: String = bound.str()?.extract()?;
93        Ok(serde_json::Value::String(s))
94    }
95}
96
97/// Converts a [`serde_json::Value`] into a Python object.
98fn json_value_to_py(py: Python<'_>, value: &serde_json::Value) -> PyResult<Py<PyAny>> {
99    match value {
100        serde_json::Value::Null => Ok(py.None()),
101        serde_json::Value::Bool(b) => Ok((*b).into_pyobject(py)?.to_owned().into_any().unbind()),
102        serde_json::Value::Number(n) => {
103            if let Some(i) = n.as_i64() {
104                Ok(i.into_pyobject(py)?.into_any().unbind())
105            } else if let Some(f) = n.as_f64() {
106                Ok(f.into_pyobject(py)?.into_any().unbind())
107            } else {
108                Ok(n.to_string().into_pyobject(py)?.into_any().unbind())
109            }
110        }
111        serde_json::Value::String(s) => Ok(s.into_pyobject(py)?.into_any().unbind()),
112        serde_json::Value::Array(arr) => {
113            let items: Vec<Py<PyAny>> = arr
114                .iter()
115                .map(|v| json_value_to_py(py, v))
116                .collect::<PyResult<_>>()?;
117            Ok(pyo3::types::PyList::new(py, items)?.into_any().unbind())
118        }
119        serde_json::Value::Object(obj) => {
120            let dict = pyo3::types::PyDict::new(py);
121            for (k, v) in obj {
122                dict.set_item(k, json_value_to_py(py, v)?)?;
123            }
124            Ok(dict.into_any().unbind())
125        }
126    }
127}
128
129/// Converts Python mapping values into JSON values.
130pub(crate) fn coerce_json_config(
131    raw: HashMap<String, Py<PyAny>>,
132) -> PyResult<HashMap<String, serde_json::Value>> {
133    Python::attach(|py| -> PyResult<HashMap<String, serde_json::Value>> {
134        let mut result = HashMap::with_capacity(raw.len());
135        for (key, value) in raw {
136            let json_value = py_to_json_value(value.bind(py))?;
137            result.insert(key, json_value);
138        }
139        Ok(result)
140    })
141}
142
143// Normalizes a Python `max_notional_per_order` dict (values can be `int`, `float`,
144// `str`, or `Decimal`, matching the legacy Python v1 config contract) into the
145// string-keyed map stored on `LiveRiskEngineConfig`.
146fn coerce_max_notional_per_order(
147    raw: HashMap<String, Py<PyAny>>,
148) -> PyResult<HashMap<String, String>> {
149    Python::attach(|py| -> PyResult<HashMap<String, String>> {
150        let mut result = HashMap::with_capacity(raw.len());
151        for (instrument_id, value) in raw {
152            let value_str: String = value.bind(py).str()?.extract()?;
153            result.insert(instrument_id, value_str);
154        }
155        Ok(result)
156    })
157}
158
159#[pyo3_stub_gen::derive::gen_stub_pymethods]
160#[pymethods]
161impl LiveDataEngineConfig {
162    /// Configuration for live data engines.
163    #[new]
164    #[expect(clippy::too_many_arguments)]
165    #[allow(
166        clippy::needless_pass_by_value,
167        reason = "PyO3 #[new] requires owned params"
168    )]
169    #[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))]
170    fn py_new(
171        time_bars_build_with_no_updates: Option<bool>,
172        time_bars_timestamp_on_close: Option<bool>,
173        time_bars_skip_first_non_full_bar: Option<bool>,
174        time_bars_interval_type: Option<Py<PyAny>>,
175        time_bars_build_delay: Option<u64>,
176        time_bars_origin_offset: Option<HashMap<String, u64>>,
177        validate_data_sequence: Option<bool>,
178        buffer_deltas: Option<bool>,
179        emit_quotes_from_book: Option<bool>,
180        emit_quotes_from_book_depths: Option<bool>,
181        external_clients: Option<Vec<ClientId>>,
182        debug: Option<bool>,
183    ) -> PyResult<Self> {
184        let default = Self::default();
185        let time_bars_interval_type = match time_bars_interval_type {
186            Some(ref obj) => coerce_bar_interval_type(obj)?,
187            None => default.time_bars_interval_type,
188        };
189        Ok(Self {
190            time_bars_build_with_no_updates: time_bars_build_with_no_updates
191                .unwrap_or(default.time_bars_build_with_no_updates),
192            time_bars_timestamp_on_close: time_bars_timestamp_on_close
193                .unwrap_or(default.time_bars_timestamp_on_close),
194            time_bars_skip_first_non_full_bar: time_bars_skip_first_non_full_bar
195                .unwrap_or(default.time_bars_skip_first_non_full_bar),
196            time_bars_interval_type,
197            time_bars_build_delay: time_bars_build_delay.unwrap_or(default.time_bars_build_delay),
198            time_bars_origin_offset: time_bars_origin_offset.unwrap_or_default(),
199            validate_data_sequence: validate_data_sequence
200                .unwrap_or(default.validate_data_sequence),
201            buffer_deltas: buffer_deltas.unwrap_or(default.buffer_deltas),
202            emit_quotes_from_book: emit_quotes_from_book.unwrap_or(default.emit_quotes_from_book),
203            emit_quotes_from_book_depths: emit_quotes_from_book_depths
204                .unwrap_or(default.emit_quotes_from_book_depths),
205            external_clients,
206            debug: debug.unwrap_or(default.debug),
207            qsize: default.qsize,
208        })
209    }
210
211    fn __repr__(&self) -> String {
212        format!("{self:?}")
213    }
214
215    fn __str__(&self) -> String {
216        format!("{self:?}")
217    }
218}
219
220#[pyo3_stub_gen::derive::gen_stub_pymethods]
221#[pymethods]
222impl LiveRiskEngineConfig {
223    /// Configuration for live risk engines.
224    #[new]
225    #[pyo3(signature = (bypass=None, max_order_submit_rate=None, max_order_modify_rate=None, max_notional_per_order=None, debug=None))]
226    fn py_new(
227        bypass: Option<bool>,
228        max_order_submit_rate: Option<String>,
229        max_order_modify_rate: Option<String>,
230        max_notional_per_order: Option<HashMap<String, Py<PyAny>>>,
231        debug: Option<bool>,
232    ) -> PyResult<Self> {
233        let default = Self::default();
234        let max_order_submit_rate =
235            max_order_submit_rate.unwrap_or_else(|| default.max_order_submit_rate.clone());
236        let max_order_modify_rate =
237            max_order_modify_rate.unwrap_or_else(|| default.max_order_modify_rate.clone());
238        let max_notional_per_order = match max_notional_per_order {
239            Some(raw) => coerce_max_notional_per_order(raw)?,
240            None => HashMap::new(),
241        };
242
243        parse_rate_limit(
244            "LiveRiskEngineConfig.max_order_submit_rate",
245            &max_order_submit_rate,
246        )
247        .map_err(config_error_to_pyvalue_err)?;
248        parse_rate_limit(
249            "LiveRiskEngineConfig.max_order_modify_rate",
250            &max_order_modify_rate,
251        )
252        .map_err(config_error_to_pyvalue_err)?;
253        validate_max_notional_per_order(
254            "LiveRiskEngineConfig.max_notional_per_order",
255            &max_notional_per_order,
256        )
257        .map_err(config_error_to_pyvalue_err)?;
258
259        Ok(Self {
260            bypass: bypass.unwrap_or(default.bypass),
261            max_order_submit_rate,
262            max_order_modify_rate,
263            max_notional_per_order,
264            debug: debug.unwrap_or(default.debug),
265            qsize: default.qsize,
266        })
267    }
268
269    fn __repr__(&self) -> String {
270        format!("{self:?}")
271    }
272
273    fn __str__(&self) -> String {
274        format!("{self:?}")
275    }
276}
277
278#[pyo3_stub_gen::derive::gen_stub_pymethods]
279#[pymethods]
280impl LiveExecEngineConfig {
281    /// Configuration for live execution engines.
282    #[new]
283    #[expect(clippy::too_many_arguments)]
284    #[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))]
285    fn py_new(
286        load_cache: Option<bool>,
287        manage_own_order_books: Option<bool>,
288        snapshot_positions_interval_secs: Option<f64>,
289        external_clients: Option<Vec<ClientId>>,
290        allow_overfills: Option<bool>,
291        reconciliation: Option<bool>,
292        reconciliation_startup_delay_secs: Option<f64>,
293        reconciliation_lookback_mins: Option<u32>,
294        reconciliation_instrument_ids: Option<Vec<String>>,
295        filter_unclaimed_external_orders: Option<bool>,
296        filter_position_reports: Option<bool>,
297        filtered_client_order_ids: Option<Vec<String>>,
298        generate_missing_orders: Option<bool>,
299        inflight_check_interval_ms: Option<u32>,
300        inflight_check_threshold_ms: Option<u32>,
301        inflight_check_retries: Option<u32>,
302        open_check_interval_secs: Option<f64>,
303        open_check_lookback_mins: Option<u32>,
304        open_check_threshold_ms: Option<u32>,
305        open_check_missing_retries: Option<u32>,
306        open_check_open_only: Option<bool>,
307        max_single_order_queries_per_cycle: Option<u32>,
308        single_order_query_delay_ms: Option<u32>,
309        position_check_interval_secs: Option<f64>,
310        position_check_lookback_mins: Option<u32>,
311        position_check_threshold_ms: Option<u32>,
312        position_check_retries: Option<u32>,
313        purge_closed_orders_interval_mins: Option<u32>,
314        purge_closed_orders_buffer_mins: Option<u32>,
315        purge_closed_positions_interval_mins: Option<u32>,
316        purge_closed_positions_buffer_mins: Option<u32>,
317        purge_account_events_interval_mins: Option<u32>,
318        purge_account_events_lookback_mins: Option<u32>,
319        own_books_audit_interval_secs: Option<f64>,
320        debug: Option<bool>,
321    ) -> PyResult<Self> {
322        let default = Self::default();
323
324        if let Some(delay) = reconciliation_startup_delay_secs {
325            validate_non_negative_finite_f64(
326                "LiveExecEngineConfig.reconciliation_startup_delay_secs",
327                delay,
328            )
329            .map_err(config_error_to_pyvalue_err)?;
330        }
331
332        if let Some(ids) = reconciliation_instrument_ids.as_ref() {
333            validate_instrument_id_strings(
334                "LiveExecEngineConfig.reconciliation_instrument_ids",
335                ids,
336            )
337            .map_err(config_error_to_pyvalue_err)?;
338        }
339
340        if let Some(ids) = filtered_client_order_ids.as_ref() {
341            validate_client_order_id_strings("LiveExecEngineConfig.filtered_client_order_ids", ids)
342                .map_err(config_error_to_pyvalue_err)?;
343        }
344
345        Ok(Self {
346            load_cache: load_cache.unwrap_or(default.load_cache),
347            manage_own_order_books: manage_own_order_books
348                .unwrap_or(default.manage_own_order_books),
349            snapshot_orders: default.snapshot_orders,
350            snapshot_positions: default.snapshot_positions,
351            snapshot_positions_interval_secs,
352            external_clients,
353            allow_overfills: allow_overfills.unwrap_or(default.allow_overfills),
354            reconciliation: reconciliation.unwrap_or(default.reconciliation),
355            reconciliation_startup_delay_secs: reconciliation_startup_delay_secs
356                .unwrap_or(default.reconciliation_startup_delay_secs),
357            reconciliation_lookback_mins,
358            reconciliation_instrument_ids,
359            filter_unclaimed_external_orders: filter_unclaimed_external_orders
360                .unwrap_or(default.filter_unclaimed_external_orders),
361            filter_position_reports: filter_position_reports
362                .unwrap_or(default.filter_position_reports),
363            filtered_client_order_ids,
364            generate_missing_orders: generate_missing_orders
365                .unwrap_or(default.generate_missing_orders),
366            inflight_check_interval_ms: inflight_check_interval_ms
367                .unwrap_or(default.inflight_check_interval_ms),
368            inflight_check_threshold_ms: inflight_check_threshold_ms
369                .unwrap_or(default.inflight_check_threshold_ms),
370            inflight_check_retries: inflight_check_retries
371                .unwrap_or(default.inflight_check_retries),
372            open_check_interval_secs,
373            open_check_lookback_mins: open_check_lookback_mins.or(default.open_check_lookback_mins),
374            open_check_threshold_ms: open_check_threshold_ms
375                .unwrap_or(default.open_check_threshold_ms),
376            open_check_missing_retries: open_check_missing_retries
377                .unwrap_or(default.open_check_missing_retries),
378            open_check_open_only: open_check_open_only.unwrap_or(default.open_check_open_only),
379            max_single_order_queries_per_cycle: max_single_order_queries_per_cycle
380                .unwrap_or(default.max_single_order_queries_per_cycle),
381            single_order_query_delay_ms: single_order_query_delay_ms
382                .unwrap_or(default.single_order_query_delay_ms),
383            position_check_interval_secs,
384            position_check_lookback_mins: position_check_lookback_mins
385                .unwrap_or(default.position_check_lookback_mins),
386            position_check_threshold_ms: position_check_threshold_ms
387                .unwrap_or(default.position_check_threshold_ms),
388            position_check_retries: position_check_retries
389                .unwrap_or(default.position_check_retries),
390            purge_closed_orders_interval_mins,
391            purge_closed_orders_buffer_mins,
392            purge_closed_positions_interval_mins,
393            purge_closed_positions_buffer_mins,
394            purge_account_events_interval_mins,
395            purge_account_events_lookback_mins,
396            purge_from_database: default.purge_from_database,
397            debug: debug.unwrap_or(default.debug),
398            own_books_audit_interval_secs,
399            qsize: default.qsize,
400        })
401    }
402
403    fn __repr__(&self) -> String {
404        format!("{self:?}")
405    }
406
407    fn __str__(&self) -> String {
408        format!("{self:?}")
409    }
410}
411
412#[pyo3_stub_gen::derive::gen_stub_pymethods]
413#[pymethods]
414impl RoutingConfig {
415    /// Configuration for live client message routing.
416    #[new]
417    #[pyo3(signature = (default=None, venues=None))]
418    fn py_new(default: Option<bool>, venues: Option<Vec<String>>) -> Self {
419        Self {
420            default: default.unwrap_or(false),
421            venues,
422        }
423    }
424
425    fn __repr__(&self) -> String {
426        format!("{self:?}")
427    }
428
429    fn __str__(&self) -> String {
430        format!("{self:?}")
431    }
432
433    #[getter]
434    fn default(&self) -> bool {
435        self.default
436    }
437
438    #[getter]
439    fn venues(&self) -> Option<Vec<String>> {
440        self.venues.clone()
441    }
442}
443
444#[pyo3_stub_gen::derive::gen_stub_pymethods]
445#[pymethods]
446impl InstrumentProviderConfig {
447    /// Configuration for instrument providers.
448    #[new]
449    #[allow(
450        clippy::needless_pass_by_value,
451        reason = "PyO3 #[new] requires owned params"
452    )]
453    #[pyo3(signature = (load_all=None, load_ids=None, filters=None, filter_callable=None, log_warnings=None))]
454    fn py_new(
455        load_all: Option<bool>,
456        load_ids: Option<Vec<String>>,
457        filters: Option<HashMap<String, Py<PyAny>>>,
458        filter_callable: Option<String>,
459        log_warnings: Option<bool>,
460    ) -> PyResult<Self> {
461        let default = Self::default();
462        let filters = match filters {
463            Some(raw) => coerce_json_config(raw)?,
464            None => HashMap::new(),
465        };
466        Ok(Self {
467            load_all: load_all.unwrap_or(default.load_all),
468            load_ids,
469            filters,
470            filter_callable,
471            log_warnings: log_warnings.unwrap_or(default.log_warnings),
472        })
473    }
474
475    fn __repr__(&self) -> String {
476        format!("{self:?}")
477    }
478
479    fn __str__(&self) -> String {
480        format!("{self:?}")
481    }
482
483    #[getter]
484    fn load_all(&self) -> bool {
485        self.load_all
486    }
487
488    #[getter]
489    fn load_ids(&self) -> Option<Vec<String>> {
490        self.load_ids.clone()
491    }
492
493    #[getter]
494    fn filters(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
495        let dict = pyo3::types::PyDict::new(py);
496        for (k, v) in &self.filters {
497            let py_val = json_value_to_py(py, v)?;
498            dict.set_item(k, py_val)?;
499        }
500        Ok(dict.into_any().unbind())
501    }
502
503    #[getter]
504    fn filter_callable(&self) -> Option<String> {
505        self.filter_callable.clone()
506    }
507
508    #[getter]
509    fn log_warnings(&self) -> bool {
510        self.log_warnings
511    }
512}
513
514#[pyo3_stub_gen::derive::gen_stub_pymethods]
515#[pymethods]
516impl LiveDataClientConfig {
517    /// Configuration for live data clients.
518    #[new]
519    #[pyo3(signature = (handle_revised_bars=None, instrument_provider=None, routing=None))]
520    fn py_new(
521        handle_revised_bars: Option<bool>,
522        instrument_provider: Option<InstrumentProviderConfig>,
523        routing: Option<RoutingConfig>,
524    ) -> Self {
525        Self {
526            handle_revised_bars: handle_revised_bars.unwrap_or(false),
527            instrument_provider: instrument_provider.unwrap_or_default(),
528            routing: routing.unwrap_or_default(),
529        }
530    }
531
532    fn __repr__(&self) -> String {
533        format!("{self:?}")
534    }
535
536    fn __str__(&self) -> String {
537        format!("{self:?}")
538    }
539
540    #[getter]
541    fn handle_revised_bars(&self) -> bool {
542        self.handle_revised_bars
543    }
544
545    #[getter]
546    fn instrument_provider(&self) -> InstrumentProviderConfig {
547        self.instrument_provider.clone()
548    }
549
550    #[getter]
551    fn routing(&self) -> RoutingConfig {
552        self.routing.clone()
553    }
554}
555
556#[pyo3_stub_gen::derive::gen_stub_pymethods]
557#[pymethods]
558impl LiveExecClientConfig {
559    /// Configuration for live execution clients.
560    #[new]
561    #[pyo3(signature = (instrument_provider=None, routing=None))]
562    fn py_new(
563        instrument_provider: Option<InstrumentProviderConfig>,
564        routing: Option<RoutingConfig>,
565    ) -> Self {
566        Self {
567            instrument_provider: instrument_provider.unwrap_or_default(),
568            routing: routing.unwrap_or_default(),
569        }
570    }
571
572    fn __repr__(&self) -> String {
573        format!("{self:?}")
574    }
575
576    fn __str__(&self) -> String {
577        format!("{self:?}")
578    }
579
580    #[getter]
581    fn instrument_provider(&self) -> InstrumentProviderConfig {
582        self.instrument_provider.clone()
583    }
584
585    #[getter]
586    fn routing(&self) -> RoutingConfig {
587        self.routing.clone()
588    }
589}
590
591#[pyo3_stub_gen::derive::gen_stub_pymethods]
592#[pymethods]
593impl PluginConfig {
594    /// Configuration for one Rust-native plug-in instance loaded by a live node.
595    #[new]
596    #[pyo3(signature = (path, type_name, config=None, sha256=None))]
597    fn py_new(
598        path: String,
599        type_name: String,
600        config: Option<HashMap<String, Py<PyAny>>>,
601        sha256: Option<String>,
602    ) -> PyResult<Self> {
603        let config = match config {
604            Some(config) => coerce_json_config(config)?,
605            None => HashMap::new(),
606        };
607
608        Ok(Self {
609            path,
610            type_name,
611            config,
612            sha256,
613        })
614    }
615
616    #[getter]
617    fn path(&self) -> &str {
618        &self.path
619    }
620
621    #[getter]
622    fn type_name(&self) -> &str {
623        &self.type_name
624    }
625
626    #[getter]
627    fn config(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
628        let dict = PyDict::new(py);
629        for (key, value) in &self.config {
630            dict.set_item(key, json_value_to_py(py, value)?)?;
631        }
632        Ok(dict.unbind())
633    }
634
635    #[getter]
636    fn sha256(&self) -> Option<&str> {
637        self.sha256.as_deref()
638    }
639}
640
641#[pyo3_stub_gen::derive::gen_stub_pymethods]
642#[pymethods]
643impl LiveNodeConfig {
644    /// Configuration for live Nautilus system nodes.
645    #[new]
646    #[expect(clippy::too_many_arguments)]
647    #[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, loop_debug=None, data_engine=None, risk_engine=None, exec_engine=None, plugins=None))]
648    fn py_new(
649        environment: Option<Environment>,
650        trader_id: Option<TraderId>,
651        load_state: Option<bool>,
652        save_state: Option<bool>,
653        shutdown_on_error: Option<bool>,
654        logging: Option<LoggerConfig>,
655        instance_id: Option<UUID4>,
656        timeout_connection_secs: Option<f64>,
657        timeout_reconciliation_secs: Option<f64>,
658        timeout_portfolio_secs: Option<f64>,
659        timeout_disconnection_secs: Option<f64>,
660        delay_post_stop_secs: Option<f64>,
661        timeout_shutdown_secs: Option<f64>,
662        cache: Option<CacheConfig>,
663        msgbus: Option<MessageBusConfig>,
664        portfolio: Option<PortfolioConfig>,
665        loop_debug: Option<bool>,
666        data_engine: Option<LiveDataEngineConfig>,
667        risk_engine: Option<LiveRiskEngineConfig>,
668        exec_engine: Option<LiveExecEngineConfig>,
669        plugins: Option<Vec<PluginConfig>>,
670    ) -> PyResult<Self> {
671        let default = Self::default();
672
673        let to_duration = |value: f64, name: &str| -> PyResult<Duration> {
674            duration_from_secs_f64(name, value).map_err(config_error_to_pyvalue_err)
675        };
676
677        Ok(Self {
678            environment: environment.unwrap_or(default.environment),
679            trader_id: trader_id.unwrap_or(default.trader_id),
680            load_state: load_state.unwrap_or(default.load_state),
681            save_state: save_state.unwrap_or(default.save_state),
682            shutdown_on_error: shutdown_on_error.unwrap_or(default.shutdown_on_error),
683            logging: logging.unwrap_or(default.logging),
684            instance_id,
685            timeout_connection: to_duration(
686                timeout_connection_secs.unwrap_or(default.timeout_connection.as_secs_f64()),
687                "timeout_connection_secs",
688            )?,
689            timeout_reconciliation: to_duration(
690                timeout_reconciliation_secs.unwrap_or(default.timeout_reconciliation.as_secs_f64()),
691                "timeout_reconciliation_secs",
692            )?,
693            timeout_portfolio: to_duration(
694                timeout_portfolio_secs.unwrap_or(default.timeout_portfolio.as_secs_f64()),
695                "timeout_portfolio_secs",
696            )?,
697            timeout_disconnection: to_duration(
698                timeout_disconnection_secs.unwrap_or(default.timeout_disconnection.as_secs_f64()),
699                "timeout_disconnection_secs",
700            )?,
701            delay_post_stop: to_duration(
702                delay_post_stop_secs.unwrap_or(default.delay_post_stop.as_secs_f64()),
703                "delay_post_stop_secs",
704            )?,
705            timeout_shutdown: to_duration(
706                timeout_shutdown_secs.unwrap_or(default.timeout_shutdown.as_secs_f64()),
707                "timeout_shutdown_secs",
708            )?,
709            cache,
710            msgbus,
711            portfolio,
712            emulator: None,
713            streaming: None,
714            event_store: None,
715            loop_debug: loop_debug.unwrap_or(false),
716            data_engine: data_engine.unwrap_or_default(),
717            risk_engine: risk_engine.unwrap_or_default(),
718            exec_engine: exec_engine.unwrap_or_default(),
719            data_clients: HashMap::new(),
720            exec_clients: HashMap::new(),
721            plugins: plugins.unwrap_or_default(),
722        })
723    }
724
725    fn __repr__(&self) -> String {
726        format!("{self:?}")
727    }
728
729    fn __str__(&self) -> String {
730        format!("{self:?}")
731    }
732
733    #[getter]
734    fn environment(&self) -> Environment {
735        self.environment
736    }
737
738    #[getter]
739    fn trader_id(&self) -> TraderId {
740        self.trader_id
741    }
742
743    #[getter]
744    fn load_state(&self) -> bool {
745        self.load_state
746    }
747
748    #[getter]
749    fn save_state(&self) -> bool {
750        self.save_state
751    }
752
753    #[getter]
754    fn shutdown_on_error(&self) -> bool {
755        self.shutdown_on_error
756    }
757
758    #[getter]
759    fn timeout_connection_secs(&self) -> f64 {
760        self.timeout_connection.as_secs_f64()
761    }
762
763    #[getter]
764    fn timeout_reconciliation_secs(&self) -> f64 {
765        self.timeout_reconciliation.as_secs_f64()
766    }
767
768    #[getter]
769    fn timeout_portfolio_secs(&self) -> f64 {
770        self.timeout_portfolio.as_secs_f64()
771    }
772
773    #[getter]
774    fn timeout_disconnection_secs(&self) -> f64 {
775        self.timeout_disconnection.as_secs_f64()
776    }
777
778    #[getter]
779    fn delay_post_stop_secs(&self) -> f64 {
780        self.delay_post_stop.as_secs_f64()
781    }
782
783    #[getter]
784    fn timeout_shutdown_secs(&self) -> f64 {
785        self.timeout_shutdown.as_secs_f64()
786    }
787
788    #[getter]
789    fn plugins(&self) -> Vec<PluginConfig> {
790        self.plugins.clone()
791    }
792}