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