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::{
31 CappedOptionFeeModel, FeeModelAny, FixedFeeModel, MakerTakerFeeModel, PerContractFeeModel,
32 TieredNotionalOptionFeeModel,
33 },
34 fill::{
35 BestPriceFillModel, CompetitionAwareFillModel, DefaultFillModel, FillModelAny,
36 LimitOrderPartialFillModel, MarketHoursFillModel, OneTickSlippageFillModel,
37 ProbabilisticFillModel, SizeAwareFillModel, ThreeTierFillModel, TwoTierFillModel,
38 VolumeSensitiveFillModel,
39 },
40 latency::{LatencyModelAny, StaticLatencyModel},
41};
42#[cfg(feature = "defi")]
43use nautilus_model::defi::DefiData;
44use nautilus_model::{
45 accounts::margin_model::{LeveragedMarginModel, MarginModelAny, StandardMarginModel},
46 data::{
47 Bar, Data, FundingRateUpdate, IndexPriceUpdate, InstrumentClose, InstrumentStatus,
48 MarkPriceUpdate, OptionGreeks, OrderBookDelta, OrderBookDeltas, OrderBookDeltas_API,
49 OrderBookDepth10, QuoteTick, TradeTick,
50 },
51 enums::{AccountType, BookType, OmsType, OtoTriggerMode},
52 identifiers::{ActorId, ClientId, ComponentId, InstrumentId, StrategyId, TraderId, Venue},
53 python::instruments::pyobject_to_instrument_any,
54 types::{Currency, Money, Price},
55};
56#[cfg(feature = "examples")]
57use nautilus_trading::examples::{
58 actors::{BookImbalanceActor, BookImbalanceActorConfig},
59 strategies::{
60 CompositeMarketMaker, CompositeMarketMakerConfig, DeltaNeutralVol, DeltaNeutralVolConfig,
61 EmaCross, EmaCrossConfig, GridMarketMaker, GridMarketMakerConfig, HurstVpinDirectional,
62 HurstVpinDirectionalConfig,
63 },
64};
65use nautilus_trading::{
66 ImportableStrategyConfig,
67 python::strategy::{PyStrategy, PyStrategyInner},
68};
69use pyo3::prelude::*;
70use rust_decimal::Decimal;
71
72use super::node::create_config_instance;
73use crate::{
74 config::{BacktestEngineConfig, SimulatedVenueConfig},
75 engine::BacktestEngine,
76 modules::{FXRolloverInterestModule, SimulationModuleAny},
77 result::BacktestResult,
78};
79
80#[pyo3::pyclass(
85 module = "nautilus_trader.core.nautilus_pyo3.backtest",
86 name = "BacktestEngine",
87 unsendable
88)]
89#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.backtest")]
90#[derive(Debug)]
91pub struct PyBacktestEngine(BacktestEngine);
92
93#[cfg(feature = "defi")]
96#[pyo3_stub_gen::derive::gen_stub_pymethods]
97#[pymethods]
98impl PyBacktestEngine {
99 #[pyo3(name = "add_defi_data", signature = (data, client_id=None, sort=true))]
101 fn py_add_defi_data(
102 &mut self,
103 data: Vec<DefiData>,
104 client_id: Option<ClientId>,
105 sort: bool,
106 ) -> PyResult<()> {
107 self.0
108 .add_defi_data(data, client_id, sort)
109 .map_err(to_pyruntime_err)
110 }
111}
112
113#[pyo3_stub_gen::derive::gen_stub_pymethods]
114#[pymethods]
115impl PyBacktestEngine {
116 #[new]
117 fn py_new(config: BacktestEngineConfig) -> PyResult<Self> {
118 let engine = BacktestEngine::new(config).map_err(to_pyruntime_err)?;
119 Ok(Self(engine))
120 }
121
122 #[pyo3(
136 name = "add_venue",
137 signature = (
138 venue,
139 oms_type,
140 account_type,
141 starting_balances,
142 base_currency = None,
143 default_leverage = None,
144 leverages = None,
145 margin_model = None,
146 fill_model = None,
147 fee_model = None,
148 latency_model = None,
149 modules = None,
150 book_type = BookType::L1_MBP,
151 routing = false,
152 reject_stop_orders = true,
153 support_gtd_orders = true,
154 support_contingent_orders = true,
155 use_position_ids = true,
156 use_random_ids = false,
157 use_reduce_only = true,
158 use_message_queue = true,
159 use_market_order_acks = false,
160 bar_execution = true,
161 bar_adaptive_high_low_ordering = false,
162 trade_execution = true,
163 liquidity_consumption = false,
164 queue_position = false,
165 allow_cash_borrowing = false,
166 frozen_account = false,
167 oto_trigger_mode = OtoTriggerMode::Partial,
168 price_protection_points = None,
169 settlement_prices = None,
170 liquidation_enabled = false,
171 liquidation_trigger_ratio = None,
172 liquidation_cancel_open_orders = true,
173 )
174 )]
175 #[expect(clippy::too_many_arguments)]
176 fn py_add_venue(
177 &mut self,
178 venue: Venue,
179 oms_type: OmsType,
180 account_type: AccountType,
181 starting_balances: Vec<Money>,
182 base_currency: Option<Currency>,
183 default_leverage: Option<Decimal>,
184 leverages: Option<HashMap<InstrumentId, Decimal>>,
185 margin_model: Option<Py<PyAny>>,
186 fill_model: Option<Py<PyAny>>,
187 fee_model: Option<Py<PyAny>>,
188 latency_model: Option<Py<PyAny>>,
189 modules: Option<Vec<Py<PyAny>>>,
190 book_type: BookType,
191 routing: bool,
192 reject_stop_orders: bool,
193 support_gtd_orders: bool,
194 support_contingent_orders: bool,
195 use_position_ids: bool,
196 use_random_ids: bool,
197 use_reduce_only: bool,
198 use_message_queue: bool,
199 use_market_order_acks: bool,
200 bar_execution: bool,
201 bar_adaptive_high_low_ordering: bool,
202 trade_execution: bool,
203 liquidity_consumption: bool,
204 queue_position: bool,
205 allow_cash_borrowing: bool,
206 frozen_account: bool,
207 oto_trigger_mode: OtoTriggerMode,
208 price_protection_points: Option<u32>,
209 settlement_prices: Option<HashMap<InstrumentId, Price>>,
210 liquidation_enabled: bool,
211 liquidation_trigger_ratio: Option<f64>,
212 liquidation_cancel_open_orders: bool,
213 ) -> PyResult<()> {
214 let leverages: AHashMap<InstrumentId, Decimal> = leverages
215 .map(|m| m.into_iter().collect())
216 .unwrap_or_default();
217 let settlement_prices: AHashMap<InstrumentId, Price> = settlement_prices
218 .map(|m| m.into_iter().collect())
219 .unwrap_or_default();
220 let margin_model = margin_model
221 .map(|obj| Python::attach(|py| pyobject_to_margin_model_any(py, obj.bind(py))))
222 .transpose()?;
223 let fill_model = fill_model
224 .map(|obj| Python::attach(|py| pyobject_to_fill_model_any(py, obj.bind(py))))
225 .transpose()?
226 .unwrap_or_default();
227 let fee_model = fee_model
228 .map(|obj| Python::attach(|py| pyobject_to_fee_model_any(py, obj.bind(py))))
229 .transpose()?
230 .unwrap_or_default();
231 let latency_model = latency_model
232 .map(|obj| Python::attach(|py| pyobject_to_latency_model_any(py, obj.bind(py))))
233 .transpose()?
234 .map(Into::into);
235 let modules = modules
236 .map(|objs| {
237 objs.into_iter()
238 .map(|obj| {
239 Python::attach(|py| pyobject_to_simulation_module_any(py, obj.bind(py)))
240 })
241 .collect::<PyResult<Vec<_>>>()
242 })
243 .transpose()?
244 .unwrap_or_default()
245 .into_iter()
246 .map(Into::into)
247 .collect();
248
249 let sim_config = SimulatedVenueConfig::builder()
250 .venue(venue)
251 .oms_type(oms_type)
252 .account_type(account_type)
253 .book_type(book_type)
254 .starting_balances(starting_balances)
255 .maybe_base_currency(base_currency)
256 .maybe_default_leverage(default_leverage)
257 .leverages(leverages)
258 .maybe_margin_model(margin_model)
259 .modules(modules)
260 .fill_model(fill_model)
261 .fee_model(fee_model)
262 .maybe_latency_model(latency_model)
263 .routing(routing)
264 .reject_stop_orders(reject_stop_orders)
265 .support_gtd_orders(support_gtd_orders)
266 .support_contingent_orders(support_contingent_orders)
267 .use_position_ids(use_position_ids)
268 .use_random_ids(use_random_ids)
269 .use_reduce_only(use_reduce_only)
270 .use_message_queue(use_message_queue)
271 .use_market_order_acks(use_market_order_acks)
272 .bar_execution(bar_execution)
273 .bar_adaptive_high_low_ordering(bar_adaptive_high_low_ordering)
274 .trade_execution(trade_execution)
275 .liquidity_consumption(liquidity_consumption)
276 .allow_cash_borrowing(allow_cash_borrowing)
277 .frozen_account(frozen_account)
278 .queue_position(queue_position)
279 .oto_full_trigger(oto_trigger_mode == OtoTriggerMode::Full)
280 .maybe_price_protection_points(price_protection_points)
281 .liquidation_enabled(liquidation_enabled)
282 .liquidation_trigger_ratio(liquidation_trigger_ratio.unwrap_or(1.0))
283 .liquidation_cancel_open_orders(liquidation_cancel_open_orders)
284 .build();
285
286 self.0.add_venue(sim_config).map_err(to_pyruntime_err)?;
287
288 for (instrument_id, price) in settlement_prices {
289 self.0
290 .set_settlement_price(venue, instrument_id, price)
291 .map_err(to_pyruntime_err)?;
292 }
293
294 Ok(())
295 }
296
297 #[pyo3(name = "change_fill_model")]
299 #[expect(clippy::needless_pass_by_value)]
300 fn py_change_fill_model(
301 &mut self,
302 py: Python,
303 venue: Venue,
304 fill_model: Py<PyAny>,
305 ) -> PyResult<()> {
306 let fill_model = pyobject_to_fill_model_any(py, fill_model.bind(py))?;
307 self.0.change_fill_model(venue, fill_model);
308 Ok(())
309 }
310
311 #[pyo3(
313 name = "add_data",
314 signature = (data, client_id=None, validate=true, sort=true)
315 )]
316 fn py_add_data(
317 &mut self,
318 py: Python,
319 data: Vec<Py<PyAny>>,
320 client_id: Option<ClientId>,
321 validate: bool,
322 sort: bool,
323 ) -> PyResult<()> {
324 let rust_data: Vec<Data> = data
325 .into_iter()
326 .map(|obj| pyobject_to_data(py, obj.bind(py)))
327 .collect::<PyResult<_>>()?;
328 self.0
329 .add_data(rust_data, client_id, validate, sort)
330 .map_err(to_pyruntime_err)
331 }
332
333 #[pyo3(name = "add_instrument")]
335 fn py_add_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
336 let instrument_any = pyobject_to_instrument_any(py, instrument)?;
337 self.0
338 .add_instrument(&instrument_any)
339 .map_err(to_pyruntime_err)
340 }
341
342 #[allow(
344 unsafe_code,
345 reason = "Required for Python actor component registration"
346 )]
347 #[pyo3(name = "add_actor_from_config")]
348 #[expect(clippy::needless_pass_by_value)]
349 fn py_add_actor_from_config(
350 &mut self,
351 _py: Python,
352 config: ImportableActorConfig,
353 ) -> PyResult<()> {
354 log::debug!("`add_actor_from_config` with: {config:?}");
355
356 let parts: Vec<&str> = config.actor_path.split(':').collect();
357 if parts.len() != 2 {
358 return Err(to_pyvalue_err(
359 "actor_path must be in format 'module.path:ClassName'",
360 ));
361 }
362 let (module_name, class_name) = (parts[0], parts[1]);
363
364 log::info!("Importing actor from module: {module_name} class: {class_name}");
365
366 let (python_actor, actor_id) =
367 Python::attach(|py| -> anyhow::Result<(Py<PyAny>, ActorId)> {
368 let actor_module = py
369 .import(module_name)
370 .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
371 let actor_class = actor_module
372 .getattr(class_name)
373 .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
374
375 let config_instance =
376 create_config_instance(py, &config.config_path, &config.config)?;
377
378 let python_actor = if let Some(config_obj) = config_instance.clone() {
379 actor_class.call1((config_obj,))?
380 } else {
381 actor_class.call0()?
382 };
383
384 let mut py_data_actor_ref = python_actor
385 .extract::<PyRefMut<PyDataActor>>()
386 .map_err(Into::<PyErr>::into)
387 .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
388
389 if let Some(config_obj) = config_instance.as_ref() {
390 if let Ok(actor_id) = config_obj.getattr("actor_id")
391 && !actor_id.is_none()
392 {
393 let actor_id_val = if let Ok(actor_id_val) = actor_id.extract::<ActorId>() {
394 actor_id_val
395 } else if let Ok(actor_id_str) = actor_id.extract::<String>() {
396 ActorId::new_checked(&actor_id_str)?
397 } else {
398 anyhow::bail!("Invalid `actor_id` type");
399 };
400 py_data_actor_ref.set_actor_id(actor_id_val);
401 }
402
403 if let Ok(log_events) = config_obj.getattr("log_events")
404 && let Ok(log_events_val) = log_events.extract::<bool>()
405 {
406 py_data_actor_ref.set_log_events(log_events_val);
407 }
408
409 if let Ok(log_commands) = config_obj.getattr("log_commands")
410 && let Ok(log_commands_val) = log_commands.extract::<bool>()
411 {
412 py_data_actor_ref.set_log_commands(log_commands_val);
413 }
414 }
415
416 py_data_actor_ref.set_python_instance(python_actor.clone().unbind());
417 let actor_id = py_data_actor_ref.actor_id();
418
419 Ok((python_actor.unbind(), actor_id))
420 })
421 .map_err(to_pyruntime_err)?;
422
423 if self
424 .0
425 .kernel()
426 .trader
427 .borrow()
428 .actor_ids()
429 .contains(&actor_id)
430 {
431 return Err(to_pyruntime_err(format!(
432 "Actor '{actor_id}' is already registered"
433 )));
434 }
435
436 let trader_id = self.0.kernel().config.trader_id();
437 let cache = self.0.kernel().cache.clone();
438 let component_id = ComponentId::new(actor_id.inner().as_str());
439 let clock = self
440 .0
441 .kernel_mut()
442 .trader
443 .borrow_mut()
444 .create_component_clock(component_id);
445
446 Python::attach(|py| -> anyhow::Result<()> {
447 let py_actor = python_actor.bind(py);
448 let mut py_data_actor_ref = py_actor
449 .extract::<PyRefMut<PyDataActor>>()
450 .map_err(Into::<PyErr>::into)
451 .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
452
453 py_data_actor_ref
454 .register(trader_id, clock, cache)
455 .map_err(|e| anyhow::anyhow!("Failed to register PyDataActor: {e}"))?;
456
457 Ok(())
458 })
459 .map_err(to_pyruntime_err)?;
460
461 Python::attach(|py| -> anyhow::Result<()> {
462 let py_actor = python_actor.bind(py);
463 let py_data_actor_ref = py_actor
464 .cast::<PyDataActor>()
465 .map_err(|e| anyhow::anyhow!("Failed to downcast to PyDataActor: {e}"))?;
466 py_data_actor_ref.borrow().register_in_global_registries();
467 Ok(())
468 })
469 .map_err(to_pyruntime_err)?;
470
471 self.0
472 .kernel_mut()
473 .trader
474 .borrow_mut()
475 .add_actor_id_for_lifecycle(actor_id)
476 .map_err(to_pyruntime_err)?;
477
478 log::info!("Registered Python actor {actor_id}");
479 Ok(())
480 }
481
482 #[allow(
484 unsafe_code,
485 reason = "Required for Python strategy component registration"
486 )]
487 #[pyo3(name = "add_strategy_from_config")]
488 #[expect(clippy::needless_pass_by_value)]
489 fn py_add_strategy_from_config(
490 &mut self,
491 _py: Python,
492 config: ImportableStrategyConfig,
493 ) -> PyResult<()> {
494 log::debug!("`add_strategy_from_config` with: {config:?}");
495
496 let parts: Vec<&str> = config.strategy_path.split(':').collect();
497 if parts.len() != 2 {
498 return Err(to_pyvalue_err(
499 "strategy_path must be in format 'module.path:ClassName'",
500 ));
501 }
502 let (module_name, class_name) = (parts[0], parts[1]);
503
504 log::info!("Importing strategy from module: {module_name} class: {class_name}");
505
506 let (python_strategy, strategy_id) =
507 Python::attach(|py| -> anyhow::Result<(Py<PyAny>, StrategyId)> {
508 let strategy_module = py
509 .import(module_name)
510 .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
511 let strategy_class = strategy_module
512 .getattr(class_name)
513 .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
514
515 let config_instance =
516 create_config_instance(py, &config.config_path, &config.config)?;
517
518 let python_strategy = if let Some(config_obj) = config_instance.clone() {
519 strategy_class.call1((config_obj,))?
520 } else {
521 strategy_class.call0()?
522 };
523
524 let mut py_strategy_ref = python_strategy
525 .extract::<PyRefMut<PyStrategy>>()
526 .map_err(Into::<PyErr>::into)
527 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
528
529 if let Some(config_obj) = config_instance.as_ref() {
530 if let Ok(strategy_id) = config_obj.getattr("strategy_id")
531 && !strategy_id.is_none()
532 {
533 let strategy_id_val = if let Ok(sid) = strategy_id.extract::<StrategyId>() {
534 sid
535 } else if let Ok(sid_str) = strategy_id.extract::<String>() {
536 StrategyId::new_checked(&sid_str)?
537 } else {
538 anyhow::bail!("Invalid `strategy_id` type");
539 };
540 py_strategy_ref.set_strategy_id(strategy_id_val)?;
541 }
542
543 if let Ok(order_id_tag) = config_obj.getattr("order_id_tag")
544 && !order_id_tag.is_none()
545 {
546 let order_id_tag_val = order_id_tag
547 .extract::<String>()
548 .map_err(|e| anyhow::anyhow!("Invalid `order_id_tag` type: {e}"))?;
549 py_strategy_ref.set_order_id_tag(&order_id_tag_val)?;
550 }
551
552 if let Ok(log_events) = config_obj.getattr("log_events")
553 && let Ok(log_events_val) = log_events.extract::<bool>()
554 {
555 py_strategy_ref.set_log_events(log_events_val);
556 }
557
558 if let Ok(log_commands) = config_obj.getattr("log_commands")
559 && let Ok(log_commands_val) = log_commands.extract::<bool>()
560 {
561 py_strategy_ref.set_log_commands(log_commands_val);
562 }
563 }
564
565 py_strategy_ref.set_python_instance(python_strategy.clone().unbind());
566 let strategy_id = py_strategy_ref.strategy_id();
567
568 Ok((python_strategy.unbind(), strategy_id))
569 })
570 .map_err(to_pyruntime_err)?;
571
572 if self
573 .0
574 .kernel()
575 .trader
576 .borrow()
577 .strategy_ids()
578 .contains(&strategy_id)
579 {
580 return Err(to_pyruntime_err(format!(
581 "Strategy '{strategy_id}' is already registered"
582 )));
583 }
584
585 let trader_id = self.0.kernel().config.trader_id();
586 let cache = self.0.kernel().cache.clone();
587 let portfolio = self.0.kernel().portfolio.clone();
588 let component_id = ComponentId::new(strategy_id.inner().as_str());
589 let clock = self
590 .0
591 .kernel_mut()
592 .trader
593 .borrow_mut()
594 .create_component_clock(component_id);
595
596 Python::attach(|py| -> anyhow::Result<()> {
597 let py_strategy = python_strategy.bind(py);
598 let mut py_strategy_ref = py_strategy
599 .extract::<PyRefMut<PyStrategy>>()
600 .map_err(Into::<PyErr>::into)
601 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
602
603 py_strategy_ref
604 .register(trader_id, clock, cache, portfolio)
605 .map_err(|e| anyhow::anyhow!("Failed to register PyStrategy: {e}"))?;
606
607 Ok(())
608 })
609 .map_err(to_pyruntime_err)?;
610
611 Python::attach(|py| -> anyhow::Result<()> {
612 let py_strategy = python_strategy.bind(py);
613 let py_strategy_ref = py_strategy
614 .cast::<PyStrategy>()
615 .map_err(|e| anyhow::anyhow!("Failed to downcast to PyStrategy: {e}"))?;
616 py_strategy_ref.borrow().register_in_global_registries();
617 Ok(())
618 })
619 .map_err(to_pyruntime_err)?;
620
621 self.0
622 .kernel_mut()
623 .trader
624 .borrow_mut()
625 .add_strategy_id_with_subscriptions::<PyStrategyInner>(strategy_id)
626 .map_err(to_pyruntime_err)?;
627
628 log::info!("Registered Python strategy {strategy_id}");
629 Ok(())
630 }
631
632 #[cfg(feature = "examples")]
637 #[pyo3(name = "add_native_strategy")]
638 fn py_add_native_strategy(
639 &mut self,
640 type_name: &str,
641 config: &Bound<'_, PyAny>,
642 ) -> PyResult<()> {
643 let register = native_strategy_register(type_name).ok_or_else(|| {
644 to_pytype_err(format!("Unsupported native strategy type: {type_name}"))
645 })?;
646 register(&mut self.0, config)
647 }
648
649 #[cfg(feature = "examples")]
654 #[pyo3(name = "add_native_actor")]
655 fn py_add_native_actor(&mut self, type_name: &str, config: &Bound<'_, PyAny>) -> PyResult<()> {
656 let register = native_actor_register(type_name)
657 .ok_or_else(|| to_pytype_err(format!("Unsupported native actor type: {type_name}")))?;
658 register(&mut self.0, config)
659 }
660
661 #[pyo3(
663 name = "run",
664 signature = (start=None, end=None, run_config_id=None, streaming=false)
665 )]
666 fn py_run(
667 &mut self,
668 start: Option<u64>,
669 end: Option<u64>,
670 run_config_id: Option<String>,
671 streaming: bool,
672 ) -> PyResult<()> {
673 self.0
674 .run(
675 start.map(UnixNanos::from),
676 end.map(UnixNanos::from),
677 run_config_id,
678 streaming,
679 )
680 .map_err(to_pyruntime_err)
681 }
682
683 #[pyo3(name = "end")]
685 fn py_end(&mut self) {
686 self.0.end();
687 }
688
689 #[pyo3(name = "reset")]
691 fn py_reset(&mut self) {
692 self.0.reset();
693 }
694
695 #[pyo3(name = "dispose")]
697 fn py_dispose(&mut self) {
698 self.0.dispose();
699 }
700
701 #[pyo3(name = "get_result")]
703 fn py_get_result(&self) -> BacktestResult {
704 self.0.get_result()
705 }
706
707 #[pyo3(name = "clear_data")]
709 fn py_clear_data(&mut self) {
710 self.0.clear_data();
711 }
712
713 #[pyo3(name = "clear_actors")]
715 fn py_clear_actors(&mut self) -> PyResult<()> {
716 self.0.clear_actors().map_err(to_pyruntime_err)
717 }
718
719 #[pyo3(name = "clear_strategies")]
721 fn py_clear_strategies(&mut self) -> PyResult<()> {
722 self.0.clear_strategies().map_err(to_pyruntime_err)
723 }
724
725 #[pyo3(name = "clear_exec_algorithms")]
727 fn py_clear_exec_algorithms(&mut self) -> PyResult<()> {
728 self.0.clear_exec_algorithms().map_err(to_pyruntime_err)
729 }
730
731 #[pyo3(name = "add_actors_from_configs")]
733 fn py_add_actors_from_configs(
734 &mut self,
735 py: Python,
736 configs: Vec<ImportableActorConfig>,
737 ) -> PyResult<()> {
738 for config in configs {
739 self.py_add_actor_from_config(py, config)?;
740 }
741 Ok(())
742 }
743
744 #[pyo3(name = "add_strategies_from_configs")]
746 fn py_add_strategies_from_configs(
747 &mut self,
748 py: Python,
749 configs: Vec<ImportableStrategyConfig>,
750 ) -> PyResult<()> {
751 for config in configs {
752 self.py_add_strategy_from_config(py, config)?;
753 }
754 Ok(())
755 }
756
757 #[pyo3(name = "sort_data")]
759 fn py_sort_data(&mut self) {
760 self.0.sort_data();
761 }
762
763 #[getter]
765 #[pyo3(name = "trader_id")]
766 fn py_trader_id(&self) -> TraderId {
767 self.0.trader_id()
768 }
769
770 #[getter]
772 #[pyo3(name = "machine_id")]
773 fn py_machine_id(&self) -> String {
774 self.0.machine_id().to_string()
775 }
776
777 #[getter]
779 #[pyo3(name = "instance_id")]
780 fn py_instance_id(&self) -> UUID4 {
781 self.0.instance_id()
782 }
783
784 #[getter]
786 #[pyo3(name = "iteration")]
787 fn py_iteration(&self) -> usize {
788 self.0.iteration()
789 }
790
791 #[getter]
793 #[pyo3(name = "run_config_id")]
794 fn py_run_config_id(&self) -> Option<String> {
795 self.0.run_config_id().map(str::to_string)
796 }
797
798 #[getter]
800 #[pyo3(name = "run_id")]
801 fn py_run_id(&self) -> Option<UUID4> {
802 self.0.run_id()
803 }
804
805 #[getter]
807 #[pyo3(name = "run_started")]
808 fn py_run_started(&self) -> Option<u64> {
809 self.0.run_started().map(|n| n.as_u64())
810 }
811
812 #[getter]
814 #[pyo3(name = "run_finished")]
815 fn py_run_finished(&self) -> Option<u64> {
816 self.0.run_finished().map(|n| n.as_u64())
817 }
818
819 #[getter]
821 #[pyo3(name = "backtest_start")]
822 fn py_backtest_start(&self) -> Option<u64> {
823 self.0.backtest_start().map(|n| n.as_u64())
824 }
825
826 #[getter]
828 #[pyo3(name = "backtest_end")]
829 fn py_backtest_end(&self) -> Option<u64> {
830 self.0.backtest_end().map(|n| n.as_u64())
831 }
832
833 #[pyo3(name = "list_venues")]
835 fn py_list_venues(&self) -> Vec<Venue> {
836 self.0.list_venues()
837 }
838
839 #[getter]
841 #[pyo3(name = "cache")]
842 fn py_cache(&self) -> PyCache {
843 PyCache::from_rc(self.0.kernel().cache.clone())
844 }
845
846 fn __repr__(&self) -> String {
847 format!("{:?}", self.0)
848 }
849}
850
851impl PyBacktestEngine {
852 #[must_use]
854 pub fn inner(&self) -> &BacktestEngine {
855 &self.0
856 }
857
858 pub fn inner_mut(&mut self) -> &mut BacktestEngine {
860 &mut self.0
861 }
862}
863
864#[cfg(feature = "examples")]
865type NativeStrategyRegister = for<'py> fn(&mut BacktestEngine, &Bound<'py, PyAny>) -> PyResult<()>;
866
867#[cfg(feature = "examples")]
868type NativeActorRegister = for<'py> fn(&mut BacktestEngine, &Bound<'py, PyAny>) -> PyResult<()>;
869
870#[cfg(feature = "examples")]
871fn native_strategy_register(type_name: &str) -> Option<NativeStrategyRegister> {
872 match type_name {
873 "CompositeMarketMaker" => Some(register_composite_market_maker),
874 "DeltaNeutralVol" => Some(register_delta_neutral_vol),
875 "EmaCross" => Some(register_ema_cross),
876 "GridMarketMaker" => Some(register_grid_market_maker),
877 "HurstVpinDirectional" => Some(register_hurst_vpin_directional),
878 _ => None,
879 }
880}
881
882#[cfg(feature = "examples")]
883fn native_actor_register(type_name: &str) -> Option<NativeActorRegister> {
884 match type_name {
885 "BookImbalanceActor" => Some(register_book_imbalance_actor),
886 _ => None,
887 }
888}
889
890#[cfg(feature = "examples")]
891fn register_composite_market_maker(
892 engine: &mut BacktestEngine,
893 config: &Bound<'_, PyAny>,
894) -> PyResult<()> {
895 let config = config.extract::<CompositeMarketMakerConfig>()?;
896 engine
897 .add_strategy(CompositeMarketMaker::new(config))
898 .map_err(to_pyruntime_err)
899}
900
901#[cfg(feature = "examples")]
902fn register_delta_neutral_vol(
903 engine: &mut BacktestEngine,
904 config: &Bound<'_, PyAny>,
905) -> PyResult<()> {
906 let config = config.extract::<DeltaNeutralVolConfig>()?;
907 engine
908 .add_strategy(DeltaNeutralVol::new(config))
909 .map_err(to_pyruntime_err)
910}
911
912#[cfg(feature = "examples")]
913fn register_ema_cross(engine: &mut BacktestEngine, config: &Bound<'_, PyAny>) -> PyResult<()> {
914 let config = config.extract::<EmaCrossConfig>()?;
915 engine
916 .add_strategy(EmaCross::from_config(config))
917 .map_err(to_pyruntime_err)
918}
919
920#[cfg(feature = "examples")]
921fn register_grid_market_maker(
922 engine: &mut BacktestEngine,
923 config: &Bound<'_, PyAny>,
924) -> PyResult<()> {
925 let config = config.extract::<GridMarketMakerConfig>()?;
926 engine
927 .add_strategy(GridMarketMaker::new(config))
928 .map_err(to_pyruntime_err)
929}
930
931#[cfg(feature = "examples")]
932fn register_hurst_vpin_directional(
933 engine: &mut BacktestEngine,
934 config: &Bound<'_, PyAny>,
935) -> PyResult<()> {
936 let config = config.extract::<HurstVpinDirectionalConfig>()?;
937 engine
938 .add_strategy(HurstVpinDirectional::new(config))
939 .map_err(to_pyruntime_err)
940}
941
942#[cfg(feature = "examples")]
943fn register_book_imbalance_actor(
944 engine: &mut BacktestEngine,
945 config: &Bound<'_, PyAny>,
946) -> PyResult<()> {
947 let config = config.extract::<BookImbalanceActorConfig>()?;
948 engine
949 .add_actor(BookImbalanceActor::from_config(config))
950 .map_err(to_pyruntime_err)
951}
952
953#[cfg(all(test, feature = "examples"))]
954mod tests {
955 use pyo3::{Python, types::PyDict};
956 use rstest::rstest;
957
958 use crate::{config::BacktestEngineConfig, engine::BacktestEngine};
959
960 #[rstest]
961 #[case("CompositeMarketMaker")]
962 #[case("DeltaNeutralVol")]
963 #[case("EmaCross")]
964 #[case("GridMarketMaker")]
965 #[case("HurstVpinDirectional")]
966 fn test_native_strategy_register_accepts_supported_names(#[case] type_name: &str) {
967 assert!(super::native_strategy_register(type_name).is_some());
968 }
969
970 #[rstest]
971 #[case("BookImbalanceActor")]
972 fn test_native_actor_register_accepts_supported_names(#[case] type_name: &str) {
973 assert!(super::native_actor_register(type_name).is_some());
974 }
975
976 #[rstest]
977 fn test_native_register_rejects_unknown_names() {
978 assert!(super::native_strategy_register("UnknownStrategy").is_none());
979 assert!(super::native_actor_register("UnknownActor").is_none());
980 }
981
982 #[rstest]
983 fn test_native_strategy_register_rejects_mismatched_config() {
984 Python::initialize();
985
986 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
987 Python::attach(|py| {
988 let register = super::native_strategy_register("EmaCross").unwrap();
989 let config = PyDict::new(py);
990 let error = register(&mut engine, config.as_any()).unwrap_err();
991
992 assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
993 });
994 }
995
996 #[rstest]
997 fn test_native_actor_register_rejects_mismatched_config() {
998 Python::initialize();
999
1000 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
1001 Python::attach(|py| {
1002 let register = super::native_actor_register("BookImbalanceActor").unwrap();
1003 let config = PyDict::new(py);
1004 let error = register(&mut engine, config.as_any()).unwrap_err();
1005
1006 assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
1007 });
1008 }
1009}
1010
1011pub(crate) fn pyobject_to_fill_model_any(
1012 _py: Python,
1013 obj: &Bound<'_, PyAny>,
1014) -> PyResult<FillModelAny> {
1015 if let Ok(m) = obj.extract::<DefaultFillModel>() {
1016 return Ok(FillModelAny::Default(m));
1017 }
1018
1019 if let Ok(m) = obj.extract::<BestPriceFillModel>() {
1020 return Ok(FillModelAny::BestPrice(m));
1021 }
1022
1023 if let Ok(m) = obj.extract::<OneTickSlippageFillModel>() {
1024 return Ok(FillModelAny::OneTickSlippage(m));
1025 }
1026
1027 if let Ok(m) = obj.extract::<ProbabilisticFillModel>() {
1028 return Ok(FillModelAny::Probabilistic(m));
1029 }
1030
1031 if let Ok(m) = obj.extract::<TwoTierFillModel>() {
1032 return Ok(FillModelAny::TwoTier(m));
1033 }
1034
1035 if let Ok(m) = obj.extract::<ThreeTierFillModel>() {
1036 return Ok(FillModelAny::ThreeTier(m));
1037 }
1038
1039 if let Ok(m) = obj.extract::<LimitOrderPartialFillModel>() {
1040 return Ok(FillModelAny::LimitOrderPartialFill(m));
1041 }
1042
1043 if let Ok(m) = obj.extract::<SizeAwareFillModel>() {
1044 return Ok(FillModelAny::SizeAware(m));
1045 }
1046
1047 if let Ok(m) = obj.extract::<CompetitionAwareFillModel>() {
1048 return Ok(FillModelAny::CompetitionAware(m));
1049 }
1050
1051 if let Ok(m) = obj.extract::<VolumeSensitiveFillModel>() {
1052 return Ok(FillModelAny::VolumeSensitive(m));
1053 }
1054
1055 if let Ok(m) = obj.extract::<MarketHoursFillModel>() {
1056 return Ok(FillModelAny::MarketHours(m));
1057 }
1058
1059 let type_name = obj.get_type().name()?;
1060 Err(to_pytype_err(format!(
1061 "Cannot convert {type_name} to FillModel"
1062 )))
1063}
1064
1065pub(crate) fn pyobject_to_fee_model_any(
1066 _py: Python,
1067 obj: &Bound<'_, PyAny>,
1068) -> PyResult<FeeModelAny> {
1069 if let Ok(m) = obj.extract::<FixedFeeModel>() {
1070 return Ok(FeeModelAny::Fixed(m));
1071 }
1072
1073 if let Ok(m) = obj.extract::<MakerTakerFeeModel>() {
1074 return Ok(FeeModelAny::MakerTaker(m));
1075 }
1076
1077 if let Ok(m) = obj.extract::<PerContractFeeModel>() {
1078 return Ok(FeeModelAny::PerContract(m));
1079 }
1080
1081 if let Ok(m) = obj.extract::<CappedOptionFeeModel>() {
1082 return Ok(FeeModelAny::CappedOption(m));
1083 }
1084
1085 if let Ok(m) = obj.extract::<TieredNotionalOptionFeeModel>() {
1086 return Ok(FeeModelAny::TieredNotionalOption(m));
1087 }
1088
1089 let type_name = obj.get_type().name()?;
1090 Err(to_pytype_err(format!(
1091 "Cannot convert {type_name} to FeeModel"
1092 )))
1093}
1094
1095pub(crate) fn pyobject_to_simulation_module_any(
1096 _py: Python,
1097 obj: &Bound<'_, PyAny>,
1098) -> PyResult<SimulationModuleAny> {
1099 if let Ok(cell) = obj.cast::<FXRolloverInterestModule>() {
1100 let module = cell.borrow().clone();
1101 return Ok(SimulationModuleAny::FXRolloverInterest(module));
1102 }
1103
1104 let type_name = obj.get_type().name()?;
1105 Err(to_pytype_err(format!(
1106 "Cannot convert {type_name} to SimulationModule"
1107 )))
1108}
1109
1110pub(crate) fn pyobject_to_latency_model_any(
1111 _py: Python,
1112 obj: &Bound<'_, PyAny>,
1113) -> PyResult<LatencyModelAny> {
1114 if let Ok(m) = obj.extract::<StaticLatencyModel>() {
1115 return Ok(LatencyModelAny::Static(m));
1116 }
1117
1118 let type_name = obj.get_type().name()?;
1119 Err(to_pytype_err(format!(
1120 "Cannot convert {type_name} to LatencyModel"
1121 )))
1122}
1123
1124pub(crate) fn pyobject_to_margin_model_any(
1125 _py: Python,
1126 obj: &Bound<'_, PyAny>,
1127) -> PyResult<MarginModelAny> {
1128 if let Ok(m) = obj.extract::<StandardMarginModel>() {
1129 return Ok(MarginModelAny::Standard(m));
1130 }
1131
1132 if let Ok(m) = obj.extract::<LeveragedMarginModel>() {
1133 return Ok(MarginModelAny::Leveraged(m));
1134 }
1135
1136 let type_name = obj.get_type().name()?;
1137 Err(to_pytype_err(format!(
1138 "Cannot convert {type_name} to MarginModel"
1139 )))
1140}
1141
1142fn pyobject_to_data(_py: Python, obj: &Bound<'_, PyAny>) -> PyResult<Data> {
1143 if let Ok(delta) = obj.extract::<OrderBookDelta>() {
1144 return Ok(Data::Delta(delta));
1145 }
1146
1147 if let Ok(deltas) = obj.extract::<OrderBookDeltas>() {
1148 return Ok(Data::Deltas(OrderBookDeltas_API::new(deltas)));
1149 }
1150
1151 if let Ok(quote) = obj.extract::<QuoteTick>() {
1152 return Ok(Data::Quote(quote));
1153 }
1154
1155 if let Ok(trade) = obj.extract::<TradeTick>() {
1156 return Ok(Data::Trade(trade));
1157 }
1158
1159 if let Ok(bar) = obj.extract::<Bar>() {
1160 return Ok(Data::Bar(bar));
1161 }
1162
1163 if let Ok(depth) = obj.extract::<OrderBookDepth10>() {
1164 return Ok(Data::Depth10(Box::new(depth)));
1165 }
1166
1167 if let Ok(mark) = obj.extract::<MarkPriceUpdate>() {
1168 return Ok(Data::MarkPriceUpdate(mark));
1169 }
1170
1171 if let Ok(index) = obj.extract::<IndexPriceUpdate>() {
1172 return Ok(Data::IndexPriceUpdate(index));
1173 }
1174
1175 if let Ok(funding_rate) = obj.extract::<FundingRateUpdate>() {
1176 return Ok(Data::FundingRateUpdate(funding_rate));
1177 }
1178
1179 if let Ok(status) = obj.extract::<InstrumentStatus>() {
1180 return Ok(Data::InstrumentStatus(status));
1181 }
1182
1183 if let Ok(greeks) = obj.extract::<OptionGreeks>() {
1184 return Ok(Data::OptionGreeks(greeks));
1185 }
1186
1187 if let Ok(close) = obj.extract::<InstrumentClose>() {
1188 return Ok(Data::InstrumentClose(close));
1189 }
1190
1191 #[cfg(feature = "defi")]
1192 if let Ok(defi) = obj.extract::<DefiData>() {
1193 return Ok(Data::Defi(Box::new(defi)));
1194 }
1195
1196 if let Ok(delta) = OrderBookDelta::from_pyobject(obj) {
1198 return Ok(Data::Delta(delta));
1199 }
1200
1201 if let Ok(quote) = QuoteTick::from_pyobject(obj) {
1202 return Ok(Data::Quote(quote));
1203 }
1204
1205 if let Ok(trade) = TradeTick::from_pyobject(obj) {
1206 return Ok(Data::Trade(trade));
1207 }
1208
1209 if let Ok(bar) = Bar::from_pyobject(obj) {
1210 return Ok(Data::Bar(bar));
1211 }
1212
1213 if let Ok(mark) = MarkPriceUpdate::from_pyobject(obj) {
1214 return Ok(Data::MarkPriceUpdate(mark));
1215 }
1216
1217 if let Ok(index) = IndexPriceUpdate::from_pyobject(obj) {
1218 return Ok(Data::IndexPriceUpdate(index));
1219 }
1220
1221 if let Ok(funding_rate) = FundingRateUpdate::from_pyobject(obj) {
1222 return Ok(Data::FundingRateUpdate(funding_rate));
1223 }
1224
1225 if let Ok(status) = InstrumentStatus::from_pyobject(obj) {
1226 return Ok(Data::InstrumentStatus(status));
1227 }
1228
1229 if let Ok(greeks) = OptionGreeks::from_pyobject(obj) {
1230 return Ok(Data::OptionGreeks(greeks));
1231 }
1232
1233 if let Ok(close) = InstrumentClose::from_pyobject(obj) {
1234 return Ok(Data::InstrumentClose(close));
1235 }
1236
1237 let type_name = obj.get_type().name()?;
1238 Err(to_pytype_err(format!("Cannot convert {type_name} to Data")))
1239}