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