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