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