1use std::collections::HashMap;
19
20use ahash::AHashMap;
21use nautilus_common::{
22 actor::data_actor::ImportableActorConfig,
23 python::{actor::PyDataActor, cache::PyCache},
24};
25use nautilus_core::{
26 UUID4, UnixNanos,
27 python::{to_pyruntime_err, to_pytype_err, to_pyvalue_err},
28};
29use nautilus_execution::models::{
30 fee::{FeeModelAny, FixedFeeModel, MakerTakerFeeModel, PerContractFeeModel},
31 fill::{
32 BestPriceFillModel, CompetitionAwareFillModel, DefaultFillModel, FillModelAny,
33 LimitOrderPartialFillModel, MarketHoursFillModel, OneTickSlippageFillModel,
34 ProbabilisticFillModel, SizeAwareFillModel, ThreeTierFillModel, TwoTierFillModel,
35 VolumeSensitiveFillModel,
36 },
37 latency::{LatencyModelAny, StaticLatencyModel},
38};
39use nautilus_model::{
40 accounts::margin_model::{LeveragedMarginModel, MarginModelAny, StandardMarginModel},
41 data::{
42 Bar, Data, IndexPriceUpdate, InstrumentClose, InstrumentStatus, MarkPriceUpdate,
43 OrderBookDelta, OrderBookDeltas, OrderBookDeltas_API, OrderBookDepth10, QuoteTick,
44 TradeTick,
45 },
46 enums::{AccountType, BookType, OmsType, OtoTriggerMode},
47 identifiers::{ActorId, ClientId, ComponentId, InstrumentId, StrategyId, TraderId, Venue},
48 python::instruments::pyobject_to_instrument_any,
49 types::{Currency, Money, Price},
50};
51use nautilus_trading::{
52 ImportableStrategyConfig,
53 python::strategy::{PyStrategy, PyStrategyInner},
54};
55use pyo3::prelude::*;
56use rust_decimal::Decimal;
57
58use super::node::create_config_instance;
59use crate::{
60 config::{BacktestEngineConfig, SimulatedVenueConfig},
61 engine::BacktestEngine,
62 modules::{FXRolloverInterestModule, SimulationModuleAny},
63 result::BacktestResult,
64};
65
66#[pyo3::pyclass(
71 module = "nautilus_trader.core.nautilus_pyo3.backtest",
72 name = "BacktestEngine",
73 unsendable
74)]
75#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.backtest")]
76#[derive(Debug)]
77pub struct PyBacktestEngine(BacktestEngine);
78
79#[pyo3_stub_gen::derive::gen_stub_pymethods]
80#[pymethods]
81impl PyBacktestEngine {
82 #[new]
83 fn py_new(config: BacktestEngineConfig) -> PyResult<Self> {
84 let engine = BacktestEngine::new(config).map_err(to_pyruntime_err)?;
85 Ok(Self(engine))
86 }
87
88 #[pyo3(
90 name = "add_venue",
91 signature = (
92 venue,
93 oms_type,
94 account_type,
95 starting_balances,
96 base_currency = None,
97 default_leverage = None,
98 leverages = None,
99 margin_model = None,
100 fill_model = None,
101 fee_model = None,
102 latency_model = None,
103 modules = None,
104 book_type = BookType::L1_MBP,
105 routing = false,
106 reject_stop_orders = true,
107 support_gtd_orders = true,
108 support_contingent_orders = true,
109 use_position_ids = true,
110 use_random_ids = false,
111 use_reduce_only = true,
112 use_message_queue = true,
113 use_market_order_acks = false,
114 bar_execution = true,
115 bar_adaptive_high_low_ordering = false,
116 trade_execution = true,
117 liquidity_consumption = false,
118 queue_position = false,
119 allow_cash_borrowing = false,
120 frozen_account = false,
121 oto_trigger_mode = OtoTriggerMode::Partial,
122 price_protection_points = None,
123 settlement_prices = None,
124 )
125 )]
126 #[expect(clippy::too_many_arguments)]
127 fn py_add_venue(
128 &mut self,
129 venue: Venue,
130 oms_type: OmsType,
131 account_type: AccountType,
132 starting_balances: Vec<Money>,
133 base_currency: Option<Currency>,
134 default_leverage: Option<Decimal>,
135 leverages: Option<HashMap<InstrumentId, Decimal>>,
136 margin_model: Option<Py<PyAny>>,
137 fill_model: Option<Py<PyAny>>,
138 fee_model: Option<Py<PyAny>>,
139 latency_model: Option<Py<PyAny>>,
140 modules: Option<Vec<Py<PyAny>>>,
141 book_type: BookType,
142 routing: bool,
143 reject_stop_orders: bool,
144 support_gtd_orders: bool,
145 support_contingent_orders: bool,
146 use_position_ids: bool,
147 use_random_ids: bool,
148 use_reduce_only: bool,
149 use_message_queue: bool,
150 use_market_order_acks: bool,
151 bar_execution: bool,
152 bar_adaptive_high_low_ordering: bool,
153 trade_execution: bool,
154 liquidity_consumption: bool,
155 queue_position: bool,
156 allow_cash_borrowing: bool,
157 frozen_account: bool,
158 oto_trigger_mode: OtoTriggerMode,
159 price_protection_points: Option<u32>,
160 settlement_prices: Option<HashMap<InstrumentId, Price>>,
161 ) -> PyResult<()> {
162 let leverages: AHashMap<InstrumentId, Decimal> = leverages
163 .map(|m| m.into_iter().collect())
164 .unwrap_or_default();
165 let settlement_prices: AHashMap<InstrumentId, Price> = settlement_prices
166 .map(|m| m.into_iter().collect())
167 .unwrap_or_default();
168 let margin_model = margin_model
169 .map(|obj| Python::attach(|py| pyobject_to_margin_model_any(py, obj.bind(py))))
170 .transpose()?;
171 let fill_model = fill_model
172 .map(|obj| Python::attach(|py| pyobject_to_fill_model_any(py, obj.bind(py))))
173 .transpose()?
174 .unwrap_or_default();
175 let fee_model = fee_model
176 .map(|obj| Python::attach(|py| pyobject_to_fee_model_any(py, obj.bind(py))))
177 .transpose()?
178 .unwrap_or_default();
179 let latency_model = latency_model
180 .map(|obj| Python::attach(|py| pyobject_to_latency_model_any(py, obj.bind(py))))
181 .transpose()?
182 .map(Into::into);
183 let modules = modules
184 .map(|objs| {
185 objs.into_iter()
186 .map(|obj| {
187 Python::attach(|py| pyobject_to_simulation_module_any(py, obj.bind(py)))
188 })
189 .collect::<PyResult<Vec<_>>>()
190 })
191 .transpose()?
192 .unwrap_or_default()
193 .into_iter()
194 .map(Into::into)
195 .collect();
196
197 let sim_config = SimulatedVenueConfig::builder()
198 .venue(venue)
199 .oms_type(oms_type)
200 .account_type(account_type)
201 .book_type(book_type)
202 .starting_balances(starting_balances)
203 .maybe_base_currency(base_currency)
204 .maybe_default_leverage(default_leverage)
205 .leverages(leverages)
206 .maybe_margin_model(margin_model)
207 .modules(modules)
208 .fill_model(fill_model)
209 .fee_model(fee_model)
210 .maybe_latency_model(latency_model)
211 .routing(routing)
212 .reject_stop_orders(reject_stop_orders)
213 .support_gtd_orders(support_gtd_orders)
214 .support_contingent_orders(support_contingent_orders)
215 .use_position_ids(use_position_ids)
216 .use_random_ids(use_random_ids)
217 .use_reduce_only(use_reduce_only)
218 .use_message_queue(use_message_queue)
219 .use_market_order_acks(use_market_order_acks)
220 .bar_execution(bar_execution)
221 .bar_adaptive_high_low_ordering(bar_adaptive_high_low_ordering)
222 .trade_execution(trade_execution)
223 .liquidity_consumption(liquidity_consumption)
224 .allow_cash_borrowing(allow_cash_borrowing)
225 .frozen_account(frozen_account)
226 .queue_position(queue_position)
227 .oto_full_trigger(oto_trigger_mode == OtoTriggerMode::Full)
228 .maybe_price_protection_points(price_protection_points)
229 .build();
230
231 self.0.add_venue(sim_config).map_err(to_pyruntime_err)?;
232
233 for (instrument_id, price) in settlement_prices {
234 self.0
235 .set_settlement_price(venue, instrument_id, price)
236 .map_err(to_pyruntime_err)?;
237 }
238
239 Ok(())
240 }
241
242 #[pyo3(name = "change_fill_model")]
244 #[expect(clippy::needless_pass_by_value)]
245 fn py_change_fill_model(
246 &mut self,
247 py: Python,
248 venue: Venue,
249 fill_model: Py<PyAny>,
250 ) -> PyResult<()> {
251 let fill_model = pyobject_to_fill_model_any(py, fill_model.bind(py))?;
252 self.0.change_fill_model(venue, fill_model);
253 Ok(())
254 }
255
256 #[pyo3(
258 name = "add_data",
259 signature = (data, client_id=None, validate=true, sort=true)
260 )]
261 fn py_add_data(
262 &mut self,
263 py: Python,
264 data: Vec<Py<PyAny>>,
265 client_id: Option<ClientId>,
266 validate: bool,
267 sort: bool,
268 ) -> PyResult<()> {
269 let rust_data: Vec<Data> = data
270 .into_iter()
271 .map(|obj| pyobject_to_data(py, obj.bind(py)))
272 .collect::<PyResult<_>>()?;
273 self.0
274 .add_data(rust_data, client_id, validate, sort)
275 .map_err(to_pyruntime_err)
276 }
277
278 #[pyo3(name = "add_instrument")]
280 fn py_add_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
281 let instrument_any = pyobject_to_instrument_any(py, instrument)?;
282 self.0
283 .add_instrument(&instrument_any)
284 .map_err(to_pyruntime_err)
285 }
286
287 #[allow(
289 unsafe_code,
290 reason = "Required for Python actor component registration"
291 )]
292 #[pyo3(name = "add_actor_from_config")]
293 #[expect(clippy::needless_pass_by_value)]
294 fn py_add_actor_from_config(
295 &mut self,
296 _py: Python,
297 config: ImportableActorConfig,
298 ) -> PyResult<()> {
299 log::debug!("`add_actor_from_config` with: {config:?}");
300
301 let parts: Vec<&str> = config.actor_path.split(':').collect();
302 if parts.len() != 2 {
303 return Err(to_pyvalue_err(
304 "actor_path must be in format 'module.path:ClassName'",
305 ));
306 }
307 let (module_name, class_name) = (parts[0], parts[1]);
308
309 log::info!("Importing actor from module: {module_name} class: {class_name}");
310
311 let (python_actor, actor_id) =
312 Python::attach(|py| -> anyhow::Result<(Py<PyAny>, ActorId)> {
313 let actor_module = py
314 .import(module_name)
315 .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
316 let actor_class = actor_module
317 .getattr(class_name)
318 .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
319
320 let config_instance =
321 create_config_instance(py, &config.config_path, &config.config)?;
322
323 let python_actor = if let Some(config_obj) = config_instance.clone() {
324 actor_class.call1((config_obj,))?
325 } else {
326 actor_class.call0()?
327 };
328
329 let mut py_data_actor_ref = python_actor
330 .extract::<PyRefMut<PyDataActor>>()
331 .map_err(Into::<PyErr>::into)
332 .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
333
334 if let Some(config_obj) = config_instance.as_ref() {
335 if let Ok(actor_id) = config_obj.getattr("actor_id")
336 && !actor_id.is_none()
337 {
338 let actor_id_val = if let Ok(actor_id_val) = actor_id.extract::<ActorId>() {
339 actor_id_val
340 } else if let Ok(actor_id_str) = actor_id.extract::<String>() {
341 ActorId::new_checked(&actor_id_str)?
342 } else {
343 anyhow::bail!("Invalid `actor_id` type");
344 };
345 py_data_actor_ref.set_actor_id(actor_id_val);
346 }
347
348 if let Ok(log_events) = config_obj.getattr("log_events")
349 && let Ok(log_events_val) = log_events.extract::<bool>()
350 {
351 py_data_actor_ref.set_log_events(log_events_val);
352 }
353
354 if let Ok(log_commands) = config_obj.getattr("log_commands")
355 && let Ok(log_commands_val) = log_commands.extract::<bool>()
356 {
357 py_data_actor_ref.set_log_commands(log_commands_val);
358 }
359 }
360
361 py_data_actor_ref.set_python_instance(python_actor.clone().unbind());
362 let actor_id = py_data_actor_ref.actor_id();
363
364 Ok((python_actor.unbind(), actor_id))
365 })
366 .map_err(to_pyruntime_err)?;
367
368 if self
369 .0
370 .kernel()
371 .trader
372 .borrow()
373 .actor_ids()
374 .contains(&actor_id)
375 {
376 return Err(to_pyruntime_err(format!(
377 "Actor '{actor_id}' is already registered"
378 )));
379 }
380
381 let trader_id = self.0.kernel().config.trader_id();
382 let cache = self.0.kernel().cache.clone();
383 let component_id = ComponentId::new(actor_id.inner().as_str());
384 let clock = self
385 .0
386 .kernel_mut()
387 .trader
388 .borrow_mut()
389 .create_component_clock(component_id);
390
391 Python::attach(|py| -> anyhow::Result<()> {
392 let py_actor = python_actor.bind(py);
393 let mut py_data_actor_ref = py_actor
394 .extract::<PyRefMut<PyDataActor>>()
395 .map_err(Into::<PyErr>::into)
396 .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
397
398 py_data_actor_ref
399 .register(trader_id, clock, cache)
400 .map_err(|e| anyhow::anyhow!("Failed to register PyDataActor: {e}"))?;
401
402 Ok(())
403 })
404 .map_err(to_pyruntime_err)?;
405
406 Python::attach(|py| -> anyhow::Result<()> {
407 let py_actor = python_actor.bind(py);
408 let py_data_actor_ref = py_actor
409 .cast::<PyDataActor>()
410 .map_err(|e| anyhow::anyhow!("Failed to downcast to PyDataActor: {e}"))?;
411 py_data_actor_ref.borrow().register_in_global_registries();
412 Ok(())
413 })
414 .map_err(to_pyruntime_err)?;
415
416 self.0
417 .kernel_mut()
418 .trader
419 .borrow_mut()
420 .add_actor_id_for_lifecycle(actor_id)
421 .map_err(to_pyruntime_err)?;
422
423 log::info!("Registered Python actor {actor_id}");
424 Ok(())
425 }
426
427 #[allow(
429 unsafe_code,
430 reason = "Required for Python strategy component registration"
431 )]
432 #[pyo3(name = "add_strategy_from_config")]
433 #[expect(clippy::needless_pass_by_value)]
434 fn py_add_strategy_from_config(
435 &mut self,
436 _py: Python,
437 config: ImportableStrategyConfig,
438 ) -> PyResult<()> {
439 log::debug!("`add_strategy_from_config` with: {config:?}");
440
441 let parts: Vec<&str> = config.strategy_path.split(':').collect();
442 if parts.len() != 2 {
443 return Err(to_pyvalue_err(
444 "strategy_path must be in format 'module.path:ClassName'",
445 ));
446 }
447 let (module_name, class_name) = (parts[0], parts[1]);
448
449 log::info!("Importing strategy from module: {module_name} class: {class_name}");
450
451 let (python_strategy, strategy_id) =
452 Python::attach(|py| -> anyhow::Result<(Py<PyAny>, StrategyId)> {
453 let strategy_module = py
454 .import(module_name)
455 .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
456 let strategy_class = strategy_module
457 .getattr(class_name)
458 .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
459
460 let config_instance =
461 create_config_instance(py, &config.config_path, &config.config)?;
462
463 let python_strategy = if let Some(config_obj) = config_instance.clone() {
464 strategy_class.call1((config_obj,))?
465 } else {
466 strategy_class.call0()?
467 };
468
469 let mut py_strategy_ref = python_strategy
470 .extract::<PyRefMut<PyStrategy>>()
471 .map_err(Into::<PyErr>::into)
472 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
473
474 if let Some(config_obj) = config_instance.as_ref() {
475 if let Ok(strategy_id) = config_obj.getattr("strategy_id")
476 && !strategy_id.is_none()
477 {
478 let strategy_id_val = if let Ok(sid) = strategy_id.extract::<StrategyId>() {
479 sid
480 } else if let Ok(sid_str) = strategy_id.extract::<String>() {
481 StrategyId::new_checked(&sid_str)?
482 } else {
483 anyhow::bail!("Invalid `strategy_id` type");
484 };
485 py_strategy_ref.set_strategy_id(strategy_id_val);
486 }
487
488 if let Ok(log_events) = config_obj.getattr("log_events")
489 && let Ok(log_events_val) = log_events.extract::<bool>()
490 {
491 py_strategy_ref.set_log_events(log_events_val);
492 }
493
494 if let Ok(log_commands) = config_obj.getattr("log_commands")
495 && let Ok(log_commands_val) = log_commands.extract::<bool>()
496 {
497 py_strategy_ref.set_log_commands(log_commands_val);
498 }
499 }
500
501 py_strategy_ref.set_python_instance(python_strategy.clone().unbind());
502 let strategy_id = py_strategy_ref.strategy_id();
503
504 Ok((python_strategy.unbind(), strategy_id))
505 })
506 .map_err(to_pyruntime_err)?;
507
508 if self
509 .0
510 .kernel()
511 .trader
512 .borrow()
513 .strategy_ids()
514 .contains(&strategy_id)
515 {
516 return Err(to_pyruntime_err(format!(
517 "Strategy '{strategy_id}' is already registered"
518 )));
519 }
520
521 let trader_id = self.0.kernel().config.trader_id();
522 let cache = self.0.kernel().cache.clone();
523 let portfolio = self.0.kernel().portfolio.clone();
524 let component_id = ComponentId::new(strategy_id.inner().as_str());
525 let clock = self
526 .0
527 .kernel_mut()
528 .trader
529 .borrow_mut()
530 .create_component_clock(component_id);
531
532 Python::attach(|py| -> anyhow::Result<()> {
533 let py_strategy = python_strategy.bind(py);
534 let mut py_strategy_ref = py_strategy
535 .extract::<PyRefMut<PyStrategy>>()
536 .map_err(Into::<PyErr>::into)
537 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
538
539 py_strategy_ref
540 .register(trader_id, clock, cache, portfolio)
541 .map_err(|e| anyhow::anyhow!("Failed to register PyStrategy: {e}"))?;
542
543 Ok(())
544 })
545 .map_err(to_pyruntime_err)?;
546
547 Python::attach(|py| -> anyhow::Result<()> {
548 let py_strategy = python_strategy.bind(py);
549 let py_strategy_ref = py_strategy
550 .cast::<PyStrategy>()
551 .map_err(|e| anyhow::anyhow!("Failed to downcast to PyStrategy: {e}"))?;
552 py_strategy_ref.borrow().register_in_global_registries();
553 Ok(())
554 })
555 .map_err(to_pyruntime_err)?;
556
557 self.0
558 .kernel_mut()
559 .trader
560 .borrow_mut()
561 .add_strategy_id_with_subscriptions::<PyStrategyInner>(strategy_id)
562 .map_err(to_pyruntime_err)?;
563
564 log::info!("Registered Python strategy {strategy_id}");
565 Ok(())
566 }
567
568 #[cfg(feature = "examples")]
573 #[pyo3(name = "add_native_strategy")]
574 fn py_add_native_strategy(&mut self, config: &Bound<'_, PyAny>) -> PyResult<()> {
575 use nautilus_trading::examples::strategies::{
576 DeltaNeutralVol, DeltaNeutralVolConfig, EmaCross, EmaCrossConfig, GridMarketMaker,
577 GridMarketMakerConfig, HurstVpinDirectional, HurstVpinDirectionalConfig,
578 };
579
580 if let Ok(config) = config.extract::<EmaCrossConfig>() {
581 self.0
582 .add_strategy(EmaCross::from_config(config))
583 .map_err(to_pyruntime_err)
584 } else if let Ok(config) = config.extract::<GridMarketMakerConfig>() {
585 self.0
586 .add_strategy(GridMarketMaker::new(config))
587 .map_err(to_pyruntime_err)
588 } else if let Ok(config) = config.extract::<DeltaNeutralVolConfig>() {
589 self.0
590 .add_strategy(DeltaNeutralVol::new(config))
591 .map_err(to_pyruntime_err)
592 } else if let Ok(config) = config.extract::<HurstVpinDirectionalConfig>() {
593 self.0
594 .add_strategy(HurstVpinDirectional::new(config))
595 .map_err(to_pyruntime_err)
596 } else {
597 let type_name = config.get_type().name()?;
598 Err(to_pytype_err(format!(
599 "Unsupported native strategy config type: {type_name}",
600 )))
601 }
602 }
603
604 #[cfg(feature = "examples")]
609 #[pyo3(name = "add_native_actor")]
610 fn py_add_native_actor(&mut self, config: &Bound<'_, PyAny>) -> PyResult<()> {
611 use nautilus_trading::examples::actors::{BookImbalanceActor, BookImbalanceActorConfig};
612
613 if let Ok(config) = config.extract::<BookImbalanceActorConfig>() {
614 self.0
615 .add_actor(BookImbalanceActor::from_config(config))
616 .map_err(to_pyruntime_err)
617 } else {
618 let type_name = config.get_type().name()?;
619 Err(to_pytype_err(format!(
620 "Unsupported native actor config type: {type_name}",
621 )))
622 }
623 }
624
625 #[pyo3(
627 name = "run",
628 signature = (start=None, end=None, run_config_id=None, streaming=false)
629 )]
630 fn py_run(
631 &mut self,
632 start: Option<u64>,
633 end: Option<u64>,
634 run_config_id: Option<String>,
635 streaming: bool,
636 ) -> PyResult<()> {
637 self.0
638 .run(
639 start.map(UnixNanos::from),
640 end.map(UnixNanos::from),
641 run_config_id,
642 streaming,
643 )
644 .map_err(to_pyruntime_err)
645 }
646
647 #[pyo3(name = "end")]
649 fn py_end(&mut self) {
650 self.0.end();
651 }
652
653 #[pyo3(name = "reset")]
655 fn py_reset(&mut self) {
656 self.0.reset();
657 }
658
659 #[pyo3(name = "dispose")]
661 fn py_dispose(&mut self) {
662 self.0.dispose();
663 }
664
665 #[pyo3(name = "get_result")]
667 fn py_get_result(&self) -> BacktestResult {
668 self.0.get_result()
669 }
670
671 #[pyo3(name = "clear_data")]
673 fn py_clear_data(&mut self) {
674 self.0.clear_data();
675 }
676
677 #[pyo3(name = "clear_actors")]
679 fn py_clear_actors(&mut self) -> PyResult<()> {
680 self.0.clear_actors().map_err(to_pyruntime_err)
681 }
682
683 #[pyo3(name = "clear_strategies")]
685 fn py_clear_strategies(&mut self) -> PyResult<()> {
686 self.0.clear_strategies().map_err(to_pyruntime_err)
687 }
688
689 #[pyo3(name = "clear_exec_algorithms")]
691 fn py_clear_exec_algorithms(&mut self) -> PyResult<()> {
692 self.0.clear_exec_algorithms().map_err(to_pyruntime_err)
693 }
694
695 #[pyo3(name = "add_actors_from_configs")]
697 fn py_add_actors_from_configs(
698 &mut self,
699 py: Python,
700 configs: Vec<ImportableActorConfig>,
701 ) -> PyResult<()> {
702 for config in configs {
703 self.py_add_actor_from_config(py, config)?;
704 }
705 Ok(())
706 }
707
708 #[pyo3(name = "add_strategies_from_configs")]
710 fn py_add_strategies_from_configs(
711 &mut self,
712 py: Python,
713 configs: Vec<ImportableStrategyConfig>,
714 ) -> PyResult<()> {
715 for config in configs {
716 self.py_add_strategy_from_config(py, config)?;
717 }
718 Ok(())
719 }
720
721 #[pyo3(name = "sort_data")]
723 fn py_sort_data(&mut self) {
724 self.0.sort_data();
725 }
726
727 #[getter]
729 #[pyo3(name = "trader_id")]
730 fn py_trader_id(&self) -> TraderId {
731 self.0.trader_id()
732 }
733
734 #[getter]
736 #[pyo3(name = "machine_id")]
737 fn py_machine_id(&self) -> String {
738 self.0.machine_id().to_string()
739 }
740
741 #[getter]
743 #[pyo3(name = "instance_id")]
744 fn py_instance_id(&self) -> UUID4 {
745 self.0.instance_id()
746 }
747
748 #[getter]
750 #[pyo3(name = "iteration")]
751 fn py_iteration(&self) -> usize {
752 self.0.iteration()
753 }
754
755 #[getter]
757 #[pyo3(name = "run_config_id")]
758 fn py_run_config_id(&self) -> Option<String> {
759 self.0.run_config_id().map(str::to_string)
760 }
761
762 #[getter]
764 #[pyo3(name = "run_id")]
765 fn py_run_id(&self) -> Option<UUID4> {
766 self.0.run_id()
767 }
768
769 #[getter]
771 #[pyo3(name = "run_started")]
772 fn py_run_started(&self) -> Option<u64> {
773 self.0.run_started().map(|n| n.as_u64())
774 }
775
776 #[getter]
778 #[pyo3(name = "run_finished")]
779 fn py_run_finished(&self) -> Option<u64> {
780 self.0.run_finished().map(|n| n.as_u64())
781 }
782
783 #[getter]
785 #[pyo3(name = "backtest_start")]
786 fn py_backtest_start(&self) -> Option<u64> {
787 self.0.backtest_start().map(|n| n.as_u64())
788 }
789
790 #[getter]
792 #[pyo3(name = "backtest_end")]
793 fn py_backtest_end(&self) -> Option<u64> {
794 self.0.backtest_end().map(|n| n.as_u64())
795 }
796
797 #[pyo3(name = "list_venues")]
799 fn py_list_venues(&self) -> Vec<Venue> {
800 self.0.list_venues()
801 }
802
803 #[getter]
805 #[pyo3(name = "cache")]
806 fn py_cache(&self) -> PyCache {
807 PyCache::from_rc(self.0.kernel().cache.clone())
808 }
809
810 fn __repr__(&self) -> String {
811 format!("{:?}", self.0)
812 }
813}
814
815impl PyBacktestEngine {
816 #[must_use]
818 pub fn inner(&self) -> &BacktestEngine {
819 &self.0
820 }
821
822 pub fn inner_mut(&mut self) -> &mut BacktestEngine {
824 &mut self.0
825 }
826}
827
828pub(crate) fn pyobject_to_fill_model_any(
829 _py: Python,
830 obj: &Bound<'_, PyAny>,
831) -> PyResult<FillModelAny> {
832 if let Ok(m) = obj.extract::<DefaultFillModel>() {
833 return Ok(FillModelAny::Default(m));
834 }
835
836 if let Ok(m) = obj.extract::<BestPriceFillModel>() {
837 return Ok(FillModelAny::BestPrice(m));
838 }
839
840 if let Ok(m) = obj.extract::<OneTickSlippageFillModel>() {
841 return Ok(FillModelAny::OneTickSlippage(m));
842 }
843
844 if let Ok(m) = obj.extract::<ProbabilisticFillModel>() {
845 return Ok(FillModelAny::Probabilistic(m));
846 }
847
848 if let Ok(m) = obj.extract::<TwoTierFillModel>() {
849 return Ok(FillModelAny::TwoTier(m));
850 }
851
852 if let Ok(m) = obj.extract::<ThreeTierFillModel>() {
853 return Ok(FillModelAny::ThreeTier(m));
854 }
855
856 if let Ok(m) = obj.extract::<LimitOrderPartialFillModel>() {
857 return Ok(FillModelAny::LimitOrderPartialFill(m));
858 }
859
860 if let Ok(m) = obj.extract::<SizeAwareFillModel>() {
861 return Ok(FillModelAny::SizeAware(m));
862 }
863
864 if let Ok(m) = obj.extract::<CompetitionAwareFillModel>() {
865 return Ok(FillModelAny::CompetitionAware(m));
866 }
867
868 if let Ok(m) = obj.extract::<VolumeSensitiveFillModel>() {
869 return Ok(FillModelAny::VolumeSensitive(m));
870 }
871
872 if let Ok(m) = obj.extract::<MarketHoursFillModel>() {
873 return Ok(FillModelAny::MarketHours(m));
874 }
875
876 let type_name = obj.get_type().name()?;
877 Err(to_pytype_err(format!(
878 "Cannot convert {type_name} to FillModel"
879 )))
880}
881
882pub(crate) fn pyobject_to_fee_model_any(
883 _py: Python,
884 obj: &Bound<'_, PyAny>,
885) -> PyResult<FeeModelAny> {
886 if let Ok(m) = obj.extract::<FixedFeeModel>() {
887 return Ok(FeeModelAny::Fixed(m));
888 }
889
890 if let Ok(m) = obj.extract::<MakerTakerFeeModel>() {
891 return Ok(FeeModelAny::MakerTaker(m));
892 }
893
894 if let Ok(m) = obj.extract::<PerContractFeeModel>() {
895 return Ok(FeeModelAny::PerContract(m));
896 }
897
898 let type_name = obj.get_type().name()?;
899 Err(to_pytype_err(format!(
900 "Cannot convert {type_name} to FeeModel"
901 )))
902}
903
904pub(crate) fn pyobject_to_simulation_module_any(
905 _py: Python,
906 obj: &Bound<'_, PyAny>,
907) -> PyResult<SimulationModuleAny> {
908 if let Ok(cell) = obj.cast::<FXRolloverInterestModule>() {
909 let module = cell.borrow().clone();
910 return Ok(SimulationModuleAny::FXRolloverInterest(module));
911 }
912
913 let type_name = obj.get_type().name()?;
914 Err(to_pytype_err(format!(
915 "Cannot convert {type_name} to SimulationModule"
916 )))
917}
918
919pub(crate) fn pyobject_to_latency_model_any(
920 _py: Python,
921 obj: &Bound<'_, PyAny>,
922) -> PyResult<LatencyModelAny> {
923 if let Ok(m) = obj.extract::<StaticLatencyModel>() {
924 return Ok(LatencyModelAny::Static(m));
925 }
926
927 let type_name = obj.get_type().name()?;
928 Err(to_pytype_err(format!(
929 "Cannot convert {type_name} to LatencyModel"
930 )))
931}
932
933pub(crate) fn pyobject_to_margin_model_any(
934 _py: Python,
935 obj: &Bound<'_, PyAny>,
936) -> PyResult<MarginModelAny> {
937 if let Ok(m) = obj.extract::<StandardMarginModel>() {
938 return Ok(MarginModelAny::Standard(m));
939 }
940
941 if let Ok(m) = obj.extract::<LeveragedMarginModel>() {
942 return Ok(MarginModelAny::Leveraged(m));
943 }
944
945 let type_name = obj.get_type().name()?;
946 Err(to_pytype_err(format!(
947 "Cannot convert {type_name} to MarginModel"
948 )))
949}
950
951fn pyobject_to_data(_py: Python, obj: &Bound<'_, PyAny>) -> PyResult<Data> {
952 if let Ok(delta) = obj.extract::<OrderBookDelta>() {
953 return Ok(Data::Delta(delta));
954 }
955
956 if let Ok(deltas) = obj.extract::<OrderBookDeltas>() {
957 return Ok(Data::Deltas(OrderBookDeltas_API::new(deltas)));
958 }
959
960 if let Ok(quote) = obj.extract::<QuoteTick>() {
961 return Ok(Data::Quote(quote));
962 }
963
964 if let Ok(trade) = obj.extract::<TradeTick>() {
965 return Ok(Data::Trade(trade));
966 }
967
968 if let Ok(bar) = obj.extract::<Bar>() {
969 return Ok(Data::Bar(bar));
970 }
971
972 if let Ok(depth) = obj.extract::<OrderBookDepth10>() {
973 return Ok(Data::Depth10(Box::new(depth)));
974 }
975
976 if let Ok(mark) = obj.extract::<MarkPriceUpdate>() {
977 return Ok(Data::MarkPriceUpdate(mark));
978 }
979
980 if let Ok(index) = obj.extract::<IndexPriceUpdate>() {
981 return Ok(Data::IndexPriceUpdate(index));
982 }
983
984 if let Ok(status) = obj.extract::<InstrumentStatus>() {
985 return Ok(Data::InstrumentStatus(status));
986 }
987
988 if let Ok(close) = obj.extract::<InstrumentClose>() {
989 return Ok(Data::InstrumentClose(close));
990 }
991
992 if let Ok(delta) = OrderBookDelta::from_pyobject(obj) {
994 return Ok(Data::Delta(delta));
995 }
996
997 if let Ok(quote) = QuoteTick::from_pyobject(obj) {
998 return Ok(Data::Quote(quote));
999 }
1000
1001 if let Ok(trade) = TradeTick::from_pyobject(obj) {
1002 return Ok(Data::Trade(trade));
1003 }
1004
1005 if let Ok(bar) = Bar::from_pyobject(obj) {
1006 return Ok(Data::Bar(bar));
1007 }
1008
1009 if let Ok(mark) = MarkPriceUpdate::from_pyobject(obj) {
1010 return Ok(Data::MarkPriceUpdate(mark));
1011 }
1012
1013 if let Ok(index) = IndexPriceUpdate::from_pyobject(obj) {
1014 return Ok(Data::IndexPriceUpdate(index));
1015 }
1016
1017 if let Ok(status) = InstrumentStatus::from_pyobject(obj) {
1018 return Ok(Data::InstrumentStatus(status));
1019 }
1020
1021 if let Ok(close) = InstrumentClose::from_pyobject(obj) {
1022 return Ok(Data::InstrumentClose(close));
1023 }
1024
1025 let type_name = obj.get_type().name()?;
1026 Err(to_pytype_err(format!("Cannot convert {type_name} to Data")))
1027}