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