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 batch_deltas = None,
682 ))]
683 #[expect(clippy::too_many_arguments)]
684 fn py_new(
685 #[gen_stub(override_type(type_repr = "model.NautilusDataType"))] data_type: &Bound<
686 '_,
687 PyAny,
688 >,
689 catalog_path: String,
690 catalog_fs_protocol: Option<String>,
691 catalog_fs_storage_options: Option<HashMap<String, String>>,
692 catalog_fs_rust_storage_options: Option<HashMap<String, String>>,
693 instrument_id: Option<InstrumentId>,
694 instrument_ids: Option<Vec<InstrumentId>>,
695 #[gen_stub(override_type(
696 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
697 imports = ("datetime", "pandas as pd")
698 ))]
699 start_time: Option<Py<PyAny>>,
700 #[gen_stub(override_type(
701 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
702 imports = ("datetime", "pandas as pd")
703 ))]
704 end_time: Option<Py<PyAny>>,
705 filter_expr: Option<String>,
706 client_id: Option<ClientId>,
707 metadata: Option<HashMap<String, String>>,
708 bar_spec: Option<BarSpecification>,
709 bar_types: Option<Vec<String>>,
710 optimize_file_loading: Option<bool>,
711 catalog_backend: Option<pyo3::PyRef<'_, PyCatalogBackend>>,
712 batch_deltas: Option<bool>,
713 ) -> pyo3::PyResult<Self> {
714 let data_type = data_type
715 .extract::<pyo3::PyRef<'_, PyNautilusDataType>>()
716 .map(|data_type| data_type.inner())
717 .map_err(|_| to_pytype_err("data_type must be NautilusDataType"))?;
718 let start_time = timestamp_from_python(start_time)?;
719 let end_time = timestamp_from_python(end_time)?;
720 Self::builder()
721 .data_type(data_type)
722 .catalog_path(catalog_path)
723 .catalog_backend(
724 catalog_backend
725 .map(|backend| backend.inner())
726 .unwrap_or_default(),
727 )
728 .maybe_catalog_fs_protocol(catalog_fs_protocol)
729 .maybe_catalog_fs_storage_options(catalog_fs_storage_options.map(|m| {
730 m.into_iter()
731 .map(|(key, value)| (key, value.into()))
732 .collect()
733 }))
734 .maybe_catalog_fs_rust_storage_options(catalog_fs_rust_storage_options.map(|m| {
735 m.into_iter()
736 .map(|(key, value)| (key, value.into()))
737 .collect()
738 }))
739 .maybe_instrument_id(instrument_id)
740 .maybe_instrument_ids(instrument_ids)
741 .maybe_start_time(start_time)
742 .maybe_end_time(end_time)
743 .maybe_filter_expr(filter_expr)
744 .maybe_client_id(client_id)
745 .maybe_metadata(metadata.map(|m| m.into_iter().collect()))
746 .maybe_bar_spec(bar_spec)
747 .maybe_bar_types(bar_types)
748 .maybe_optimize_file_loading(optimize_file_loading)
749 .maybe_batch_deltas(batch_deltas)
750 .build()
751 .map_err(config_error_to_pyvalue_err)
752 }
753
754 #[getter]
756 #[pyo3(name = "catalog_backend")]
757 fn py_catalog_backend(&self) -> PyCatalogBackend {
758 PyCatalogBackend::new(self.catalog_backend())
759 }
760
761 #[getter]
762 #[pyo3(name = "data_type")]
763 fn py_data_type(&self) -> PyNautilusDataType {
764 PyNautilusDataType::new(self.data_type().clone())
765 }
766
767 #[getter]
768 #[pyo3(name = "catalog_path")]
769 fn py_catalog_path(&self) -> &str {
770 self.catalog_path()
771 }
772
773 #[getter]
774 #[pyo3(name = "instrument_id")]
775 fn py_instrument_id(&self) -> Option<InstrumentId> {
776 self.instrument_id()
777 }
778
779 #[getter]
780 #[pyo3(name = "catalog_fs_protocol")]
781 fn py_catalog_fs_protocol(&self) -> Option<&str> {
782 self.catalog_fs_protocol()
783 }
784
785 #[getter]
786 #[pyo3(name = "catalog_fs_storage_option_keys")]
787 fn py_catalog_fs_storage_option_keys(&self) -> Option<Vec<String>> {
788 self.catalog_fs_storage_options().map(|options| {
789 let mut keys = options.keys().cloned().collect::<Vec<_>>();
790 keys.sort_unstable();
791 keys
792 })
793 }
794
795 #[getter]
796 #[pyo3(name = "catalog_fs_rust_storage_option_keys")]
797 fn py_catalog_fs_rust_storage_option_keys(&self) -> Option<Vec<String>> {
798 self.catalog_fs_rust_storage_options().map(|options| {
799 let mut keys = options.keys().cloned().collect::<Vec<_>>();
800 keys.sort_unstable();
801 keys
802 })
803 }
804
805 #[getter]
806 #[pyo3(name = "instrument_ids")]
807 fn py_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
808 self.instrument_ids().map(<[InstrumentId]>::to_vec)
809 }
810
811 #[getter]
812 #[pyo3(name = "start_time")]
813 fn py_start_time(&self) -> Option<u64> {
814 self.start_time().map(|timestamp| timestamp.as_u64())
815 }
816
817 #[getter]
818 #[pyo3(name = "end_time")]
819 fn py_end_time(&self) -> Option<u64> {
820 self.end_time().map(|timestamp| timestamp.as_u64())
821 }
822
823 #[getter]
824 #[pyo3(name = "filter_expr")]
825 fn py_filter_expr(&self) -> Option<&str> {
826 self.filter_expr()
827 }
828
829 #[getter]
830 #[pyo3(name = "client_id")]
831 fn py_client_id(&self) -> Option<ClientId> {
832 self.client_id()
833 }
834
835 #[getter]
836 #[pyo3(name = "metadata")]
837 fn py_metadata(&self) -> Option<HashMap<String, String>> {
838 self.metadata().map(|metadata| {
839 metadata
840 .iter()
841 .map(|(key, value)| (key.clone(), value.clone()))
842 .collect()
843 })
844 }
845
846 #[getter]
847 #[pyo3(name = "bar_spec")]
848 fn py_bar_spec(&self) -> Option<BarSpecification> {
849 self.bar_spec()
850 }
851
852 #[getter]
853 #[pyo3(name = "bar_types")]
854 fn py_bar_types(&self) -> Option<Vec<String>> {
855 self.bar_types().map(<[String]>::to_vec)
856 }
857
858 #[getter]
859 #[pyo3(name = "optimize_file_loading")]
860 fn py_optimize_file_loading(&self) -> bool {
861 self.optimize_file_loading()
862 }
863
864 #[getter]
865 #[pyo3(name = "batch_deltas")]
866 fn py_batch_deltas(&self) -> bool {
867 self.batch_deltas()
868 }
869
870 fn __repr__(&self) -> String {
871 format!("{self:?}")
872 }
873}
874
875#[pyo3_stub_gen::derive::gen_stub_pymethods]
876#[pyo3::pymethods]
877impl BacktestRunConfig {
878 #[new]
881 #[pyo3(signature = (
882 venues,
883 data,
884 engine = None,
885 id = None,
886 chunk_size = None,
887 raise_exception = None,
888 dispose_on_completion = None,
889 start = None,
890 end = None,
891 ))]
892 #[expect(clippy::too_many_arguments)]
893 fn py_new(
894 venues: Vec<BacktestVenueConfig>,
895 data: Vec<BacktestDataConfig>,
896 engine: Option<BacktestEngineConfig>,
897 id: Option<String>,
898 chunk_size: Option<usize>,
899 raise_exception: Option<bool>,
900 dispose_on_completion: Option<bool>,
901 #[gen_stub(override_type(
902 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
903 imports = ("datetime", "pandas as pd")
904 ))]
905 start: Option<Py<PyAny>>,
906 #[gen_stub(override_type(
907 type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
908 imports = ("datetime", "pandas as pd")
909 ))]
910 end: Option<Py<PyAny>>,
911 ) -> pyo3::PyResult<Self> {
912 let start = timestamp_from_python(start)?;
913 let end = timestamp_from_python(end)?;
914 Self::builder()
915 .venues(venues)
916 .data(data)
917 .maybe_engine(engine)
918 .maybe_id(id)
919 .maybe_chunk_size(chunk_size)
920 .maybe_raise_exception(raise_exception)
921 .maybe_dispose_on_completion(dispose_on_completion)
922 .maybe_start(start)
923 .maybe_end(end)
924 .build()
925 .map_err(config_error_to_pyvalue_err)
926 }
927
928 #[getter]
929 #[pyo3(name = "id")]
930 fn py_id(&self) -> &str {
931 self.id()
932 }
933
934 #[getter]
935 #[pyo3(name = "venues")]
936 fn py_venues(&self) -> Vec<BacktestVenueConfig> {
937 self.venues().to_vec()
938 }
939
940 #[getter]
941 #[pyo3(name = "data")]
942 fn py_data(&self) -> Vec<BacktestDataConfig> {
943 self.data().to_vec()
944 }
945
946 #[getter]
947 #[pyo3(name = "engine")]
948 fn py_engine(&self) -> BacktestEngineConfig {
949 self.engine().clone()
950 }
951
952 #[getter]
953 #[pyo3(name = "chunk_size")]
954 fn py_chunk_size(&self) -> Option<usize> {
955 self.chunk_size()
956 }
957
958 #[getter]
959 #[pyo3(name = "raise_exception")]
960 fn py_raise_exception(&self) -> bool {
961 self.raise_exception()
962 }
963
964 #[getter]
965 #[pyo3(name = "dispose_on_completion")]
966 fn py_dispose_on_completion(&self) -> bool {
967 self.dispose_on_completion()
968 }
969
970 #[getter]
971 #[pyo3(name = "start")]
972 fn py_start(&self) -> Option<u64> {
973 self.start().map(|timestamp| timestamp.as_u64())
974 }
975
976 #[getter]
977 #[pyo3(name = "end")]
978 fn py_end(&self) -> Option<u64> {
979 self.end().map(|timestamp| timestamp.as_u64())
980 }
981
982 fn __repr__(&self) -> String {
983 format!("{self:?}")
984 }
985}
986
987fn timestamp_from_python(value: Option<Py<PyAny>>) -> PyResult<Option<UnixNanos>> {
988 value
989 .map(|value| {
990 Python::attach(|py| {
991 py.import("nautilus_trader.core.datetime")?
992 .getattr("dt_to_unix_nanos")?
993 .call1((value,))?
994 .extract::<u64>()
995 .map(UnixNanos::from)
996 })
997 })
998 .transpose()
999}
1000
1001fn enum_from_python<'py, E>(value: &Bound<'py, PyAny>) -> PyResult<E>
1002where
1003 E: pyo3::conversion::FromPyObjectOwned<'py> + FromStr,
1004 E::Err: Display,
1005{
1006 if let Ok(value) = value.extract::<E>() {
1007 return Ok(value);
1008 }
1009 value
1010 .extract::<String>()?
1011 .parse::<E>()
1012 .map_err(to_pyvalue_err)
1013}
1014
1015fn margin_model_any_to_pyobject(py: Python<'_>, model: &MarginModelAny) -> PyResult<Py<PyAny>> {
1016 match model {
1017 MarginModelAny::Standard(model) => (*model).into_py_any(py),
1018 MarginModelAny::Leveraged(model) => (*model).into_py_any(py),
1019 }
1020}
1021
1022#[cfg(test)]
1023mod tests {
1024 use ahash::AHashMap;
1025 use nautilus_core::string::secret::SecretString;
1026 use nautilus_model::data::NautilusDataType;
1027 use rstest::rstest;
1028
1029 use super::*;
1030
1031 #[rstest]
1032 fn test_data_config_py_new_keeps_storage_option_values() {
1033 Python::initialize();
1034
1035 let config = Python::attach(|py| {
1036 let data_type =
1037 Bound::new(py, PyNautilusDataType::new(NautilusDataType::QuoteTick)).unwrap();
1038 BacktestDataConfig::py_new(
1039 data_type.as_any(),
1040 "bucket/catalog".to_string(),
1041 Some("s3".to_string()),
1042 Some(HashMap::from([("key".to_string(), "fs-key".to_string())])),
1043 Some(HashMap::from([(
1044 "aws_secret_access_key".to_string(),
1045 "rust-secret".to_string(),
1046 )])),
1047 Some(InstrumentId::from("EUR/USD.SIM")),
1048 None,
1049 None,
1050 None,
1051 None,
1052 None,
1053 None,
1054 None,
1055 None,
1056 None,
1057 None,
1058 None,
1059 )
1060 .unwrap()
1061 });
1062
1063 assert_eq!(
1064 config.catalog_fs_storage_options(),
1065 Some(&AHashMap::from([(
1066 "key".to_string(),
1067 SecretString::from("fs-key"),
1068 )]))
1069 );
1070 assert_eq!(
1071 config.catalog_fs_rust_storage_options(),
1072 Some(&AHashMap::from([(
1073 "aws_secret_access_key".to_string(),
1074 SecretString::from("rust-secret"),
1075 )]))
1076 );
1077 }
1078
1079 #[rstest]
1080 #[case::default(None, true)]
1081 #[case::disabled(Some(false), false)]
1082 #[case::enabled(Some(true), true)]
1083 fn test_data_config_py_new_sets_batch_deltas(
1084 #[case] batch_deltas: Option<bool>,
1085 #[case] expected: bool,
1086 ) {
1087 Python::initialize();
1088
1089 let config = Python::attach(|py| {
1090 let data_type = Bound::new(
1091 py,
1092 PyNautilusDataType::new(NautilusDataType::OrderBookDelta),
1093 )
1094 .unwrap();
1095 BacktestDataConfig::py_new(
1096 data_type.as_any(),
1097 "catalog".to_string(),
1098 None,
1099 None,
1100 None,
1101 Some(InstrumentId::from("EUR/USD.SIM")),
1102 None,
1103 None,
1104 None,
1105 None,
1106 None,
1107 None,
1108 None,
1109 None,
1110 None,
1111 None,
1112 batch_deltas,
1113 )
1114 .unwrap()
1115 });
1116
1117 assert_eq!(config.batch_deltas(), expected);
1118 assert_eq!(config.py_batch_deltas(), expected);
1119 }
1120}