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, time::Duration};
19
20use nautilus_common::{
21    cache::CacheConfig, enums::Environment, logging::logger::LoggerConfig,
22    msgbus::MessageBusConfig, python::config_error_to_pyvalue_err,
23};
24use nautilus_core::{UUID4, UnixNanos, python::to_pyvalue_err};
25use nautilus_data::engine::config::DataEngineConfig;
26use nautilus_execution::{
27    engine::config::ExecutionEngineConfig, python::fee::pyobject_to_fee_model_any,
28};
29use nautilus_model::{
30    data::BarSpecification,
31    enums::{AccountType, BookType, OmsType, OtoTriggerMode},
32    identifiers::{ClientId, InstrumentId, TraderId},
33    types::Currency,
34};
35use nautilus_portfolio::config::PortfolioConfig;
36use nautilus_risk::engine::config::RiskEngineConfig;
37use pyo3::{Py, PyAny, Python};
38use rust_decimal::Decimal;
39use ustr::Ustr;
40
41use super::engine::{
42    pyobject_to_fill_model_any, pyobject_to_latency_model_any, pyobject_to_margin_model_any,
43    pyobject_to_simulation_module_any,
44};
45use crate::config::{
46    BacktestDataConfig, BacktestEngineConfig, BacktestRunConfig, BacktestVenueConfig,
47    NautilusDataType,
48};
49
50#[pyo3_stub_gen::derive::gen_stub_pymethods]
51#[pyo3::pymethods]
52impl BacktestEngineConfig {
53    /// Configuration for ``BacktestEngine`` instances.
54    #[new]
55    #[pyo3(signature = (
56        trader_id = None,
57        load_state = None,
58        save_state = None,
59        shutdown_on_error = None,
60        bypass_logging = None,
61        run_analysis = None,
62        timeout_connection = None,
63        timeout_reconciliation = None,
64        timeout_portfolio = None,
65        timeout_disconnection = None,
66        delay_post_stop = None,
67        timeout_shutdown = None,
68        logging = None,
69        instance_id = None,
70        cache = None,
71        msgbus = None,
72        data_engine = None,
73        risk_engine = None,
74        exec_engine = None,
75        portfolio = None,
76    ))]
77    #[expect(clippy::too_many_arguments)]
78    fn py_new(
79        trader_id: Option<TraderId>,
80        load_state: Option<bool>,
81        save_state: Option<bool>,
82        shutdown_on_error: Option<bool>,
83        bypass_logging: Option<bool>,
84        run_analysis: Option<bool>,
85        timeout_connection: Option<u64>,
86        timeout_reconciliation: Option<u64>,
87        timeout_portfolio: Option<u64>,
88        timeout_disconnection: Option<u64>,
89        delay_post_stop: Option<u64>,
90        timeout_shutdown: Option<u64>,
91        logging: Option<LoggerConfig>,
92        instance_id: Option<UUID4>,
93        cache: Option<CacheConfig>,
94        msgbus: Option<MessageBusConfig>,
95        data_engine: Option<DataEngineConfig>,
96        risk_engine: Option<RiskEngineConfig>,
97        exec_engine: Option<ExecutionEngineConfig>,
98        portfolio: Option<PortfolioConfig>,
99    ) -> Self {
100        let defaults = Self::default();
101        Self {
102            environment: Environment::Backtest,
103            trader_id: trader_id.unwrap_or_default(),
104            load_state: load_state.unwrap_or(defaults.load_state),
105            save_state: save_state.unwrap_or(defaults.save_state),
106            shutdown_on_error: shutdown_on_error.unwrap_or(defaults.shutdown_on_error),
107            bypass_logging: bypass_logging.unwrap_or(defaults.bypass_logging),
108            run_analysis: run_analysis.unwrap_or(defaults.run_analysis),
109            timeout_connection: Duration::from_secs(timeout_connection.unwrap_or(60)),
110            timeout_reconciliation: Duration::from_secs(timeout_reconciliation.unwrap_or(30)),
111            timeout_portfolio: Duration::from_secs(timeout_portfolio.unwrap_or(10)),
112            timeout_disconnection: Duration::from_secs(timeout_disconnection.unwrap_or(10)),
113            delay_post_stop: Duration::from_secs(delay_post_stop.unwrap_or(10)),
114            timeout_shutdown: Duration::from_secs(timeout_shutdown.unwrap_or(5)),
115            logging: logging.unwrap_or_default(),
116            instance_id,
117            cache,
118            msgbus,
119            data_engine,
120            risk_engine,
121            exec_engine,
122            portfolio,
123            streaming: None,
124        }
125    }
126
127    #[getter]
128    #[pyo3(name = "trader_id")]
129    fn py_trader_id(&self) -> TraderId {
130        self.trader_id
131    }
132
133    #[getter]
134    #[pyo3(name = "load_state")]
135    const fn py_load_state(&self) -> bool {
136        self.load_state
137    }
138
139    #[getter]
140    #[pyo3(name = "save_state")]
141    const fn py_save_state(&self) -> bool {
142        self.save_state
143    }
144
145    #[getter]
146    #[pyo3(name = "shutdown_on_error")]
147    const fn py_shutdown_on_error(&self) -> bool {
148        self.shutdown_on_error
149    }
150
151    #[getter]
152    #[pyo3(name = "bypass_logging")]
153    const fn py_bypass_logging(&self) -> bool {
154        self.bypass_logging
155    }
156
157    #[getter]
158    #[pyo3(name = "run_analysis")]
159    const fn py_run_analysis(&self) -> bool {
160        self.run_analysis
161    }
162
163    #[getter]
164    #[pyo3(name = "timeout_connection")]
165    fn py_timeout_connection(&self) -> f64 {
166        self.timeout_connection.as_secs_f64()
167    }
168
169    #[getter]
170    #[pyo3(name = "timeout_reconciliation")]
171    fn py_timeout_reconciliation(&self) -> f64 {
172        self.timeout_reconciliation.as_secs_f64()
173    }
174
175    #[getter]
176    #[pyo3(name = "timeout_portfolio")]
177    fn py_timeout_portfolio(&self) -> f64 {
178        self.timeout_portfolio.as_secs_f64()
179    }
180
181    #[getter]
182    #[pyo3(name = "timeout_disconnection")]
183    fn py_timeout_disconnection(&self) -> f64 {
184        self.timeout_disconnection.as_secs_f64()
185    }
186
187    #[getter]
188    #[pyo3(name = "delay_post_stop")]
189    fn py_delay_post_stop(&self) -> f64 {
190        self.delay_post_stop.as_secs_f64()
191    }
192
193    #[getter]
194    #[pyo3(name = "timeout_shutdown")]
195    fn py_timeout_shutdown(&self) -> f64 {
196        self.timeout_shutdown.as_secs_f64()
197    }
198
199    #[getter]
200    #[pyo3(name = "cache")]
201    fn py_cache(&self) -> Option<CacheConfig> {
202        self.cache.clone()
203    }
204
205    #[getter]
206    #[pyo3(name = "msgbus")]
207    fn py_msgbus(&self) -> Option<MessageBusConfig> {
208        self.msgbus.clone()
209    }
210
211    #[getter]
212    #[pyo3(name = "data_engine")]
213    fn py_data_engine(&self) -> Option<DataEngineConfig> {
214        self.data_engine.clone()
215    }
216
217    #[getter]
218    #[pyo3(name = "risk_engine")]
219    fn py_risk_engine(&self) -> Option<RiskEngineConfig> {
220        self.risk_engine.clone()
221    }
222
223    #[getter]
224    #[pyo3(name = "exec_engine")]
225    fn py_exec_engine(&self) -> Option<ExecutionEngineConfig> {
226        self.exec_engine.clone()
227    }
228
229    #[getter]
230    #[pyo3(name = "portfolio")]
231    const fn py_portfolio(&self) -> Option<PortfolioConfig> {
232        self.portfolio
233    }
234
235    fn __repr__(&self) -> String {
236        format!("{self:?}")
237    }
238}
239
240#[pyo3_stub_gen::derive::gen_stub_pymethods]
241#[pyo3::pymethods]
242impl BacktestVenueConfig {
243    /// Represents a venue configuration for one specific backtest engine.
244    #[new]
245    #[pyo3(signature = (
246        name,
247        oms_type,
248        account_type,
249        book_type,
250        starting_balances,
251        routing = None,
252        frozen_account = None,
253        reject_stop_orders = None,
254        support_gtd_orders = None,
255        support_contingent_orders = None,
256        use_position_ids = None,
257        use_random_ids = None,
258        use_reduce_only = None,
259        bar_execution = None,
260        bar_adaptive_high_low_ordering = None,
261        trade_execution = None,
262        use_market_order_acks = None,
263        liquidity_consumption = None,
264        allow_cash_borrowing = None,
265        queue_position = None,
266        oto_trigger_mode = None,
267        base_currency = None,
268        default_leverage = None,
269        leverages = None,
270        margin_model = None,
271        modules = None,
272        fill_model = None,
273        latency_model = None,
274        fee_model = None,
275        price_protection_points = None,
276        settlement_prices = None,
277        liquidation_enabled = None,
278        liquidation_trigger_ratio = None,
279        liquidation_cancel_open_orders = None,
280    ))]
281    #[expect(clippy::too_many_arguments)]
282    fn py_new(
283        name: &str,
284        oms_type: OmsType,
285        account_type: AccountType,
286        book_type: BookType,
287        starting_balances: Vec<String>,
288        routing: Option<bool>,
289        frozen_account: Option<bool>,
290        reject_stop_orders: Option<bool>,
291        support_gtd_orders: Option<bool>,
292        support_contingent_orders: Option<bool>,
293        use_position_ids: Option<bool>,
294        use_random_ids: Option<bool>,
295        use_reduce_only: Option<bool>,
296        bar_execution: Option<bool>,
297        bar_adaptive_high_low_ordering: Option<bool>,
298        trade_execution: Option<bool>,
299        use_market_order_acks: Option<bool>,
300        liquidity_consumption: Option<bool>,
301        allow_cash_borrowing: Option<bool>,
302        queue_position: Option<bool>,
303        oto_trigger_mode: Option<OtoTriggerMode>,
304        base_currency: Option<Currency>,
305        default_leverage: Option<Decimal>,
306        leverages: Option<HashMap<InstrumentId, Decimal>>,
307        margin_model: Option<Py<PyAny>>,
308        modules: Option<Vec<Py<PyAny>>>,
309        fill_model: Option<Py<PyAny>>,
310        latency_model: Option<Py<PyAny>>,
311        fee_model: Option<Py<PyAny>>,
312        price_protection_points: Option<u32>,
313        settlement_prices: Option<HashMap<InstrumentId, f64>>,
314        liquidation_enabled: Option<bool>,
315        liquidation_trigger_ratio: Option<f64>,
316        liquidation_cancel_open_orders: Option<bool>,
317    ) -> pyo3::PyResult<Self> {
318        let margin_model = margin_model
319            .map(|obj| Python::attach(|py| pyobject_to_margin_model_any(py, obj.bind(py))))
320            .transpose()?;
321        let modules = modules
322            .map(|objs| {
323                objs.into_iter()
324                    .map(|obj| {
325                        Python::attach(|py| pyobject_to_simulation_module_any(py, obj.bind(py)))
326                    })
327                    .collect::<pyo3::PyResult<Vec<_>>>()
328            })
329            .transpose()?
330            .unwrap_or_default();
331        let fill_model = fill_model
332            .map(|obj| Python::attach(|py| pyobject_to_fill_model_any(py, obj.bind(py))))
333            .transpose()?;
334        let latency_model = latency_model
335            .map(|obj| Python::attach(|py| pyobject_to_latency_model_any(py, obj.bind(py))))
336            .transpose()?;
337        let fee_model = fee_model
338            .map(|obj| Python::attach(|py| pyobject_to_fee_model_any(obj.bind(py))))
339            .transpose()?;
340
341        Self::builder()
342            .name(Ustr::from(name))
343            .oms_type(oms_type)
344            .account_type(account_type)
345            .book_type(book_type)
346            .starting_balances(starting_balances)
347            .maybe_routing(routing)
348            .maybe_frozen_account(frozen_account)
349            .maybe_reject_stop_orders(reject_stop_orders)
350            .maybe_support_gtd_orders(support_gtd_orders)
351            .maybe_support_contingent_orders(support_contingent_orders)
352            .maybe_use_position_ids(use_position_ids)
353            .maybe_use_random_ids(use_random_ids)
354            .maybe_use_reduce_only(use_reduce_only)
355            .maybe_bar_execution(bar_execution)
356            .maybe_bar_adaptive_high_low_ordering(bar_adaptive_high_low_ordering)
357            .maybe_trade_execution(trade_execution)
358            .maybe_use_market_order_acks(use_market_order_acks)
359            .maybe_liquidity_consumption(liquidity_consumption)
360            .maybe_allow_cash_borrowing(allow_cash_borrowing)
361            .maybe_queue_position(queue_position)
362            .maybe_oto_trigger_mode(oto_trigger_mode)
363            .maybe_base_currency(base_currency)
364            .maybe_default_leverage(default_leverage)
365            .maybe_leverages(leverages.map(|m| m.into_iter().collect()))
366            .maybe_margin_model(margin_model)
367            .modules(modules)
368            .maybe_fill_model(fill_model)
369            .maybe_latency_model(latency_model)
370            .maybe_fee_model(fee_model)
371            .maybe_price_protection_points(price_protection_points)
372            .maybe_settlement_prices(settlement_prices.map(|m| m.into_iter().collect()))
373            .maybe_liquidation_enabled(liquidation_enabled)
374            .maybe_liquidation_trigger_ratio(liquidation_trigger_ratio)
375            .maybe_liquidation_cancel_open_orders(liquidation_cancel_open_orders)
376            .build()
377            .map_err(config_error_to_pyvalue_err)
378    }
379
380    #[getter]
381    #[pyo3(name = "name")]
382    fn py_name(&self) -> &str {
383        self.name().as_str()
384    }
385
386    #[getter]
387    #[pyo3(name = "oms_type")]
388    fn py_oms_type(&self) -> OmsType {
389        self.oms_type()
390    }
391
392    #[getter]
393    #[pyo3(name = "account_type")]
394    fn py_account_type(&self) -> AccountType {
395        self.account_type()
396    }
397
398    #[getter]
399    #[pyo3(name = "book_type")]
400    fn py_book_type(&self) -> BookType {
401        self.book_type()
402    }
403
404    #[getter]
405    #[pyo3(name = "starting_balances")]
406    fn py_starting_balances(&self) -> Vec<String> {
407        self.starting_balances().to_vec()
408    }
409
410    #[getter]
411    #[pyo3(name = "bar_execution")]
412    fn py_bar_execution(&self) -> bool {
413        self.bar_execution()
414    }
415
416    #[getter]
417    #[pyo3(name = "trade_execution")]
418    fn py_trade_execution(&self) -> bool {
419        self.trade_execution()
420    }
421
422    #[getter]
423    #[pyo3(name = "liquidation_enabled")]
424    fn py_liquidation_enabled(&self) -> bool {
425        self.liquidation_enabled()
426    }
427
428    #[getter]
429    #[pyo3(name = "liquidation_trigger_ratio")]
430    fn py_liquidation_trigger_ratio(&self) -> f64 {
431        self.liquidation_trigger_ratio()
432    }
433
434    #[getter]
435    #[pyo3(name = "liquidation_cancel_open_orders")]
436    fn py_liquidation_cancel_open_orders(&self) -> bool {
437        self.liquidation_cancel_open_orders()
438    }
439
440    fn __repr__(&self) -> String {
441        format!("{self:?}")
442    }
443}
444
445#[pyo3_stub_gen::derive::gen_stub_pymethods]
446#[pyo3::pymethods]
447impl BacktestDataConfig {
448    /// Represents the data configuration for one specific backtest run.
449    #[new]
450    #[pyo3(signature = (
451        data_type,
452        catalog_path,
453        catalog_fs_protocol = None,
454        catalog_fs_storage_options = None,
455        catalog_fs_rust_storage_options = None,
456        instrument_id = None,
457        instrument_ids = None,
458        start_time = None,
459        end_time = None,
460        filter_expr = None,
461        client_id = None,
462        metadata = None,
463        bar_spec = None,
464        bar_types = None,
465        optimize_file_loading = None,
466    ))]
467    #[expect(clippy::too_many_arguments)]
468    fn py_new(
469        data_type: &str,
470        catalog_path: String,
471        catalog_fs_protocol: Option<String>,
472        catalog_fs_storage_options: Option<HashMap<String, String>>,
473        catalog_fs_rust_storage_options: Option<HashMap<String, String>>,
474        instrument_id: Option<InstrumentId>,
475        instrument_ids: Option<Vec<InstrumentId>>,
476        start_time: Option<u64>,
477        end_time: Option<u64>,
478        filter_expr: Option<String>,
479        client_id: Option<ClientId>,
480        metadata: Option<HashMap<String, String>>,
481        bar_spec: Option<BarSpecification>,
482        bar_types: Option<Vec<String>>,
483        optimize_file_loading: Option<bool>,
484    ) -> pyo3::PyResult<Self> {
485        let data_type = data_type
486            .parse::<NautilusDataType>()
487            .map_err(to_pyvalue_err)?;
488        Self::builder()
489            .data_type(data_type)
490            .catalog_path(catalog_path)
491            .maybe_catalog_fs_protocol(catalog_fs_protocol)
492            .maybe_catalog_fs_storage_options(
493                catalog_fs_storage_options.map(|m| m.into_iter().collect()),
494            )
495            .maybe_catalog_fs_rust_storage_options(
496                catalog_fs_rust_storage_options.map(|m| m.into_iter().collect()),
497            )
498            .maybe_instrument_id(instrument_id)
499            .maybe_instrument_ids(instrument_ids)
500            .maybe_start_time(start_time.map(UnixNanos::from))
501            .maybe_end_time(end_time.map(UnixNanos::from))
502            .maybe_filter_expr(filter_expr)
503            .maybe_client_id(client_id)
504            .maybe_metadata(metadata.map(|m| m.into_iter().collect()))
505            .maybe_bar_spec(bar_spec)
506            .maybe_bar_types(bar_types)
507            .maybe_optimize_file_loading(optimize_file_loading)
508            .build()
509            .map_err(config_error_to_pyvalue_err)
510    }
511
512    #[getter]
513    #[pyo3(name = "data_type")]
514    fn py_data_type(&self) -> String {
515        self.data_type().to_string()
516    }
517
518    #[getter]
519    #[pyo3(name = "catalog_path")]
520    fn py_catalog_path(&self) -> &str {
521        self.catalog_path()
522    }
523
524    #[getter]
525    #[pyo3(name = "instrument_id")]
526    fn py_instrument_id(&self) -> Option<InstrumentId> {
527        self.instrument_id()
528    }
529
530    fn __repr__(&self) -> String {
531        format!("{self:?}")
532    }
533}
534
535#[pyo3_stub_gen::derive::gen_stub_pymethods]
536#[pyo3::pymethods]
537impl BacktestRunConfig {
538    /// Represents the configuration for one specific backtest run.
539    /// This includes a backtest engine with its actors and strategies, with the external inputs of venues and data.
540    #[new]
541    #[pyo3(signature = (
542        venues,
543        data,
544        engine = None,
545        id = None,
546        chunk_size = None,
547        raise_exception = None,
548        dispose_on_completion = None,
549        start = None,
550        end = None,
551    ))]
552    #[expect(clippy::too_many_arguments)]
553    fn py_new(
554        venues: Vec<BacktestVenueConfig>,
555        data: Vec<BacktestDataConfig>,
556        engine: Option<BacktestEngineConfig>,
557        id: Option<String>,
558        chunk_size: Option<usize>,
559        raise_exception: Option<bool>,
560        dispose_on_completion: Option<bool>,
561        start: Option<u64>,
562        end: Option<u64>,
563    ) -> pyo3::PyResult<Self> {
564        Self::builder()
565            .venues(venues)
566            .data(data)
567            .maybe_engine(engine)
568            .maybe_id(id)
569            .maybe_chunk_size(chunk_size)
570            .maybe_raise_exception(raise_exception)
571            .maybe_dispose_on_completion(dispose_on_completion)
572            .maybe_start(start.map(UnixNanos::from))
573            .maybe_end(end.map(UnixNanos::from))
574            .build()
575            .map_err(config_error_to_pyvalue_err)
576    }
577
578    #[getter]
579    #[pyo3(name = "id")]
580    fn py_id(&self) -> &str {
581        self.id()
582    }
583
584    fn __repr__(&self) -> String {
585        format!("{self:?}")
586    }
587}