1use 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
126fn 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
149fn py_to_json_value(bound: &pyo3::Bound<'_, PyAny>) -> PyResult<serde_json::Value> {
154 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 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 let s: String = bound.str()?.extract()?;
175 Ok(serde_json::Value::String(s))
176 }
177}
178
179fn 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
211fn 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}