Skip to main content

nautilus_backtest/python/
config.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings for backtest configuration types.
17
18use 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    /// Configuration for ``BacktestEngine`` instances.
63    #[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    /// Represents a venue configuration for one specific backtest engine.
291    #[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    /// Represents the data configuration for one specific backtest run.
658    #[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    /// Represents the configuration for one specific backtest run.
843    /// This includes a backtest engine with its actors and strategies, with the external inputs of venues and data.
844    #[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}