1use std::{collections::HashMap, fmt::Display, str::FromStr, time::Duration};
19
20use nautilus_common::{
21 cache::CacheConfig, enums::Environment, logging::logger::LoggerConfig,
22 msgbus::MessageBusConfig, python::config_error_to_pyvalue_err,
23};
24use nautilus_core::{UUID4, UnixNanos, python::to_pyvalue_err};
25use nautilus_data::engine::config::DataEngineConfig;
26use nautilus_execution::{
27 engine::config::ExecutionEngineConfig,
28 models::latency::LatencyModelAny,
29 python::{
30 fee::{fee_model_any_to_pyobject, pyobject_to_fee_model_any},
31 fill::{fill_model_any_to_pyobject, pyobject_to_fill_model_any},
32 },
33};
34use nautilus_model::{
35 accounts::margin_model::MarginModelAny,
36 data::BarSpecification,
37 enums::{AccountType, BookType, OmsType, OtoTriggerMode},
38 identifiers::{ClientId, InstrumentId, TraderId},
39 types::Currency,
40};
41use nautilus_persistence::config::DataCatalogConfig;
42use nautilus_portfolio::config::PortfolioConfig;
43use nautilus_risk::engine::config::RiskEngineConfig;
44use nautilus_system::config::StreamingConfig;
45use nautilus_trading::ImportableControllerConfig;
46use pyo3::{Bound, IntoPyObjectExt, Py, PyAny, PyResult, Python, types::PyAnyMethods};
47use rust_decimal::Decimal;
48use ustr::Ustr;
49
50use super::{
51 engine::{pyobject_to_latency_model_any, pyobject_to_margin_model_any},
52 modules::{pyobject_to_simulation_module_any, simulation_module_any_to_pyobject},
53};
54use crate::config::{
55 BacktestDataConfig, BacktestEngineConfig, BacktestRunConfig, BacktestVenueConfig,
56 NautilusDataType,
57};
58
59#[pyo3_stub_gen::derive::gen_stub_pymethods]
60#[pyo3::pymethods]
61impl BacktestEngineConfig {
62 #[new]
64 #[pyo3(signature = (
65 trader_id = None,
66 load_state = None,
67 save_state = None,
68 shutdown_on_error = None,
69 bypass_logging = None,
70 run_analysis = None,
71 timeout_connection = None,
72 timeout_reconciliation = None,
73 timeout_portfolio = None,
74 timeout_disconnection = None,
75 delay_post_stop = None,
76 timeout_shutdown = None,
77 logging = None,
78 instance_id = None,
79 cache = None,
80 msgbus = None,
81 data_engine = None,
82 risk_engine = None,
83 exec_engine = None,
84 portfolio = None,
85 controller = None,
86 streaming = None,
87 catalogs = None,
88 ))]
89 #[expect(clippy::too_many_arguments)]
90 fn py_new(
91 trader_id: Option<TraderId>,
92 load_state: Option<bool>,
93 save_state: Option<bool>,
94 shutdown_on_error: Option<bool>,
95 bypass_logging: Option<bool>,
96 run_analysis: Option<bool>,
97 timeout_connection: Option<u64>,
98 timeout_reconciliation: Option<u64>,
99 timeout_portfolio: Option<u64>,
100 timeout_disconnection: Option<u64>,
101 delay_post_stop: Option<u64>,
102 timeout_shutdown: Option<u64>,
103 logging: Option<LoggerConfig>,
104 instance_id: Option<UUID4>,
105 cache: Option<CacheConfig>,
106 msgbus: Option<MessageBusConfig>,
107 data_engine: Option<DataEngineConfig>,
108 risk_engine: Option<RiskEngineConfig>,
109 exec_engine: Option<ExecutionEngineConfig>,
110 portfolio: Option<PortfolioConfig>,
111 controller: Option<ImportableControllerConfig>,
112 streaming: Option<StreamingConfig>,
113 catalogs: Option<Vec<DataCatalogConfig>>,
114 ) -> Self {
115 let defaults = Self::default();
116 Self {
117 environment: Environment::Backtest,
118 trader_id: trader_id.unwrap_or_default(),
119 load_state: load_state.unwrap_or(defaults.load_state),
120 save_state: save_state.unwrap_or(defaults.save_state),
121 shutdown_on_error: shutdown_on_error.unwrap_or(defaults.shutdown_on_error),
122 bypass_logging: bypass_logging.unwrap_or(defaults.bypass_logging),
123 run_analysis: run_analysis.unwrap_or(defaults.run_analysis),
124 timeout_connection: Duration::from_secs(timeout_connection.unwrap_or(60)),
125 timeout_reconciliation: Duration::from_secs(timeout_reconciliation.unwrap_or(30)),
126 timeout_portfolio: Duration::from_secs(timeout_portfolio.unwrap_or(10)),
127 timeout_disconnection: Duration::from_secs(timeout_disconnection.unwrap_or(10)),
128 delay_post_stop: Duration::from_secs(delay_post_stop.unwrap_or(10)),
129 timeout_shutdown: Duration::from_secs(timeout_shutdown.unwrap_or(5)),
130 logging: logging.unwrap_or_default(),
131 instance_id,
132 cache,
133 msgbus,
134 data_engine,
135 risk_engine,
136 exec_engine,
137 portfolio,
138 controller,
139 streaming,
140 catalogs: catalogs.unwrap_or_default(),
141 }
142 }
143
144 #[getter]
145 #[pyo3(name = "trader_id")]
146 fn py_trader_id(&self) -> TraderId {
147 self.trader_id
148 }
149
150 #[getter]
151 #[pyo3(name = "load_state")]
152 const fn py_load_state(&self) -> bool {
153 self.load_state
154 }
155
156 #[getter]
157 #[pyo3(name = "save_state")]
158 const fn py_save_state(&self) -> bool {
159 self.save_state
160 }
161
162 #[getter]
163 #[pyo3(name = "shutdown_on_error")]
164 const fn py_shutdown_on_error(&self) -> bool {
165 self.shutdown_on_error
166 }
167
168 #[getter]
169 #[pyo3(name = "bypass_logging")]
170 const fn py_bypass_logging(&self) -> bool {
171 self.bypass_logging
172 }
173
174 #[getter]
175 #[pyo3(name = "run_analysis")]
176 const fn py_run_analysis(&self) -> bool {
177 self.run_analysis
178 }
179
180 #[getter]
181 #[pyo3(name = "timeout_connection")]
182 fn py_timeout_connection(&self) -> f64 {
183 self.timeout_connection.as_secs_f64()
184 }
185
186 #[getter]
187 #[pyo3(name = "timeout_reconciliation")]
188 fn py_timeout_reconciliation(&self) -> f64 {
189 self.timeout_reconciliation.as_secs_f64()
190 }
191
192 #[getter]
193 #[pyo3(name = "timeout_portfolio")]
194 fn py_timeout_portfolio(&self) -> f64 {
195 self.timeout_portfolio.as_secs_f64()
196 }
197
198 #[getter]
199 #[pyo3(name = "timeout_disconnection")]
200 fn py_timeout_disconnection(&self) -> f64 {
201 self.timeout_disconnection.as_secs_f64()
202 }
203
204 #[getter]
205 #[pyo3(name = "delay_post_stop")]
206 fn py_delay_post_stop(&self) -> f64 {
207 self.delay_post_stop.as_secs_f64()
208 }
209
210 #[getter]
211 #[pyo3(name = "timeout_shutdown")]
212 fn py_timeout_shutdown(&self) -> f64 {
213 self.timeout_shutdown.as_secs_f64()
214 }
215
216 #[getter]
217 #[pyo3(name = "logging")]
218 fn py_logging(&self) -> LoggerConfig {
219 self.logging.clone()
220 }
221
222 #[getter]
223 #[pyo3(name = "instance_id")]
224 const fn py_instance_id(&self) -> Option<UUID4> {
225 self.instance_id
226 }
227
228 #[getter]
229 #[pyo3(name = "cache")]
230 fn py_cache(&self) -> Option<CacheConfig> {
231 self.cache.clone()
232 }
233
234 #[getter]
235 #[pyo3(name = "msgbus")]
236 fn py_msgbus(&self) -> Option<MessageBusConfig> {
237 self.msgbus.clone()
238 }
239
240 #[getter]
241 #[pyo3(name = "data_engine")]
242 fn py_data_engine(&self) -> Option<DataEngineConfig> {
243 self.data_engine.clone()
244 }
245
246 #[getter]
247 #[pyo3(name = "risk_engine")]
248 fn py_risk_engine(&self) -> Option<RiskEngineConfig> {
249 self.risk_engine.clone()
250 }
251
252 #[getter]
253 #[pyo3(name = "exec_engine")]
254 fn py_exec_engine(&self) -> Option<ExecutionEngineConfig> {
255 self.exec_engine.clone()
256 }
257
258 #[getter]
259 #[pyo3(name = "portfolio")]
260 const fn py_portfolio(&self) -> Option<PortfolioConfig> {
261 self.portfolio
262 }
263
264 #[getter]
265 #[pyo3(name = "controller")]
266 fn py_controller(&self) -> Option<ImportableControllerConfig> {
267 self.controller.clone()
268 }
269
270 #[getter]
271 #[pyo3(name = "streaming")]
272 fn py_streaming(&self) -> Option<StreamingConfig> {
273 self.streaming.clone()
274 }
275
276 #[getter]
277 #[pyo3(name = "catalogs")]
278 fn py_catalogs(&self) -> Vec<DataCatalogConfig> {
279 self.catalogs.clone()
280 }
281
282 fn __repr__(&self) -> String {
283 format!("{self:?}")
284 }
285}
286
287#[pyo3_stub_gen::derive::gen_stub_pymethods]
288#[pyo3::pymethods]
289impl BacktestVenueConfig {
290 #[new]
292 #[pyo3(signature = (
293 name,
294 oms_type,
295 account_type,
296 starting_balances,
297 book_type = None,
298 routing = None,
299 frozen_account = None,
300 reject_stop_orders = None,
301 support_gtd_orders = None,
302 support_contingent_orders = None,
303 use_position_ids = None,
304 use_random_ids = None,
305 use_reduce_only = None,
306 bar_execution = None,
307 bar_adaptive_high_low_ordering = None,
308 trade_execution = None,
309 use_market_order_acks = None,
310 liquidity_consumption = None,
311 allow_cash_borrowing = None,
312 queue_position = None,
313 oto_trigger_mode = None,
314 base_currency = None,
315 default_leverage = None,
316 leverages = None,
317 margin_model = None,
318 modules = None,
319 fill_model = None,
320 latency_model = None,
321 fee_model = None,
322 price_protection_points = None,
323 liquidation_enabled = None,
324 liquidation_trigger_ratio = None,
325 liquidation_cancel_open_orders = None,
326 ))]
327 #[expect(clippy::too_many_arguments)]
328 fn py_new(
329 name: &str,
330 #[gen_stub(override_type(type_repr = "model.OmsType | str"))] oms_type: &Bound<'_, PyAny>,
331 #[gen_stub(override_type(type_repr = "model.AccountType | str"))] account_type: &Bound<
332 '_,
333 PyAny,
334 >,
335 starting_balances: Vec<String>,
336 #[gen_stub(override_type(type_repr = "model.BookType | str | None"))] book_type: Option<
337 &Bound<'_, PyAny>,
338 >,
339 routing: Option<bool>,
340 frozen_account: Option<bool>,
341 reject_stop_orders: Option<bool>,
342 support_gtd_orders: Option<bool>,
343 support_contingent_orders: Option<bool>,
344 use_position_ids: Option<bool>,
345 use_random_ids: Option<bool>,
346 use_reduce_only: Option<bool>,
347 bar_execution: Option<bool>,
348 bar_adaptive_high_low_ordering: Option<bool>,
349 trade_execution: Option<bool>,
350 use_market_order_acks: Option<bool>,
351 liquidity_consumption: Option<bool>,
352 allow_cash_borrowing: Option<bool>,
353 queue_position: Option<bool>,
354 #[gen_stub(override_type(type_repr = "model.OtoTriggerMode | str | None"))]
355 oto_trigger_mode: Option<&Bound<'_, PyAny>>,
356 base_currency: Option<Currency>,
357 default_leverage: Option<Decimal>,
358 leverages: Option<HashMap<InstrumentId, Decimal>>,
359 margin_model: Option<Py<PyAny>>,
360 modules: Option<Vec<Py<PyAny>>>,
361 fill_model: Option<Py<PyAny>>,
362 latency_model: Option<Py<PyAny>>,
363 fee_model: Option<Py<PyAny>>,
364 price_protection_points: Option<u32>,
365 liquidation_enabled: Option<bool>,
366 liquidation_trigger_ratio: Option<f64>,
367 liquidation_cancel_open_orders: Option<bool>,
368 ) -> pyo3::PyResult<Self> {
369 let oms_type = enum_from_python(oms_type)?;
370 let account_type = enum_from_python(account_type)?;
371 let book_type = book_type
372 .map(enum_from_python)
373 .transpose()?
374 .unwrap_or(BookType::L1_MBP);
375 let oto_trigger_mode = oto_trigger_mode.map(enum_from_python).transpose()?;
376 let margin_model = margin_model
377 .map(|obj| Python::attach(|py| pyobject_to_margin_model_any(py, obj.bind(py))))
378 .transpose()?;
379 let modules = modules
380 .map(|objs| {
381 objs.into_iter()
382 .map(|obj| Python::attach(|py| pyobject_to_simulation_module_any(obj.bind(py))))
383 .collect::<pyo3::PyResult<Vec<_>>>()
384 })
385 .transpose()?
386 .unwrap_or_default();
387 let fill_model = fill_model
388 .map(|obj| Python::attach(|py| pyobject_to_fill_model_any(obj.bind(py))))
389 .transpose()?;
390 let latency_model = latency_model
391 .map(|obj| Python::attach(|py| pyobject_to_latency_model_any(py, obj.bind(py))))
392 .transpose()?;
393 let fee_model = fee_model
394 .map(|obj| Python::attach(|py| pyobject_to_fee_model_any(obj.bind(py))))
395 .transpose()?;
396
397 Self::builder()
398 .name(Ustr::from(name))
399 .oms_type(oms_type)
400 .account_type(account_type)
401 .book_type(book_type)
402 .starting_balances(starting_balances)
403 .maybe_routing(routing)
404 .maybe_frozen_account(frozen_account)
405 .maybe_reject_stop_orders(reject_stop_orders)
406 .maybe_support_gtd_orders(support_gtd_orders)
407 .maybe_support_contingent_orders(support_contingent_orders)
408 .maybe_use_position_ids(use_position_ids)
409 .maybe_use_random_ids(use_random_ids)
410 .maybe_use_reduce_only(use_reduce_only)
411 .maybe_bar_execution(bar_execution)
412 .maybe_bar_adaptive_high_low_ordering(bar_adaptive_high_low_ordering)
413 .maybe_trade_execution(trade_execution)
414 .maybe_use_market_order_acks(use_market_order_acks)
415 .maybe_liquidity_consumption(liquidity_consumption)
416 .maybe_allow_cash_borrowing(allow_cash_borrowing)
417 .maybe_queue_position(queue_position)
418 .maybe_oto_trigger_mode(oto_trigger_mode)
419 .maybe_base_currency(base_currency)
420 .maybe_default_leverage(default_leverage)
421 .maybe_leverages(leverages.map(|m| m.into_iter().collect()))
422 .maybe_margin_model(margin_model)
423 .modules(modules)
424 .maybe_fill_model(fill_model)
425 .maybe_latency_model(latency_model)
426 .maybe_fee_model(fee_model)
427 .maybe_price_protection_points(price_protection_points)
428 .maybe_liquidation_enabled(liquidation_enabled)
429 .maybe_liquidation_trigger_ratio(liquidation_trigger_ratio)
430 .maybe_liquidation_cancel_open_orders(liquidation_cancel_open_orders)
431 .build()
432 .map_err(config_error_to_pyvalue_err)
433 }
434
435 #[getter]
436 #[pyo3(name = "name")]
437 fn py_name(&self) -> &str {
438 self.name().as_str()
439 }
440
441 #[getter]
442 #[pyo3(name = "oms_type")]
443 fn py_oms_type(&self) -> OmsType {
444 self.oms_type()
445 }
446
447 #[getter]
448 #[pyo3(name = "account_type")]
449 fn py_account_type(&self) -> AccountType {
450 self.account_type()
451 }
452
453 #[getter]
454 #[pyo3(name = "book_type")]
455 fn py_book_type(&self) -> BookType {
456 self.book_type()
457 }
458
459 #[getter]
460 #[pyo3(name = "starting_balances")]
461 fn py_starting_balances(&self) -> Vec<String> {
462 self.starting_balances().to_vec()
463 }
464
465 #[getter]
466 #[pyo3(name = "routing")]
467 fn py_routing(&self) -> bool {
468 self.routing()
469 }
470
471 #[getter]
472 #[pyo3(name = "frozen_account")]
473 fn py_frozen_account(&self) -> bool {
474 self.frozen_account()
475 }
476
477 #[getter]
478 #[pyo3(name = "reject_stop_orders")]
479 fn py_reject_stop_orders(&self) -> bool {
480 self.reject_stop_orders()
481 }
482
483 #[getter]
484 #[pyo3(name = "support_gtd_orders")]
485 fn py_support_gtd_orders(&self) -> bool {
486 self.support_gtd_orders()
487 }
488
489 #[getter]
490 #[pyo3(name = "support_contingent_orders")]
491 fn py_support_contingent_orders(&self) -> bool {
492 self.support_contingent_orders()
493 }
494
495 #[getter]
496 #[pyo3(name = "use_position_ids")]
497 fn py_use_position_ids(&self) -> bool {
498 self.use_position_ids()
499 }
500
501 #[getter]
502 #[pyo3(name = "use_random_ids")]
503 fn py_use_random_ids(&self) -> bool {
504 self.use_random_ids()
505 }
506
507 #[getter]
508 #[pyo3(name = "use_reduce_only")]
509 fn py_use_reduce_only(&self) -> bool {
510 self.use_reduce_only()
511 }
512
513 #[getter]
514 #[pyo3(name = "bar_execution")]
515 fn py_bar_execution(&self) -> bool {
516 self.bar_execution()
517 }
518
519 #[getter]
520 #[pyo3(name = "trade_execution")]
521 fn py_trade_execution(&self) -> bool {
522 self.trade_execution()
523 }
524
525 #[getter]
526 #[pyo3(name = "bar_adaptive_high_low_ordering")]
527 fn py_bar_adaptive_high_low_ordering(&self) -> bool {
528 self.bar_adaptive_high_low_ordering()
529 }
530
531 #[getter]
532 #[pyo3(name = "use_market_order_acks")]
533 fn py_use_market_order_acks(&self) -> bool {
534 self.use_market_order_acks()
535 }
536
537 #[getter]
538 #[pyo3(name = "liquidity_consumption")]
539 fn py_liquidity_consumption(&self) -> bool {
540 self.liquidity_consumption()
541 }
542
543 #[getter]
544 #[pyo3(name = "allow_cash_borrowing")]
545 fn py_allow_cash_borrowing(&self) -> bool {
546 self.allow_cash_borrowing()
547 }
548
549 #[getter]
550 #[pyo3(name = "queue_position")]
551 fn py_queue_position(&self) -> bool {
552 self.queue_position()
553 }
554
555 #[getter]
556 #[pyo3(name = "oto_trigger_mode")]
557 fn py_oto_trigger_mode(&self) -> OtoTriggerMode {
558 self.oto_trigger_mode()
559 }
560
561 #[getter]
562 #[pyo3(name = "base_currency")]
563 fn py_base_currency(&self) -> Option<Currency> {
564 self.base_currency()
565 }
566
567 #[getter]
568 #[pyo3(name = "default_leverage")]
569 fn py_default_leverage(&self) -> Option<Decimal> {
570 self.default_leverage()
571 }
572
573 #[getter]
574 #[pyo3(name = "leverages")]
575 fn py_leverages(&self) -> Option<HashMap<InstrumentId, Decimal>> {
576 self.leverages().map(|leverages| {
577 leverages
578 .iter()
579 .map(|(key, value)| (*key, *value))
580 .collect()
581 })
582 }
583
584 #[getter]
585 #[pyo3(name = "margin_model")]
586 fn py_margin_model(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
587 self.margin_model()
588 .map(|model| margin_model_any_to_pyobject(py, model))
589 .transpose()
590 }
591
592 #[getter]
593 #[pyo3(name = "modules")]
594 fn py_modules(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
595 self.modules()
596 .iter()
597 .map(|module| simulation_module_any_to_pyobject(py, module))
598 .collect()
599 }
600
601 #[getter]
602 #[pyo3(name = "fill_model")]
603 fn py_fill_model(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
604 self.fill_model()
605 .map(|model| fill_model_any_to_pyobject(py, model))
606 .transpose()
607 }
608
609 #[getter]
610 #[pyo3(name = "latency_model")]
611 fn py_latency_model(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
612 self.latency_model()
613 .map(|model| latency_model_any_to_pyobject(py, model))
614 .transpose()
615 }
616
617 #[getter]
618 #[pyo3(name = "fee_model")]
619 fn py_fee_model(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
620 self.fee_model()
621 .map(|model| fee_model_any_to_pyobject(py, model))
622 .transpose()
623 }
624
625 #[getter]
626 #[pyo3(name = "price_protection_points")]
627 fn py_price_protection_points(&self) -> u32 {
628 self.price_protection_points()
629 }
630
631 #[getter]
632 #[pyo3(name = "liquidation_enabled")]
633 fn py_liquidation_enabled(&self) -> bool {
634 self.liquidation_enabled()
635 }
636
637 #[getter]
638 #[pyo3(name = "liquidation_trigger_ratio")]
639 fn py_liquidation_trigger_ratio(&self) -> f64 {
640 self.liquidation_trigger_ratio()
641 }
642
643 #[getter]
644 #[pyo3(name = "liquidation_cancel_open_orders")]
645 fn py_liquidation_cancel_open_orders(&self) -> bool {
646 self.liquidation_cancel_open_orders()
647 }
648
649 fn __repr__(&self) -> String {
650 format!("{self:?}")
651 }
652}
653
654#[pyo3_stub_gen::derive::gen_stub_pymethods]
655#[pyo3::pymethods]
656impl BacktestDataConfig {
657 #[new]
659 #[pyo3(signature = (
660 data_type,
661 catalog_path,
662 catalog_fs_protocol = None,
663 catalog_fs_storage_options = None,
664 catalog_fs_rust_storage_options = None,
665 instrument_id = None,
666 instrument_ids = None,
667 start_time = None,
668 end_time = None,
669 filter_expr = None,
670 client_id = None,
671 metadata = None,
672 bar_spec = None,
673 bar_types = None,
674 optimize_file_loading = None,
675 ))]
676 #[expect(clippy::too_many_arguments)]
677 fn py_new(
678 data_type: &str,
679 catalog_path: String,
680 catalog_fs_protocol: Option<String>,
681 catalog_fs_storage_options: Option<HashMap<String, String>>,
682 catalog_fs_rust_storage_options: Option<HashMap<String, String>>,
683 instrument_id: Option<InstrumentId>,
684 instrument_ids: Option<Vec<InstrumentId>>,
685 #[gen_stub(override_type(
686 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
687 imports = ("datetime", "pandas as pd")
688 ))]
689 start_time: Option<Py<PyAny>>,
690 #[gen_stub(override_type(
691 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
692 imports = ("datetime", "pandas as pd")
693 ))]
694 end_time: Option<Py<PyAny>>,
695 filter_expr: Option<String>,
696 client_id: Option<ClientId>,
697 metadata: Option<HashMap<String, String>>,
698 bar_spec: Option<BarSpecification>,
699 bar_types: Option<Vec<String>>,
700 optimize_file_loading: Option<bool>,
701 ) -> pyo3::PyResult<Self> {
702 let data_type = data_type
703 .parse::<NautilusDataType>()
704 .map_err(to_pyvalue_err)?;
705 let start_time = timestamp_from_python(start_time)?;
706 let end_time = timestamp_from_python(end_time)?;
707 Self::builder()
708 .data_type(data_type)
709 .catalog_path(catalog_path)
710 .maybe_catalog_fs_protocol(catalog_fs_protocol)
711 .maybe_catalog_fs_storage_options(
712 catalog_fs_storage_options.map(|m| m.into_iter().collect()),
713 )
714 .maybe_catalog_fs_rust_storage_options(
715 catalog_fs_rust_storage_options.map(|m| m.into_iter().collect()),
716 )
717 .maybe_instrument_id(instrument_id)
718 .maybe_instrument_ids(instrument_ids)
719 .maybe_start_time(start_time)
720 .maybe_end_time(end_time)
721 .maybe_filter_expr(filter_expr)
722 .maybe_client_id(client_id)
723 .maybe_metadata(metadata.map(|m| m.into_iter().collect()))
724 .maybe_bar_spec(bar_spec)
725 .maybe_bar_types(bar_types)
726 .maybe_optimize_file_loading(optimize_file_loading)
727 .build()
728 .map_err(config_error_to_pyvalue_err)
729 }
730
731 #[getter]
732 #[pyo3(name = "data_type")]
733 fn py_data_type(&self) -> String {
734 self.data_type().to_string()
735 }
736
737 #[getter]
738 #[pyo3(name = "catalog_path")]
739 fn py_catalog_path(&self) -> &str {
740 self.catalog_path()
741 }
742
743 #[getter]
744 #[pyo3(name = "instrument_id")]
745 fn py_instrument_id(&self) -> Option<InstrumentId> {
746 self.instrument_id()
747 }
748
749 #[getter]
750 #[pyo3(name = "catalog_fs_protocol")]
751 fn py_catalog_fs_protocol(&self) -> Option<&str> {
752 self.catalog_fs_protocol()
753 }
754
755 #[getter]
756 #[pyo3(name = "catalog_fs_storage_option_keys")]
757 fn py_catalog_fs_storage_option_keys(&self) -> Option<Vec<String>> {
758 self.catalog_fs_storage_options().map(|options| {
759 let mut keys = options.keys().cloned().collect::<Vec<_>>();
760 keys.sort_unstable();
761 keys
762 })
763 }
764
765 #[getter]
766 #[pyo3(name = "catalog_fs_rust_storage_option_keys")]
767 fn py_catalog_fs_rust_storage_option_keys(&self) -> Option<Vec<String>> {
768 self.catalog_fs_rust_storage_options().map(|options| {
769 let mut keys = options.keys().cloned().collect::<Vec<_>>();
770 keys.sort_unstable();
771 keys
772 })
773 }
774
775 #[getter]
776 #[pyo3(name = "instrument_ids")]
777 fn py_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
778 self.instrument_ids().map(<[InstrumentId]>::to_vec)
779 }
780
781 #[getter]
782 #[pyo3(name = "start_time")]
783 fn py_start_time(&self) -> Option<u64> {
784 self.start_time().map(|timestamp| timestamp.as_u64())
785 }
786
787 #[getter]
788 #[pyo3(name = "end_time")]
789 fn py_end_time(&self) -> Option<u64> {
790 self.end_time().map(|timestamp| timestamp.as_u64())
791 }
792
793 #[getter]
794 #[pyo3(name = "filter_expr")]
795 fn py_filter_expr(&self) -> Option<&str> {
796 self.filter_expr()
797 }
798
799 #[getter]
800 #[pyo3(name = "client_id")]
801 fn py_client_id(&self) -> Option<ClientId> {
802 self.client_id()
803 }
804
805 #[getter]
806 #[pyo3(name = "metadata")]
807 fn py_metadata(&self) -> Option<HashMap<String, String>> {
808 self.metadata().map(|metadata| {
809 metadata
810 .iter()
811 .map(|(key, value)| (key.clone(), value.clone()))
812 .collect()
813 })
814 }
815
816 #[getter]
817 #[pyo3(name = "bar_spec")]
818 fn py_bar_spec(&self) -> Option<BarSpecification> {
819 self.bar_spec()
820 }
821
822 #[getter]
823 #[pyo3(name = "bar_types")]
824 fn py_bar_types(&self) -> Option<Vec<String>> {
825 self.bar_types().map(<[String]>::to_vec)
826 }
827
828 #[getter]
829 #[pyo3(name = "optimize_file_loading")]
830 fn py_optimize_file_loading(&self) -> bool {
831 self.optimize_file_loading()
832 }
833
834 fn __repr__(&self) -> String {
835 format!("{self:?}")
836 }
837}
838
839#[pyo3_stub_gen::derive::gen_stub_pymethods]
840#[pyo3::pymethods]
841impl BacktestRunConfig {
842 #[new]
845 #[pyo3(signature = (
846 venues,
847 data,
848 engine = None,
849 id = None,
850 chunk_size = None,
851 raise_exception = None,
852 dispose_on_completion = None,
853 start = None,
854 end = None,
855 ))]
856 #[expect(clippy::too_many_arguments)]
857 fn py_new(
858 venues: Vec<BacktestVenueConfig>,
859 data: Vec<BacktestDataConfig>,
860 engine: Option<BacktestEngineConfig>,
861 id: Option<String>,
862 chunk_size: Option<usize>,
863 raise_exception: Option<bool>,
864 dispose_on_completion: Option<bool>,
865 #[gen_stub(override_type(
866 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
867 imports = ("datetime", "pandas as pd")
868 ))]
869 start: Option<Py<PyAny>>,
870 #[gen_stub(override_type(
871 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
872 imports = ("datetime", "pandas as pd")
873 ))]
874 end: Option<Py<PyAny>>,
875 ) -> pyo3::PyResult<Self> {
876 let start = timestamp_from_python(start)?;
877 let end = timestamp_from_python(end)?;
878 Self::builder()
879 .venues(venues)
880 .data(data)
881 .maybe_engine(engine)
882 .maybe_id(id)
883 .maybe_chunk_size(chunk_size)
884 .maybe_raise_exception(raise_exception)
885 .maybe_dispose_on_completion(dispose_on_completion)
886 .maybe_start(start)
887 .maybe_end(end)
888 .build()
889 .map_err(config_error_to_pyvalue_err)
890 }
891
892 #[getter]
893 #[pyo3(name = "id")]
894 fn py_id(&self) -> &str {
895 self.id()
896 }
897
898 #[getter]
899 #[pyo3(name = "venues")]
900 fn py_venues(&self) -> Vec<BacktestVenueConfig> {
901 self.venues().to_vec()
902 }
903
904 #[getter]
905 #[pyo3(name = "data")]
906 fn py_data(&self) -> Vec<BacktestDataConfig> {
907 self.data().to_vec()
908 }
909
910 #[getter]
911 #[pyo3(name = "engine")]
912 fn py_engine(&self) -> BacktestEngineConfig {
913 self.engine().clone()
914 }
915
916 #[getter]
917 #[pyo3(name = "chunk_size")]
918 fn py_chunk_size(&self) -> Option<usize> {
919 self.chunk_size()
920 }
921
922 #[getter]
923 #[pyo3(name = "raise_exception")]
924 fn py_raise_exception(&self) -> bool {
925 self.raise_exception()
926 }
927
928 #[getter]
929 #[pyo3(name = "dispose_on_completion")]
930 fn py_dispose_on_completion(&self) -> bool {
931 self.dispose_on_completion()
932 }
933
934 #[getter]
935 #[pyo3(name = "start")]
936 fn py_start(&self) -> Option<u64> {
937 self.start().map(|timestamp| timestamp.as_u64())
938 }
939
940 #[getter]
941 #[pyo3(name = "end")]
942 fn py_end(&self) -> Option<u64> {
943 self.end().map(|timestamp| timestamp.as_u64())
944 }
945
946 fn __repr__(&self) -> String {
947 format!("{self:?}")
948 }
949}
950
951fn timestamp_from_python(value: Option<Py<PyAny>>) -> PyResult<Option<UnixNanos>> {
952 value
953 .map(|value| {
954 Python::attach(|py| {
955 py.import("nautilus_trader.core.datetime")?
956 .getattr("dt_to_unix_nanos")?
957 .call1((value,))?
958 .extract::<u64>()
959 .map(UnixNanos::from)
960 })
961 })
962 .transpose()
963}
964
965fn enum_from_python<'py, E>(value: &Bound<'py, PyAny>) -> PyResult<E>
966where
967 E: pyo3::conversion::FromPyObjectOwned<'py> + FromStr,
968 E::Err: Display,
969{
970 if let Ok(value) = value.extract::<E>() {
971 return Ok(value);
972 }
973 value
974 .extract::<String>()?
975 .parse::<E>()
976 .map_err(to_pyvalue_err)
977}
978
979fn margin_model_any_to_pyobject(py: Python<'_>, model: &MarginModelAny) -> PyResult<Py<PyAny>> {
980 match model {
981 MarginModelAny::Standard(model) => (*model).into_py_any(py),
982 MarginModelAny::Leveraged(model) => (*model).into_py_any(py),
983 }
984}
985
986fn latency_model_any_to_pyobject(py: Python<'_>, model: &LatencyModelAny) -> PyResult<Py<PyAny>> {
987 match model {
988 LatencyModelAny::Static(model) => model.clone().into_py_any(py),
989 }
990}