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::{
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    /// Configuration for ``BacktestEngine`` instances.
68    #[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    /// Represents a venue configuration for one specific backtest engine.
296    #[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    /// Represents the data configuration for one specific backtest run.
663    #[new]
664    #[pyo3(signature = (
665        data_type,
666        catalog_path,
667        catalog_fs_protocol = None,
668        catalog_fs_storage_options = None,
669        catalog_fs_rust_storage_options = None,
670        instrument_id = None,
671        instrument_ids = None,
672        start_time = None,
673        end_time = None,
674        filter_expr = None,
675        client_id = None,
676        metadata = None,
677        bar_spec = None,
678        bar_types = None,
679        optimize_file_loading = None,
680        catalog_backend = None,
681    ))]
682    #[expect(clippy::too_many_arguments)]
683    fn py_new(
684        #[gen_stub(override_type(type_repr = "model.NautilusDataType"))] data_type: &Bound<
685            '_,
686            PyAny,
687        >,
688        catalog_path: String,
689        catalog_fs_protocol: Option<String>,
690        catalog_fs_storage_options: Option<HashMap<String, String>>,
691        catalog_fs_rust_storage_options: Option<HashMap<String, String>>,
692        instrument_id: Option<InstrumentId>,
693        instrument_ids: Option<Vec<InstrumentId>>,
694        #[gen_stub(override_type(
695            type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
696            imports = ("datetime", "pandas as pd")
697        ))]
698        start_time: Option<Py<PyAny>>,
699        #[gen_stub(override_type(
700            type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
701            imports = ("datetime", "pandas as pd")
702        ))]
703        end_time: Option<Py<PyAny>>,
704        filter_expr: Option<String>,
705        client_id: Option<ClientId>,
706        metadata: Option<HashMap<String, String>>,
707        bar_spec: Option<BarSpecification>,
708        bar_types: Option<Vec<String>>,
709        optimize_file_loading: Option<bool>,
710        catalog_backend: Option<pyo3::PyRef<'_, PyCatalogBackend>>,
711    ) -> pyo3::PyResult<Self> {
712        let data_type = data_type
713            .extract::<pyo3::PyRef<'_, PyNautilusDataType>>()
714            .map(|data_type| data_type.inner())
715            .map_err(|_| to_pytype_err("data_type must be NautilusDataType"))?;
716        let start_time = timestamp_from_python(start_time)?;
717        let end_time = timestamp_from_python(end_time)?;
718        Self::builder()
719            .data_type(data_type)
720            .catalog_path(catalog_path)
721            .catalog_backend(
722                catalog_backend
723                    .map(|backend| backend.inner())
724                    .unwrap_or_default(),
725            )
726            .maybe_catalog_fs_protocol(catalog_fs_protocol)
727            .maybe_catalog_fs_storage_options(
728                catalog_fs_storage_options.map(|m| m.into_iter().collect()),
729            )
730            .maybe_catalog_fs_rust_storage_options(
731                catalog_fs_rust_storage_options.map(|m| m.into_iter().collect()),
732            )
733            .maybe_instrument_id(instrument_id)
734            .maybe_instrument_ids(instrument_ids)
735            .maybe_start_time(start_time)
736            .maybe_end_time(end_time)
737            .maybe_filter_expr(filter_expr)
738            .maybe_client_id(client_id)
739            .maybe_metadata(metadata.map(|m| m.into_iter().collect()))
740            .maybe_bar_spec(bar_spec)
741            .maybe_bar_types(bar_types)
742            .maybe_optimize_file_loading(optimize_file_loading)
743            .build()
744            .map_err(config_error_to_pyvalue_err)
745    }
746
747    /// Returns the configured catalog backend.
748    #[getter]
749    #[pyo3(name = "catalog_backend")]
750    fn py_catalog_backend(&self) -> PyCatalogBackend {
751        PyCatalogBackend::new(self.catalog_backend())
752    }
753
754    #[getter]
755    #[pyo3(name = "data_type")]
756    fn py_data_type(&self) -> PyNautilusDataType {
757        PyNautilusDataType::new(self.data_type().clone())
758    }
759
760    #[getter]
761    #[pyo3(name = "catalog_path")]
762    fn py_catalog_path(&self) -> &str {
763        self.catalog_path()
764    }
765
766    #[getter]
767    #[pyo3(name = "instrument_id")]
768    fn py_instrument_id(&self) -> Option<InstrumentId> {
769        self.instrument_id()
770    }
771
772    #[getter]
773    #[pyo3(name = "catalog_fs_protocol")]
774    fn py_catalog_fs_protocol(&self) -> Option<&str> {
775        self.catalog_fs_protocol()
776    }
777
778    #[getter]
779    #[pyo3(name = "catalog_fs_storage_option_keys")]
780    fn py_catalog_fs_storage_option_keys(&self) -> Option<Vec<String>> {
781        self.catalog_fs_storage_options().map(|options| {
782            let mut keys = options.keys().cloned().collect::<Vec<_>>();
783            keys.sort_unstable();
784            keys
785        })
786    }
787
788    #[getter]
789    #[pyo3(name = "catalog_fs_rust_storage_option_keys")]
790    fn py_catalog_fs_rust_storage_option_keys(&self) -> Option<Vec<String>> {
791        self.catalog_fs_rust_storage_options().map(|options| {
792            let mut keys = options.keys().cloned().collect::<Vec<_>>();
793            keys.sort_unstable();
794            keys
795        })
796    }
797
798    #[getter]
799    #[pyo3(name = "instrument_ids")]
800    fn py_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
801        self.instrument_ids().map(<[InstrumentId]>::to_vec)
802    }
803
804    #[getter]
805    #[pyo3(name = "start_time")]
806    fn py_start_time(&self) -> Option<u64> {
807        self.start_time().map(|timestamp| timestamp.as_u64())
808    }
809
810    #[getter]
811    #[pyo3(name = "end_time")]
812    fn py_end_time(&self) -> Option<u64> {
813        self.end_time().map(|timestamp| timestamp.as_u64())
814    }
815
816    #[getter]
817    #[pyo3(name = "filter_expr")]
818    fn py_filter_expr(&self) -> Option<&str> {
819        self.filter_expr()
820    }
821
822    #[getter]
823    #[pyo3(name = "client_id")]
824    fn py_client_id(&self) -> Option<ClientId> {
825        self.client_id()
826    }
827
828    #[getter]
829    #[pyo3(name = "metadata")]
830    fn py_metadata(&self) -> Option<HashMap<String, String>> {
831        self.metadata().map(|metadata| {
832            metadata
833                .iter()
834                .map(|(key, value)| (key.clone(), value.clone()))
835                .collect()
836        })
837    }
838
839    #[getter]
840    #[pyo3(name = "bar_spec")]
841    fn py_bar_spec(&self) -> Option<BarSpecification> {
842        self.bar_spec()
843    }
844
845    #[getter]
846    #[pyo3(name = "bar_types")]
847    fn py_bar_types(&self) -> Option<Vec<String>> {
848        self.bar_types().map(<[String]>::to_vec)
849    }
850
851    #[getter]
852    #[pyo3(name = "optimize_file_loading")]
853    fn py_optimize_file_loading(&self) -> bool {
854        self.optimize_file_loading()
855    }
856
857    fn __repr__(&self) -> String {
858        format!("{self:?}")
859    }
860}
861
862#[pyo3_stub_gen::derive::gen_stub_pymethods]
863#[pyo3::pymethods]
864impl BacktestRunConfig {
865    /// Represents the configuration for one specific backtest run.
866    /// This includes a backtest engine with its actors and strategies, with the external inputs of venues and data.
867    #[new]
868    #[pyo3(signature = (
869        venues,
870        data,
871        engine = None,
872        id = None,
873        chunk_size = None,
874        raise_exception = None,
875        dispose_on_completion = None,
876        start = None,
877        end = None,
878    ))]
879    #[expect(clippy::too_many_arguments)]
880    fn py_new(
881        venues: Vec<BacktestVenueConfig>,
882        data: Vec<BacktestDataConfig>,
883        engine: Option<BacktestEngineConfig>,
884        id: Option<String>,
885        chunk_size: Option<usize>,
886        raise_exception: Option<bool>,
887        dispose_on_completion: Option<bool>,
888        #[gen_stub(override_type(
889            type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
890            imports = ("datetime", "pandas as pd")
891        ))]
892        start: Option<Py<PyAny>>,
893        #[gen_stub(override_type(
894            type_repr = "int | str | datetime.datetime | pd.Timestamp | None",
895            imports = ("datetime", "pandas as pd")
896        ))]
897        end: Option<Py<PyAny>>,
898    ) -> pyo3::PyResult<Self> {
899        let start = timestamp_from_python(start)?;
900        let end = timestamp_from_python(end)?;
901        Self::builder()
902            .venues(venues)
903            .data(data)
904            .maybe_engine(engine)
905            .maybe_id(id)
906            .maybe_chunk_size(chunk_size)
907            .maybe_raise_exception(raise_exception)
908            .maybe_dispose_on_completion(dispose_on_completion)
909            .maybe_start(start)
910            .maybe_end(end)
911            .build()
912            .map_err(config_error_to_pyvalue_err)
913    }
914
915    #[getter]
916    #[pyo3(name = "id")]
917    fn py_id(&self) -> &str {
918        self.id()
919    }
920
921    #[getter]
922    #[pyo3(name = "venues")]
923    fn py_venues(&self) -> Vec<BacktestVenueConfig> {
924        self.venues().to_vec()
925    }
926
927    #[getter]
928    #[pyo3(name = "data")]
929    fn py_data(&self) -> Vec<BacktestDataConfig> {
930        self.data().to_vec()
931    }
932
933    #[getter]
934    #[pyo3(name = "engine")]
935    fn py_engine(&self) -> BacktestEngineConfig {
936        self.engine().clone()
937    }
938
939    #[getter]
940    #[pyo3(name = "chunk_size")]
941    fn py_chunk_size(&self) -> Option<usize> {
942        self.chunk_size()
943    }
944
945    #[getter]
946    #[pyo3(name = "raise_exception")]
947    fn py_raise_exception(&self) -> bool {
948        self.raise_exception()
949    }
950
951    #[getter]
952    #[pyo3(name = "dispose_on_completion")]
953    fn py_dispose_on_completion(&self) -> bool {
954        self.dispose_on_completion()
955    }
956
957    #[getter]
958    #[pyo3(name = "start")]
959    fn py_start(&self) -> Option<u64> {
960        self.start().map(|timestamp| timestamp.as_u64())
961    }
962
963    #[getter]
964    #[pyo3(name = "end")]
965    fn py_end(&self) -> Option<u64> {
966        self.end().map(|timestamp| timestamp.as_u64())
967    }
968
969    fn __repr__(&self) -> String {
970        format!("{self:?}")
971    }
972}
973
974fn timestamp_from_python(value: Option<Py<PyAny>>) -> PyResult<Option<UnixNanos>> {
975    value
976        .map(|value| {
977            Python::attach(|py| {
978                py.import("nautilus_trader.core.datetime")?
979                    .getattr("dt_to_unix_nanos")?
980                    .call1((value,))?
981                    .extract::<u64>()
982                    .map(UnixNanos::from)
983            })
984        })
985        .transpose()
986}
987
988fn enum_from_python<'py, E>(value: &Bound<'py, PyAny>) -> PyResult<E>
989where
990    E: pyo3::conversion::FromPyObjectOwned<'py> + FromStr,
991    E::Err: Display,
992{
993    if let Ok(value) = value.extract::<E>() {
994        return Ok(value);
995    }
996    value
997        .extract::<String>()?
998        .parse::<E>()
999        .map_err(to_pyvalue_err)
1000}
1001
1002fn margin_model_any_to_pyobject(py: Python<'_>, model: &MarginModelAny) -> PyResult<Py<PyAny>> {
1003    match model {
1004        MarginModelAny::Standard(model) => (*model).into_py_any(py),
1005        MarginModelAny::Leveraged(model) => (*model).into_py_any(py),
1006    }
1007}