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