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