1use 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 #[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 #[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 #[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 #[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}