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 data::BarSpecification,
39 enums::{AccountType, BookType, OmsType, OtoTriggerMode},
40 identifiers::{ClientId, InstrumentId, TraderId},
41 python::{
42 account::margin_model::{margin_model_any_to_pyobject, pyobject_to_margin_model_any},
43 data::PyNautilusDataType,
44 },
45 types::Currency,
46};
47use nautilus_persistence::config::{DataCatalogConfig, StreamingConfig};
48use nautilus_portfolio::config::PortfolioConfig;
49use nautilus_risk::engine::config::RiskEngineConfig;
50use nautilus_trading::ImportableControllerConfig;
51use pyo3::{Bound, Py, PyAny, PyResult, Python, types::PyAnyMethods};
52use rust_decimal::Decimal;
53use ustr::Ustr;
54
55use super::modules::{pyobject_to_simulation_module_any, simulation_module_any_to_pyobject};
56use crate::config::{
57 BacktestDataConfig, BacktestEngineConfig, BacktestRunConfig, BacktestVenueConfig,
58};
59
60#[pyo3_stub_gen::derive::gen_stub_pymethods]
61#[pyo3::pymethods]
62impl BacktestEngineConfig {
63 #[new]
65 #[pyo3(signature = (
66 trader_id = None,
67 load_state = None,
68 save_state = None,
69 shutdown_on_error = None,
70 bypass_logging = None,
71 run_analysis = None,
72 timeout_connection = None,
73 timeout_reconciliation = None,
74 timeout_portfolio = None,
75 timeout_disconnection = None,
76 delay_post_stop = None,
77 timeout_shutdown = None,
78 logging = None,
79 instance_id = None,
80 cache = None,
81 msgbus = None,
82 data_engine = None,
83 risk_engine = None,
84 exec_engine = None,
85 portfolio = None,
86 controller = None,
87 streaming = None,
88 catalogs = None,
89 ))]
90 #[expect(clippy::too_many_arguments)]
91 fn py_new(
92 trader_id: Option<TraderId>,
93 load_state: Option<bool>,
94 save_state: Option<bool>,
95 shutdown_on_error: Option<bool>,
96 bypass_logging: Option<bool>,
97 run_analysis: Option<bool>,
98 timeout_connection: Option<u64>,
99 timeout_reconciliation: Option<u64>,
100 timeout_portfolio: Option<u64>,
101 timeout_disconnection: Option<u64>,
102 delay_post_stop: Option<u64>,
103 timeout_shutdown: Option<u64>,
104 logging: Option<LoggerConfig>,
105 instance_id: Option<UUID4>,
106 cache: Option<CacheConfig>,
107 msgbus: Option<MessageBusConfig>,
108 data_engine: Option<DataEngineConfig>,
109 risk_engine: Option<RiskEngineConfig>,
110 exec_engine: Option<ExecutionEngineConfig>,
111 portfolio: Option<PortfolioConfig>,
112 controller: Option<ImportableControllerConfig>,
113 streaming: Option<StreamingConfig>,
114 catalogs: Option<Vec<DataCatalogConfig>>,
115 ) -> Self {
116 let defaults = Self::default();
117 Self {
118 environment: Environment::Backtest,
119 trader_id: trader_id.unwrap_or_default(),
120 load_state: load_state.unwrap_or(defaults.load_state),
121 save_state: save_state.unwrap_or(defaults.save_state),
122 shutdown_on_error: shutdown_on_error.unwrap_or(defaults.shutdown_on_error),
123 bypass_logging: bypass_logging.unwrap_or(defaults.bypass_logging),
124 run_analysis: run_analysis.unwrap_or(defaults.run_analysis),
125 timeout_connection: Duration::from_secs(timeout_connection.unwrap_or(60)),
126 timeout_reconciliation: Duration::from_secs(timeout_reconciliation.unwrap_or(30)),
127 timeout_portfolio: Duration::from_secs(timeout_portfolio.unwrap_or(10)),
128 timeout_disconnection: Duration::from_secs(timeout_disconnection.unwrap_or(10)),
129 delay_post_stop: Duration::from_secs(delay_post_stop.unwrap_or(10)),
130 timeout_shutdown: Duration::from_secs(timeout_shutdown.unwrap_or(5)),
131 logging: logging.unwrap_or_default(),
132 instance_id,
133 cache,
134 msgbus,
135 data_engine,
136 risk_engine,
137 exec_engine,
138 portfolio,
139 controller,
140 streaming,
141 catalogs: catalogs.unwrap_or_default(),
142 }
143 }
144
145 #[getter]
146 #[pyo3(name = "trader_id")]
147 fn py_trader_id(&self) -> TraderId {
148 self.trader_id
149 }
150
151 #[getter]
152 #[pyo3(name = "load_state")]
153 const fn py_load_state(&self) -> bool {
154 self.load_state
155 }
156
157 #[getter]
158 #[pyo3(name = "save_state")]
159 const fn py_save_state(&self) -> bool {
160 self.save_state
161 }
162
163 #[getter]
164 #[pyo3(name = "shutdown_on_error")]
165 const fn py_shutdown_on_error(&self) -> bool {
166 self.shutdown_on_error
167 }
168
169 #[getter]
170 #[pyo3(name = "bypass_logging")]
171 const fn py_bypass_logging(&self) -> bool {
172 self.bypass_logging
173 }
174
175 #[getter]
176 #[pyo3(name = "run_analysis")]
177 const fn py_run_analysis(&self) -> bool {
178 self.run_analysis
179 }
180
181 #[getter]
182 #[pyo3(name = "timeout_connection")]
183 fn py_timeout_connection(&self) -> f64 {
184 self.timeout_connection.as_secs_f64()
185 }
186
187 #[getter]
188 #[pyo3(name = "timeout_reconciliation")]
189 fn py_timeout_reconciliation(&self) -> f64 {
190 self.timeout_reconciliation.as_secs_f64()
191 }
192
193 #[getter]
194 #[pyo3(name = "timeout_portfolio")]
195 fn py_timeout_portfolio(&self) -> f64 {
196 self.timeout_portfolio.as_secs_f64()
197 }
198
199 #[getter]
200 #[pyo3(name = "timeout_disconnection")]
201 fn py_timeout_disconnection(&self) -> f64 {
202 self.timeout_disconnection.as_secs_f64()
203 }
204
205 #[getter]
206 #[pyo3(name = "delay_post_stop")]
207 fn py_delay_post_stop(&self) -> f64 {
208 self.delay_post_stop.as_secs_f64()
209 }
210
211 #[getter]
212 #[pyo3(name = "timeout_shutdown")]
213 fn py_timeout_shutdown(&self) -> f64 {
214 self.timeout_shutdown.as_secs_f64()
215 }
216
217 #[getter]
218 #[pyo3(name = "logging")]
219 fn py_logging(&self) -> LoggerConfig {
220 self.logging.clone()
221 }
222
223 #[getter]
224 #[pyo3(name = "instance_id")]
225 const fn py_instance_id(&self) -> Option<UUID4> {
226 self.instance_id
227 }
228
229 #[getter]
230 #[pyo3(name = "cache")]
231 fn py_cache(&self) -> Option<CacheConfig> {
232 self.cache.clone()
233 }
234
235 #[getter]
236 #[pyo3(name = "msgbus")]
237 fn py_msgbus(&self) -> Option<MessageBusConfig> {
238 self.msgbus.clone()
239 }
240
241 #[getter]
242 #[pyo3(name = "data_engine")]
243 fn py_data_engine(&self) -> Option<DataEngineConfig> {
244 self.data_engine.clone()
245 }
246
247 #[getter]
248 #[pyo3(name = "risk_engine")]
249 fn py_risk_engine(&self) -> Option<RiskEngineConfig> {
250 self.risk_engine.clone()
251 }
252
253 #[getter]
254 #[pyo3(name = "exec_engine")]
255 fn py_exec_engine(&self) -> Option<ExecutionEngineConfig> {
256 self.exec_engine.clone()
257 }
258
259 #[getter]
260 #[pyo3(name = "portfolio")]
261 const fn py_portfolio(&self) -> Option<PortfolioConfig> {
262 self.portfolio
263 }
264
265 #[getter]
266 #[pyo3(name = "controller")]
267 fn py_controller(&self) -> Option<ImportableControllerConfig> {
268 self.controller.clone()
269 }
270
271 #[getter]
272 #[pyo3(name = "streaming")]
273 fn py_streaming(&self) -> Option<StreamingConfig> {
274 self.streaming.clone()
275 }
276
277 #[getter]
278 #[pyo3(name = "catalogs")]
279 fn py_catalogs(&self) -> Vec<DataCatalogConfig> {
280 self.catalogs.clone()
281 }
282
283 fn __repr__(&self) -> String {
284 format!("{self:?}")
285 }
286}
287
288#[pyo3_stub_gen::derive::gen_stub_pymethods]
289#[pyo3::pymethods]
290impl BacktestVenueConfig {
291 #[new]
293 #[pyo3(signature = (
294 name,
295 oms_type,
296 account_type,
297 starting_balances,
298 book_type = None,
299 routing = None,
300 frozen_account = None,
301 reject_stop_orders = None,
302 support_gtd_orders = None,
303 support_contingent_orders = None,
304 use_position_ids = None,
305 use_random_ids = None,
306 use_reduce_only = None,
307 bar_execution = None,
308 bar_adaptive_high_low_ordering = None,
309 trade_execution = None,
310 use_market_order_acks = None,
311 liquidity_consumption = None,
312 allow_cash_borrowing = None,
313 queue_position = None,
314 oto_trigger_mode = None,
315 base_currency = None,
316 default_leverage = None,
317 leverages = None,
318 fill_model = None,
319 fee_model = None,
320 margin_model = None,
321 latency_model = None,
322 modules = None,
323 price_protection_points = None,
324 liquidation_enabled = None,
325 liquidation_trigger_ratio = None,
326 liquidation_cancel_open_orders = None,
327 ))]
328 #[expect(clippy::too_many_arguments)]
329 fn py_new(
330 name: &str,
331 #[gen_stub(override_type(type_repr = "model.OmsType | str"))] oms_type: &Bound<'_, PyAny>,
332 #[gen_stub(override_type(type_repr = "model.AccountType | str"))] account_type: &Bound<
333 '_,
334 PyAny,
335 >,
336 starting_balances: Vec<String>,
337 #[gen_stub(override_type(type_repr = "model.BookType | str | None"))] book_type: Option<
338 &Bound<'_, PyAny>,
339 >,
340 routing: Option<bool>,
341 frozen_account: Option<bool>,
342 reject_stop_orders: Option<bool>,
343 support_gtd_orders: Option<bool>,
344 support_contingent_orders: Option<bool>,
345 use_position_ids: Option<bool>,
346 use_random_ids: Option<bool>,
347 use_reduce_only: Option<bool>,
348 bar_execution: Option<bool>,
349 bar_adaptive_high_low_ordering: Option<bool>,
350 trade_execution: Option<bool>,
351 use_market_order_acks: Option<bool>,
352 liquidity_consumption: Option<bool>,
353 allow_cash_borrowing: Option<bool>,
354 queue_position: Option<bool>,
355 #[gen_stub(override_type(type_repr = "model.OtoTriggerMode | str | None"))]
356 oto_trigger_mode: Option<&Bound<'_, PyAny>>,
357 base_currency: Option<Currency>,
358 default_leverage: Option<Decimal>,
359 leverages: Option<HashMap<InstrumentId, Decimal>>,
360 fill_model: Option<Py<PyAny>>,
361 fee_model: Option<Py<PyAny>>,
362 margin_model: Option<Py<PyAny>>,
363 latency_model: Option<Py<PyAny>>,
364 modules: Option<Vec<Py<PyAny>>>,
365 price_protection_points: Option<u32>,
366 liquidation_enabled: Option<bool>,
367 liquidation_trigger_ratio: Option<f64>,
368 liquidation_cancel_open_orders: Option<bool>,
369 ) -> pyo3::PyResult<Self> {
370 let oms_type = enum_from_python(oms_type)?;
371 let account_type = enum_from_python(account_type)?;
372 let book_type = book_type
373 .map(enum_from_python)
374 .transpose()?
375 .unwrap_or(BookType::L1_MBP);
376 let oto_trigger_mode = oto_trigger_mode.map(enum_from_python).transpose()?;
377 let fill_model = fill_model
378 .map(|obj| Python::attach(|py| pyobject_to_fill_model_any(obj.bind(py))))
379 .transpose()?;
380
381 let fee_model = fee_model
382 .map(|obj| Python::attach(|py| pyobject_to_fee_model_any(obj.bind(py))))
383 .transpose()?;
384
385 let margin_model = margin_model
386 .map(|obj| Python::attach(|py| pyobject_to_margin_model_any(obj.bind(py))))
387 .transpose()?;
388
389 let latency_model = latency_model
390 .map(|obj| Python::attach(|py| pyobject_to_latency_model_any(obj.bind(py))))
391 .transpose()?;
392
393 let modules = modules
394 .map(|objs| {
395 objs.into_iter()
396 .map(|obj| Python::attach(|py| pyobject_to_simulation_module_any(obj.bind(py))))
397 .collect::<pyo3::PyResult<Vec<_>>>()
398 })
399 .transpose()?
400 .unwrap_or_default();
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_fill_model(fill_model)
428 .maybe_fee_model(fee_model)
429 .maybe_margin_model(margin_model)
430 .maybe_latency_model(latency_model)
431 .modules(modules)
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 = "fill_model")]
591 fn py_fill_model(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
592 self.fill_model()
593 .map(|model| fill_model_any_to_pyobject(py, model))
594 .transpose()
595 }
596
597 #[getter]
598 #[pyo3(name = "fee_model")]
599 fn py_fee_model(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
600 self.fee_model()
601 .map(|model| fee_model_any_to_pyobject(py, model))
602 .transpose()
603 }
604
605 #[getter]
606 #[pyo3(name = "margin_model")]
607 fn py_margin_model(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
608 self.margin_model()
609 .map(|model| margin_model_any_to_pyobject(py, model))
610 .transpose()
611 }
612
613 #[getter]
614 #[pyo3(name = "latency_model")]
615 fn py_latency_model(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
616 self.latency_model()
617 .map(|model| latency_model_any_to_pyobject(py, model))
618 .transpose()
619 }
620
621 #[getter]
622 #[pyo3(name = "modules")]
623 fn py_modules(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
624 self.modules()
625 .iter()
626 .map(|module| simulation_module_any_to_pyobject(py, module))
627 .collect()
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,
667 instrument_id = None,
668 instrument_ids = None,
669 start_time = None,
670 end_time = None,
671 filter_expr = None,
672 client_id = None,
673 metadata = None,
674 bar_spec = None,
675 bar_types = None,
676 optimize_file_loading = None,
677 batch_deltas = None,
678 ))]
679 #[expect(clippy::too_many_arguments)]
680 fn py_new(
681 #[gen_stub(override_type(type_repr = "model.NautilusDataType"))] data_type: &Bound<
682 '_,
683 PyAny,
684 >,
685 catalog: DataCatalogConfig,
686 instrument_id: Option<InstrumentId>,
687 instrument_ids: Option<Vec<InstrumentId>>,
688 #[gen_stub(override_type(
689 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
690 imports = ("datetime", "pandas as pd")
691 ))]
692 start_time: Option<Py<PyAny>>,
693 #[gen_stub(override_type(
694 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
695 imports = ("datetime", "pandas as pd")
696 ))]
697 end_time: Option<Py<PyAny>>,
698 filter_expr: Option<String>,
699 client_id: Option<ClientId>,
700 metadata: Option<HashMap<String, String>>,
701 bar_spec: Option<BarSpecification>,
702 bar_types: Option<Vec<String>>,
703 optimize_file_loading: Option<bool>,
704 batch_deltas: Option<bool>,
705 ) -> pyo3::PyResult<Self> {
706 let data_type = data_type
707 .extract::<pyo3::PyRef<'_, PyNautilusDataType>>()
708 .map(|data_type| data_type.inner())
709 .map_err(|_| to_pytype_err("data_type must be NautilusDataType"))?;
710 let start_time = timestamp_from_python(start_time)?;
711 let end_time = timestamp_from_python(end_time)?;
712 Self::builder()
713 .data_type(data_type)
714 .catalog(catalog)
715 .maybe_instrument_id(instrument_id)
716 .maybe_instrument_ids(instrument_ids)
717 .maybe_start_time(start_time)
718 .maybe_end_time(end_time)
719 .maybe_filter_expr(filter_expr)
720 .maybe_client_id(client_id)
721 .maybe_metadata(metadata.map(|m| m.into_iter().collect()))
722 .maybe_bar_spec(bar_spec)
723 .maybe_bar_types(bar_types)
724 .maybe_optimize_file_loading(optimize_file_loading)
725 .maybe_batch_deltas(batch_deltas)
726 .build()
727 .map_err(config_error_to_pyvalue_err)
728 }
729
730 #[getter]
732 #[pyo3(name = "catalog")]
733 fn py_catalog(&self) -> DataCatalogConfig {
734 self.catalog().clone()
735 }
736
737 #[getter]
738 #[pyo3(name = "data_type")]
739 fn py_data_type(&self) -> PyNautilusDataType {
740 PyNautilusDataType::new(self.data_type().clone())
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 = "instrument_ids")]
751 fn py_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
752 self.instrument_ids().map(<[InstrumentId]>::to_vec)
753 }
754
755 #[getter]
756 #[pyo3(name = "start_time")]
757 fn py_start_time(&self) -> Option<u64> {
758 self.start_time().map(|timestamp| timestamp.as_u64())
759 }
760
761 #[getter]
762 #[pyo3(name = "end_time")]
763 fn py_end_time(&self) -> Option<u64> {
764 self.end_time().map(|timestamp| timestamp.as_u64())
765 }
766
767 #[getter]
768 #[pyo3(name = "filter_expr")]
769 fn py_filter_expr(&self) -> Option<&str> {
770 self.filter_expr()
771 }
772
773 #[getter]
774 #[pyo3(name = "client_id")]
775 fn py_client_id(&self) -> Option<ClientId> {
776 self.client_id()
777 }
778
779 #[getter]
780 #[pyo3(name = "metadata")]
781 fn py_metadata(&self) -> Option<HashMap<String, String>> {
782 self.metadata().map(|metadata| {
783 metadata
784 .iter()
785 .map(|(key, value)| (key.clone(), value.clone()))
786 .collect()
787 })
788 }
789
790 #[getter]
791 #[pyo3(name = "bar_spec")]
792 fn py_bar_spec(&self) -> Option<BarSpecification> {
793 self.bar_spec()
794 }
795
796 #[getter]
797 #[pyo3(name = "bar_types")]
798 fn py_bar_types(&self) -> Option<Vec<String>> {
799 self.bar_types().map(<[String]>::to_vec)
800 }
801
802 #[getter]
803 #[pyo3(name = "optimize_file_loading")]
804 fn py_optimize_file_loading(&self) -> bool {
805 self.optimize_file_loading()
806 }
807
808 #[getter]
809 #[pyo3(name = "batch_deltas")]
810 fn py_batch_deltas(&self) -> bool {
811 self.batch_deltas()
812 }
813
814 fn __repr__(&self) -> String {
815 format!("{self:?}")
816 }
817}
818
819#[pyo3_stub_gen::derive::gen_stub_pymethods]
820#[pyo3::pymethods]
821impl BacktestRunConfig {
822 #[new]
825 #[pyo3(signature = (
826 venues,
827 data,
828 engine = None,
829 id = None,
830 chunk_size = None,
831 raise_exception = None,
832 dispose_on_completion = None,
833 start = None,
834 end = None,
835 ))]
836 #[expect(clippy::too_many_arguments)]
837 fn py_new(
838 venues: Vec<BacktestVenueConfig>,
839 data: Vec<BacktestDataConfig>,
840 engine: Option<BacktestEngineConfig>,
841 id: Option<String>,
842 chunk_size: Option<usize>,
843 raise_exception: Option<bool>,
844 dispose_on_completion: Option<bool>,
845 #[gen_stub(override_type(
846 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
847 imports = ("datetime", "pandas as pd")
848 ))]
849 start: Option<Py<PyAny>>,
850 #[gen_stub(override_type(
851 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
852 imports = ("datetime", "pandas as pd")
853 ))]
854 end: Option<Py<PyAny>>,
855 ) -> pyo3::PyResult<Self> {
856 let start = timestamp_from_python(start)?;
857 let end = timestamp_from_python(end)?;
858 Self::builder()
859 .venues(venues)
860 .data(data)
861 .maybe_engine(engine)
862 .maybe_id(id)
863 .maybe_chunk_size(chunk_size)
864 .maybe_raise_exception(raise_exception)
865 .maybe_dispose_on_completion(dispose_on_completion)
866 .maybe_start(start)
867 .maybe_end(end)
868 .build()
869 .map_err(config_error_to_pyvalue_err)
870 }
871
872 #[getter]
873 #[pyo3(name = "id")]
874 fn py_id(&self) -> &str {
875 self.id()
876 }
877
878 #[getter]
879 #[pyo3(name = "venues")]
880 fn py_venues(&self) -> Vec<BacktestVenueConfig> {
881 self.venues().to_vec()
882 }
883
884 #[getter]
885 #[pyo3(name = "data")]
886 fn py_data(&self) -> Vec<BacktestDataConfig> {
887 self.data().to_vec()
888 }
889
890 #[getter]
891 #[pyo3(name = "engine")]
892 fn py_engine(&self) -> BacktestEngineConfig {
893 self.engine().clone()
894 }
895
896 #[getter]
897 #[pyo3(name = "chunk_size")]
898 fn py_chunk_size(&self) -> Option<usize> {
899 self.chunk_size()
900 }
901
902 #[getter]
903 #[pyo3(name = "raise_exception")]
904 fn py_raise_exception(&self) -> bool {
905 self.raise_exception()
906 }
907
908 #[getter]
909 #[pyo3(name = "dispose_on_completion")]
910 fn py_dispose_on_completion(&self) -> bool {
911 self.dispose_on_completion()
912 }
913
914 #[getter]
915 #[pyo3(name = "start")]
916 fn py_start(&self) -> Option<u64> {
917 self.start().map(|timestamp| timestamp.as_u64())
918 }
919
920 #[getter]
921 #[pyo3(name = "end")]
922 fn py_end(&self) -> Option<u64> {
923 self.end().map(|timestamp| timestamp.as_u64())
924 }
925
926 fn __repr__(&self) -> String {
927 format!("{self:?}")
928 }
929}
930
931fn timestamp_from_python(value: Option<Py<PyAny>>) -> PyResult<Option<UnixNanos>> {
932 value
933 .map(|value| {
934 Python::attach(|py| {
935 py.import("nautilus_trader.core.datetime")?
936 .getattr("dt_to_unix_nanos")?
937 .call1((value,))?
938 .extract::<u64>()
939 .map(UnixNanos::from)
940 })
941 })
942 .transpose()
943}
944
945fn enum_from_python<'py, E>(value: &Bound<'py, PyAny>) -> PyResult<E>
946where
947 E: pyo3::conversion::FromPyObjectOwned<'py> + FromStr,
948 E::Err: Display,
949{
950 if let Ok(value) = value.extract::<E>() {
951 return Ok(value);
952 }
953 value
954 .extract::<String>()?
955 .parse::<E>()
956 .map_err(to_pyvalue_err)
957}
958
959#[cfg(test)]
960mod tests {
961 use ahash::AHashMap;
962 use nautilus_core::string::secret::SecretString;
963 use nautilus_model::data::NautilusDataType;
964 use rstest::rstest;
965
966 use super::*;
967
968 #[rstest]
969 fn test_data_config_py_new_keeps_catalog_storage_option_values() {
970 Python::initialize();
971
972 let config = Python::attach(|py| {
973 let data_type =
974 Bound::new(py, PyNautilusDataType::new(NautilusDataType::QuoteTick)).unwrap();
975 BacktestDataConfig::py_new(
976 data_type.as_any(),
977 DataCatalogConfig::new("bucket/catalog".to_string(), Some("s3".to_string()), None)
978 .with_storage_options(Some(AHashMap::from([(
979 "aws_secret_access_key".to_string(),
980 SecretString::from("rust-secret"),
981 )]))),
982 Some(InstrumentId::from("EUR/USD.SIM")),
983 None,
984 None,
985 None,
986 None,
987 None,
988 None,
989 None,
990 None,
991 None,
992 None,
993 )
994 .unwrap()
995 });
996
997 assert_eq!(
998 config.catalog().fs_rust_storage_options(),
999 Some(&AHashMap::from([(
1000 "aws_secret_access_key".to_string(),
1001 SecretString::from("rust-secret"),
1002 )]))
1003 );
1004 }
1005
1006 #[rstest]
1007 #[case::default(None, true)]
1008 #[case::disabled(Some(false), false)]
1009 #[case::enabled(Some(true), true)]
1010 fn test_data_config_py_new_sets_batch_deltas(
1011 #[case] batch_deltas: Option<bool>,
1012 #[case] expected: bool,
1013 ) {
1014 Python::initialize();
1015
1016 let config = Python::attach(|py| {
1017 let data_type = Bound::new(
1018 py,
1019 PyNautilusDataType::new(NautilusDataType::OrderBookDelta),
1020 )
1021 .unwrap();
1022 BacktestDataConfig::py_new(
1023 data_type.as_any(),
1024 DataCatalogConfig::new("catalog".to_string(), None, None),
1025 Some(InstrumentId::from("EUR/USD.SIM")),
1026 None,
1027 None,
1028 None,
1029 None,
1030 None,
1031 None,
1032 None,
1033 None,
1034 None,
1035 batch_deltas,
1036 )
1037 .unwrap()
1038 });
1039
1040 assert_eq!(config.batch_deltas(), expected);
1041 assert_eq!(config.py_batch_deltas(), expected);
1042 }
1043}