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