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