1use std::collections::HashMap;
19
20use ahash::AHashMap;
21use nautilus_common::{
22 actor::data_actor::ImportableActorConfig,
23 enums::ComponentState,
24 python::{
25 actor::{PyDataActor, register_python_exec_algorithm_endpoint},
26 cache::PyCache,
27 config_error_to_pyvalue_err,
28 },
29};
30use nautilus_core::{
31 UUID4, UnixNanos,
32 python::{to_pyruntime_err, to_pytype_err, to_pyvalue_err},
33};
34use nautilus_execution::{
35 models::{
36 fill::{
37 BestPriceFillModel, CompetitionAwareFillModel, DefaultFillModel, FillModelAny,
38 LimitOrderPartialFillModel, MarketHoursFillModel, OneTickSlippageFillModel,
39 ProbabilisticFillModel, SizeAwareFillModel, ThreeTierFillModel, TwoTierFillModel,
40 VolumeSensitiveFillModel,
41 },
42 latency::{LatencyModelAny, StaticLatencyModel},
43 },
44 python::fee::pyobject_to_fee_model_any,
45};
46#[cfg(feature = "defi")]
47use nautilus_model::defi::DefiData;
48use nautilus_model::{
49 accounts::margin_model::{LeveragedMarginModel, MarginModelAny, StandardMarginModel},
50 data::{
51 Bar, Data, FundingRateUpdate, IndexPriceUpdate, InstrumentClose, InstrumentStatus,
52 MarkPriceUpdate, OptionGreeks, OrderBookDelta, OrderBookDeltas, OrderBookDeltas_API,
53 OrderBookDepth10, QuoteTick, TradeTick,
54 },
55 enums::{AccountType, BookType, OmsType, OtoTriggerMode},
56 identifiers::{
57 AccountId, ActorId, ClientId, ComponentId, ExecAlgorithmId, InstrumentId, StrategyId,
58 TraderId, Venue,
59 },
60 python::instruments::pyobject_to_instrument_any,
61 types::{Currency, Money, Price},
62};
63use nautilus_portfolio::python::PyPortfolio;
64#[cfg(feature = "examples")]
65use nautilus_trading::examples::{
66 actors::{BookImbalanceActor, BookImbalanceActorConfig},
67 strategies::{
68 CompositeMarketMaker, CompositeMarketMakerConfig, DeltaNeutralVol, DeltaNeutralVolConfig,
69 EmaCross, EmaCrossConfig, GridMarketMaker, GridMarketMakerConfig, HurstVpinDirectional,
70 HurstVpinDirectionalConfig,
71 },
72};
73use nautilus_trading::{
74 ImportableExecAlgorithmConfig, ImportableStrategyConfig,
75 algorithm::{TwapAlgorithm, TwapAlgorithmConfig},
76 python::strategy::{PyStrategy, PyStrategyInner},
77};
78use pyo3::prelude::*;
79use rust_decimal::Decimal;
80
81use super::node::create_config_instance;
82use crate::{
83 config::{BacktestEngineConfig, SimulatedVenueConfig},
84 engine::BacktestEngine,
85 modules::{FXRolloverInterestModule, SimulationModuleAny},
86 result::BacktestResult,
87};
88
89#[pyo3::pyclass(
94 module = "nautilus_trader.core.nautilus_pyo3.backtest",
95 name = "BacktestEngine",
96 unsendable
97)]
98#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.backtest")]
99#[derive(Debug)]
100pub struct PyBacktestEngine(BacktestEngine);
101
102#[cfg(feature = "defi")]
105#[pyo3_stub_gen::derive::gen_stub_pymethods]
106#[pymethods]
107impl PyBacktestEngine {
108 #[pyo3(name = "add_defi_data", signature = (data, client_id=None, sort=true))]
110 fn py_add_defi_data(
111 &mut self,
112 data: Vec<DefiData>,
113 client_id: Option<ClientId>,
114 sort: bool,
115 ) -> PyResult<()> {
116 self.0
117 .add_defi_data(data, client_id, sort)
118 .map_err(to_pyruntime_err)
119 }
120}
121
122#[pyo3_stub_gen::derive::gen_stub_pymethods]
123#[pymethods]
124impl PyBacktestEngine {
125 #[new]
126 fn py_new(config: BacktestEngineConfig) -> PyResult<Self> {
127 let engine = BacktestEngine::new(config).map_err(to_pyruntime_err)?;
128 Ok(Self(engine))
129 }
130
131 #[pyo3(
145 name = "add_venue",
146 signature = (
147 venue,
148 oms_type,
149 account_type,
150 starting_balances,
151 base_currency = None,
152 default_leverage = None,
153 leverages = None,
154 margin_model = None,
155 fill_model = None,
156 fee_model = None,
157 latency_model = None,
158 modules = None,
159 book_type = BookType::L1_MBP,
160 routing = false,
161 reject_stop_orders = true,
162 support_gtd_orders = true,
163 support_contingent_orders = true,
164 use_position_ids = true,
165 use_random_ids = false,
166 use_reduce_only = true,
167 use_message_queue = true,
168 use_market_order_acks = false,
169 bar_execution = true,
170 bar_adaptive_high_low_ordering = false,
171 trade_execution = true,
172 liquidity_consumption = false,
173 queue_position = false,
174 allow_cash_borrowing = false,
175 frozen_account = false,
176 oto_trigger_mode = OtoTriggerMode::Partial,
177 price_protection_points = None,
178 settlement_prices = None,
179 liquidation_enabled = false,
180 liquidation_trigger_ratio = None,
181 liquidation_cancel_open_orders = true,
182 )
183 )]
184 #[expect(
185 clippy::fn_params_excessive_bools,
186 clippy::too_many_arguments,
187 reason = "method mirrors the existing Python keyword API"
188 )]
189 fn py_add_venue(
190 &mut self,
191 venue: Venue,
192 oms_type: OmsType,
193 account_type: AccountType,
194 starting_balances: Vec<Money>,
195 base_currency: Option<Currency>,
196 default_leverage: Option<Decimal>,
197 leverages: Option<HashMap<InstrumentId, Decimal>>,
198 margin_model: Option<Py<PyAny>>,
199 fill_model: Option<Py<PyAny>>,
200 fee_model: Option<Py<PyAny>>,
201 latency_model: Option<Py<PyAny>>,
202 modules: Option<Vec<Py<PyAny>>>,
203 book_type: BookType,
204 routing: bool,
205 reject_stop_orders: bool,
206 support_gtd_orders: bool,
207 support_contingent_orders: bool,
208 use_position_ids: bool,
209 use_random_ids: bool,
210 use_reduce_only: bool,
211 use_message_queue: bool,
212 use_market_order_acks: bool,
213 bar_execution: bool,
214 bar_adaptive_high_low_ordering: bool,
215 trade_execution: bool,
216 liquidity_consumption: bool,
217 queue_position: bool,
218 allow_cash_borrowing: bool,
219 frozen_account: bool,
220 oto_trigger_mode: OtoTriggerMode,
221 price_protection_points: Option<u32>,
222 settlement_prices: Option<HashMap<InstrumentId, Price>>,
223 liquidation_enabled: bool,
224 liquidation_trigger_ratio: Option<f64>,
225 liquidation_cancel_open_orders: bool,
226 ) -> PyResult<()> {
227 let leverages: AHashMap<InstrumentId, Decimal> = leverages
228 .map(|m| m.into_iter().collect())
229 .unwrap_or_default();
230 let settlement_prices: AHashMap<InstrumentId, Price> = settlement_prices
231 .map(|m| m.into_iter().collect())
232 .unwrap_or_default();
233 let margin_model = margin_model
234 .map(|obj| Python::attach(|py| pyobject_to_margin_model_any(py, obj.bind(py))))
235 .transpose()?;
236 let fill_model = fill_model
237 .map(|obj| Python::attach(|py| pyobject_to_fill_model_any(py, obj.bind(py))))
238 .transpose()?
239 .unwrap_or_default();
240 let fee_model = fee_model
241 .map(|obj| Python::attach(|py| pyobject_to_fee_model_any(obj.bind(py))))
242 .transpose()?
243 .unwrap_or_default();
244 let latency_model = latency_model
245 .map(|obj| Python::attach(|py| pyobject_to_latency_model_any(py, obj.bind(py))))
246 .transpose()?
247 .map(Into::into);
248 let modules = modules
249 .map(|objs| {
250 objs.into_iter()
251 .map(|obj| {
252 Python::attach(|py| pyobject_to_simulation_module_any(py, obj.bind(py)))
253 })
254 .collect::<PyResult<Vec<_>>>()
255 })
256 .transpose()?
257 .unwrap_or_default()
258 .into_iter()
259 .map(Into::into)
260 .collect();
261
262 let sim_config = SimulatedVenueConfig::builder()
263 .venue(venue)
264 .oms_type(oms_type)
265 .account_type(account_type)
266 .book_type(book_type)
267 .starting_balances(starting_balances)
268 .maybe_base_currency(base_currency)
269 .maybe_default_leverage(default_leverage)
270 .leverages(leverages)
271 .maybe_margin_model(margin_model)
272 .modules(modules)
273 .fill_model(fill_model)
274 .fee_model(fee_model)
275 .maybe_latency_model(latency_model)
276 .routing(routing)
277 .reject_stop_orders(reject_stop_orders)
278 .support_gtd_orders(support_gtd_orders)
279 .support_contingent_orders(support_contingent_orders)
280 .use_position_ids(use_position_ids)
281 .use_random_ids(use_random_ids)
282 .use_reduce_only(use_reduce_only)
283 .use_message_queue(use_message_queue)
284 .use_market_order_acks(use_market_order_acks)
285 .bar_execution(bar_execution)
286 .bar_adaptive_high_low_ordering(bar_adaptive_high_low_ordering)
287 .trade_execution(trade_execution)
288 .liquidity_consumption(liquidity_consumption)
289 .allow_cash_borrowing(allow_cash_borrowing)
290 .frozen_account(frozen_account)
291 .queue_position(queue_position)
292 .oto_full_trigger(oto_trigger_mode == OtoTriggerMode::Full)
293 .maybe_price_protection_points(price_protection_points)
294 .liquidation_enabled(liquidation_enabled)
295 .liquidation_trigger_ratio(liquidation_trigger_ratio.unwrap_or(1.0))
296 .liquidation_cancel_open_orders(liquidation_cancel_open_orders)
297 .build()
298 .map_err(config_error_to_pyvalue_err)?;
299
300 self.0.add_venue(sim_config).map_err(to_pyruntime_err)?;
301
302 for (instrument_id, price) in settlement_prices {
303 self.0
304 .set_settlement_price(venue, instrument_id, price)
305 .map_err(to_pyruntime_err)?;
306 }
307
308 Ok(())
309 }
310
311 #[pyo3(name = "change_fill_model")]
313 #[expect(clippy::needless_pass_by_value)]
314 fn py_change_fill_model(
315 &mut self,
316 py: Python,
317 venue: Venue,
318 fill_model: Py<PyAny>,
319 ) -> PyResult<()> {
320 let fill_model = pyobject_to_fill_model_any(py, fill_model.bind(py))?;
321 self.0.change_fill_model(venue, fill_model);
322 Ok(())
323 }
324
325 #[pyo3(
327 name = "add_data",
328 signature = (data, client_id=None, validate=true, sort=true)
329 )]
330 fn py_add_data(
331 &mut self,
332 py: Python,
333 data: Vec<Py<PyAny>>,
334 client_id: Option<ClientId>,
335 validate: bool,
336 sort: bool,
337 ) -> PyResult<()> {
338 let rust_data: Vec<Data> = data
339 .into_iter()
340 .map(|obj| pyobject_to_data(py, obj.bind(py)))
341 .collect::<PyResult<_>>()?;
342 self.0
343 .add_data(rust_data, client_id, validate, sort)
344 .map_err(to_pyruntime_err)
345 }
346
347 #[pyo3(name = "add_instrument")]
349 fn py_add_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
350 let instrument_any = pyobject_to_instrument_any(py, instrument)?;
351 self.0
352 .add_instrument(&instrument_any)
353 .map_err(to_pyruntime_err)
354 }
355
356 #[pyo3(name = "add_actor")]
360 fn py_add_actor(&mut self, actor: &Bound<'_, PyAny>) -> PyResult<()> {
361 log::debug!("`add_actor` with a constructed instance");
362 self.add_python_actor(&actor.clone().unbind())
363 }
364
365 #[pyo3(name = "add_actor_from_config")]
367 #[expect(clippy::needless_pass_by_value)]
368 fn py_add_actor_from_config(
369 &mut self,
370 _py: Python,
371 config: ImportableActorConfig,
372 ) -> PyResult<()> {
373 log::debug!("`add_actor_from_config` with: {config:?}");
374
375 let parts: Vec<&str> = config.actor_path.split(':').collect();
376 if parts.len() != 2 {
377 return Err(to_pyvalue_err(
378 "actor_path must be in format 'module.path:ClassName'",
379 ));
380 }
381 let (module_name, class_name) = (parts[0], parts[1]);
382
383 log::info!("Importing actor from module: {module_name} class: {class_name}");
384
385 let actor = Python::attach(|py| -> anyhow::Result<Py<PyAny>> {
386 let actor_module = py
387 .import(module_name)
388 .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
389 let actor_class = actor_module
390 .getattr(class_name)
391 .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
392
393 let config_instance = create_config_instance(py, &config.config_path, &config.config)?;
394
395 let python_actor = if let Some(config_obj) = config_instance {
396 actor_class.call1((config_obj,))?
397 } else {
398 actor_class.call0()?
399 };
400
401 Ok(python_actor.unbind())
402 })
403 .map_err(to_pyruntime_err)?;
404
405 self.add_python_actor(&actor)
406 }
407
408 #[pyo3(name = "add_strategy")]
413 fn py_add_strategy(&mut self, strategy: &Bound<'_, PyAny>) -> PyResult<()> {
414 log::debug!("`add_strategy` with a constructed instance");
415 self.add_python_strategy(&strategy.clone().unbind())
416 }
417
418 #[pyo3(name = "add_strategy_from_config")]
420 #[expect(clippy::needless_pass_by_value)]
421 fn py_add_strategy_from_config(
422 &mut self,
423 _py: Python,
424 config: ImportableStrategyConfig,
425 ) -> PyResult<()> {
426 log::debug!("`add_strategy_from_config` with: {config:?}");
427
428 let parts: Vec<&str> = config.strategy_path.split(':').collect();
429 if parts.len() != 2 {
430 return Err(to_pyvalue_err(
431 "strategy_path must be in format 'module.path:ClassName'",
432 ));
433 }
434 let (module_name, class_name) = (parts[0], parts[1]);
435
436 log::info!("Importing strategy from module: {module_name} class: {class_name}");
437
438 let strategy = Python::attach(|py| -> anyhow::Result<Py<PyAny>> {
439 let strategy_module = py
440 .import(module_name)
441 .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
442 let strategy_class = strategy_module
443 .getattr(class_name)
444 .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
445
446 let config_instance = create_config_instance(py, &config.config_path, &config.config)?;
447
448 let python_strategy = if let Some(config_obj) = config_instance {
449 strategy_class.call1((config_obj,))?
450 } else {
451 strategy_class.call0()?
452 };
453
454 Ok(python_strategy.unbind())
455 })
456 .map_err(to_pyruntime_err)?;
457
458 self.add_python_strategy(&strategy)
459 }
460
461 #[pyo3(name = "add_exec_algorithm")]
466 fn py_add_exec_algorithm(&mut self, exec_algorithm: &Bound<'_, PyAny>) -> PyResult<()> {
467 log::debug!("`add_exec_algorithm` with a constructed instance");
468 self.add_python_exec_algorithm(&exec_algorithm.clone().unbind())
469 }
470
471 #[pyo3(name = "add_exec_algorithm_from_config")]
473 #[expect(clippy::needless_pass_by_value)]
474 fn py_add_exec_algorithm_from_config(
475 &mut self,
476 _py: Python,
477 config: ImportableExecAlgorithmConfig,
478 ) -> PyResult<()> {
479 self.ensure_can_add_exec_algorithm()?;
480
481 log::debug!("`add_exec_algorithm_from_config` with: {config:?}");
482
483 let parts: Vec<&str> = config.exec_algorithm_path.split(':').collect();
484 if parts.len() != 2 {
485 return Err(to_pyvalue_err(
486 "exec_algorithm_path must be in format 'module.path:ClassName'",
487 ));
488 }
489 let (module_name, class_name) = (parts[0], parts[1]);
490
491 log::info!("Importing exec algorithm from module: {module_name} class: {class_name}");
492
493 let exec_algorithm = Python::attach(|py| -> anyhow::Result<Py<PyAny>> {
494 let algo_module = py
495 .import(module_name)
496 .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
497 let algo_class = algo_module
498 .getattr(class_name)
499 .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
500
501 let config_instance = create_config_instance(py, &config.config_path, &config.config)?;
502
503 let python_exec_algorithm = if let Some(config_obj) = config_instance {
504 algo_class.call1((config_obj,))?
505 } else {
506 algo_class.call0()?
507 };
508
509 Ok(python_exec_algorithm.unbind())
510 })
511 .map_err(to_pyruntime_err)?;
512
513 self.add_python_exec_algorithm(&exec_algorithm)
514 }
515
516 #[cfg(feature = "examples")]
522 #[pyo3(name = "add_builtin_actor")]
523 fn py_add_builtin_actor(&mut self, type_name: &str, config: &Bound<'_, PyAny>) -> PyResult<()> {
524 let register = builtin_actor_register(type_name).ok_or_else(|| {
525 to_pytype_err(format!("Unsupported built-in actor type: {type_name}"))
526 })?;
527 register(&mut self.0, config)
528 }
529
530 #[cfg(feature = "examples")]
536 #[pyo3(name = "add_builtin_strategy")]
537 fn py_add_builtin_strategy(
538 &mut self,
539 type_name: &str,
540 config: &Bound<'_, PyAny>,
541 ) -> PyResult<()> {
542 let register = builtin_strategy_register(type_name).ok_or_else(|| {
543 to_pytype_err(format!("Unsupported built-in strategy type: {type_name}"))
544 })?;
545 register(&mut self.0, config)
546 }
547
548 #[pyo3(name = "add_native_exec_algorithm")]
553 fn py_add_native_exec_algorithm(
554 &mut self,
555 type_name: &str,
556 config: &Bound<'_, PyAny>,
557 ) -> PyResult<()> {
558 let register = native_exec_algorithm_register(type_name).ok_or_else(|| {
559 to_pytype_err(format!(
560 "Unsupported native exec algorithm type: {type_name}"
561 ))
562 })?;
563 register(&mut self.0, config)
564 }
565
566 #[pyo3(
568 name = "run",
569 signature = (start=None, end=None, run_config_id=None, streaming=false)
570 )]
571 fn py_run(
572 &mut self,
573 start: Option<u64>,
574 end: Option<u64>,
575 run_config_id: Option<String>,
576 streaming: bool,
577 ) -> PyResult<()> {
578 self.0
579 .run(
580 start.map(UnixNanos::from),
581 end.map(UnixNanos::from),
582 run_config_id,
583 streaming,
584 )
585 .map_err(to_pyruntime_err)
586 }
587
588 #[pyo3(name = "end")]
590 fn py_end(&mut self) {
591 self.0.end();
592 }
593
594 #[pyo3(name = "reset")]
596 fn py_reset(&mut self) {
597 self.0.reset();
598 }
599
600 #[pyo3(name = "dispose")]
602 fn py_dispose(&mut self) {
603 self.0.dispose();
604 }
605
606 #[pyo3(name = "get_result")]
608 fn py_get_result(&self) -> BacktestResult {
609 self.0.get_result()
610 }
611
612 #[pyo3(name = "clear_data")]
614 fn py_clear_data(&mut self) {
615 self.0.clear_data();
616 }
617
618 #[pyo3(name = "clear_actors")]
620 fn py_clear_actors(&mut self) -> PyResult<()> {
621 self.0.clear_actors().map_err(to_pyruntime_err)
622 }
623
624 #[pyo3(name = "clear_strategies")]
626 fn py_clear_strategies(&mut self) -> PyResult<()> {
627 self.0.clear_strategies().map_err(to_pyruntime_err)
628 }
629
630 #[pyo3(name = "clear_exec_algorithms")]
632 fn py_clear_exec_algorithms(&mut self) -> PyResult<()> {
633 self.0.clear_exec_algorithms().map_err(to_pyruntime_err)
634 }
635
636 #[pyo3(name = "add_actors_from_configs")]
638 fn py_add_actors_from_configs(
639 &mut self,
640 py: Python,
641 configs: Vec<ImportableActorConfig>,
642 ) -> PyResult<()> {
643 for config in configs {
644 self.py_add_actor_from_config(py, config)?;
645 }
646 Ok(())
647 }
648
649 #[pyo3(name = "add_strategies_from_configs")]
651 fn py_add_strategies_from_configs(
652 &mut self,
653 py: Python,
654 configs: Vec<ImportableStrategyConfig>,
655 ) -> PyResult<()> {
656 for config in configs {
657 self.py_add_strategy_from_config(py, config)?;
658 }
659 Ok(())
660 }
661
662 #[pyo3(name = "add_exec_algorithms_from_configs")]
664 fn py_add_exec_algorithms_from_configs(
665 &mut self,
666 py: Python,
667 configs: Vec<ImportableExecAlgorithmConfig>,
668 ) -> PyResult<()> {
669 for config in configs {
670 self.py_add_exec_algorithm_from_config(py, config)?;
671 }
672 Ok(())
673 }
674
675 #[pyo3(name = "add_actors")]
677 fn py_add_actors(&mut self, actors: Vec<Py<PyAny>>) -> PyResult<()> {
678 for actor in actors {
679 self.add_python_actor(&actor)?;
680 }
681 Ok(())
682 }
683
684 #[pyo3(name = "add_strategies")]
686 fn py_add_strategies(&mut self, strategies: Vec<Py<PyAny>>) -> PyResult<()> {
687 for strategy in strategies {
688 self.add_python_strategy(&strategy)?;
689 }
690 Ok(())
691 }
692
693 #[pyo3(name = "add_exec_algorithms")]
695 fn py_add_exec_algorithms(&mut self, exec_algorithms: Vec<Py<PyAny>>) -> PyResult<()> {
696 for exec_algorithm in exec_algorithms {
697 self.add_python_exec_algorithm(&exec_algorithm)?;
698 }
699 Ok(())
700 }
701
702 #[pyo3(name = "sort_data")]
704 fn py_sort_data(&mut self) {
705 self.0.sort_data();
706 }
707
708 #[getter]
710 #[pyo3(name = "trader_id")]
711 fn py_trader_id(&self) -> TraderId {
712 self.0.trader_id()
713 }
714
715 #[getter]
717 #[pyo3(name = "machine_id")]
718 fn py_machine_id(&self) -> String {
719 self.0.machine_id().to_string()
720 }
721
722 #[getter]
724 #[pyo3(name = "instance_id")]
725 fn py_instance_id(&self) -> UUID4 {
726 self.0.instance_id()
727 }
728
729 #[getter]
731 #[pyo3(name = "iteration")]
732 fn py_iteration(&self) -> usize {
733 self.0.iteration()
734 }
735
736 #[getter]
738 #[pyo3(name = "run_config_id")]
739 fn py_run_config_id(&self) -> Option<String> {
740 self.0.run_config_id().map(str::to_string)
741 }
742
743 #[getter]
745 #[pyo3(name = "run_id")]
746 fn py_run_id(&self) -> Option<UUID4> {
747 self.0.run_id()
748 }
749
750 #[getter]
752 #[pyo3(name = "run_started")]
753 fn py_run_started(&self) -> Option<u64> {
754 self.0.run_started().map(|n| n.as_u64())
755 }
756
757 #[getter]
759 #[pyo3(name = "run_finished")]
760 fn py_run_finished(&self) -> Option<u64> {
761 self.0.run_finished().map(|n| n.as_u64())
762 }
763
764 #[getter]
766 #[pyo3(name = "backtest_start")]
767 fn py_backtest_start(&self) -> Option<u64> {
768 self.0.backtest_start().map(|n| n.as_u64())
769 }
770
771 #[getter]
773 #[pyo3(name = "backtest_end")]
774 fn py_backtest_end(&self) -> Option<u64> {
775 self.0.backtest_end().map(|n| n.as_u64())
776 }
777
778 #[pyo3(name = "list_venues")]
780 fn py_list_venues(&self) -> Vec<Venue> {
781 self.0.list_venues()
782 }
783
784 #[getter]
786 #[pyo3(name = "cache")]
787 fn py_cache(&self) -> PyCache {
788 PyCache::from_rc(self.0.kernel().cache.clone())
789 }
790
791 #[getter]
793 #[pyo3(name = "portfolio")]
794 fn py_portfolio(&self) -> PyPortfolio {
795 PyPortfolio::from_rc(self.0.kernel().portfolio.clone())
796 }
797
798 #[pyo3(name = "generate_orders_report")]
804 fn py_generate_orders_report<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
805 let orders = self.cache_bound(py)?.call_method0("orders")?;
806 Self::report_provider(py)?.call_method1("generate_orders_report", (orders,))
807 }
808
809 #[pyo3(name = "generate_order_fills_report")]
815 fn py_generate_order_fills_report<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
816 let orders = self.cache_bound(py)?.call_method0("orders")?;
817 Self::report_provider(py)?.call_method1("generate_order_fills_report", (orders,))
818 }
819
820 #[pyo3(name = "generate_fills_report")]
826 fn py_generate_fills_report<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
827 let orders = self.cache_bound(py)?.call_method0("orders")?;
828 Self::report_provider(py)?.call_method1("generate_fills_report", (orders,))
829 }
830
831 #[pyo3(name = "generate_positions_report")]
837 fn py_generate_positions_report<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
838 let cache = self.cache_bound(py)?;
839 let positions = cache.call_method0("positions")?;
840 let snapshots = cache.call_method0("position_snapshots")?;
841 Self::report_provider(py)?.call_method1("generate_positions_report", (positions, snapshots))
842 }
843
844 #[pyo3(name = "generate_account_report", signature = (venue=None, account_id=None))]
853 fn py_generate_account_report<'py>(
854 &self,
855 py: Python<'py>,
856 venue: Option<Venue>,
857 account_id: Option<AccountId>,
858 ) -> PyResult<Bound<'py, PyAny>> {
859 let cache = self.cache_bound(py)?;
860 let account = match (account_id, venue) {
861 (Some(aid), _) => cache.call_method1("account", (aid,))?,
862 (None, Some(v)) => cache.call_method1("account_for_venue", (v,))?,
863 (None, None) => {
864 return Err(to_pyvalue_err(
865 "At least one of 'venue' or 'account_id' must be provided",
866 ));
867 }
868 };
869
870 if account.is_none() {
871 return py.import("pandas")?.call_method0("DataFrame");
872 }
873 Self::report_provider(py)?.call_method1("generate_account_report", (account,))
874 }
875
876 fn __repr__(&self) -> String {
877 format!("{:?}", self.0)
878 }
879}
880
881impl PyBacktestEngine {
882 fn cache_bound<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyCache>> {
883 Ok(Py::new(py, self.py_cache())?.into_bound(py))
884 }
885
886 fn report_provider(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
887 py.import("nautilus_trader.analysis.reporter")?
888 .getattr("ReportProvider")
889 }
890
891 #[must_use]
893 pub fn inner(&self) -> &BacktestEngine {
894 &self.0
895 }
896
897 pub fn inner_mut(&mut self) -> &mut BacktestEngine {
899 &mut self.0
900 }
901
902 #[allow(
909 unsafe_code,
910 reason = "Required for Python strategy component registration"
911 )]
912 fn add_python_strategy(&mut self, strategy: &Py<PyAny>) -> PyResult<()> {
913 let strategy_id = Python::attach(|py| -> anyhow::Result<StrategyId> {
914 let bound = strategy.bind(py);
915
916 let config_instance = bound
917 .getattr("config")
918 .ok()
919 .filter(|config| !config.is_none());
920
921 let mut py_strategy_ref = bound
922 .extract::<PyRefMut<PyStrategy>>()
923 .map_err(Into::<PyErr>::into)
924 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
925
926 if let Some(config_obj) = config_instance.as_ref() {
927 if let Ok(strategy_id) = config_obj.getattr("strategy_id")
928 && !strategy_id.is_none()
929 {
930 let strategy_id_val = if let Ok(sid) = strategy_id.extract::<StrategyId>() {
931 sid
932 } else if let Ok(sid_str) = strategy_id.extract::<String>() {
933 StrategyId::new_checked(&sid_str)?
934 } else {
935 anyhow::bail!("Invalid `strategy_id` type");
936 };
937 py_strategy_ref.set_strategy_id(strategy_id_val)?;
938 }
939
940 if let Ok(order_id_tag) = config_obj.getattr("order_id_tag")
941 && !order_id_tag.is_none()
942 {
943 let order_id_tag_val = order_id_tag
944 .extract::<String>()
945 .map_err(|e| anyhow::anyhow!("Invalid `order_id_tag` type: {e}"))?;
946 py_strategy_ref.set_order_id_tag(&order_id_tag_val)?;
947 }
948
949 if let Ok(log_events) = config_obj.getattr("log_events")
950 && let Ok(log_events_val) = log_events.extract::<bool>()
951 {
952 py_strategy_ref.set_log_events(log_events_val);
953 }
954
955 if let Ok(log_commands) = config_obj.getattr("log_commands")
956 && let Ok(log_commands_val) = log_commands.extract::<bool>()
957 {
958 py_strategy_ref.set_log_commands(log_commands_val);
959 }
960 }
961
962 py_strategy_ref.set_python_instance(strategy.clone_ref(py));
963 let strategy_id = py_strategy_ref.strategy_id();
964
965 Ok(strategy_id)
966 })
967 .map_err(to_pyruntime_err)?;
968
969 if self
970 .0
971 .kernel()
972 .trader
973 .borrow()
974 .strategy_ids()
975 .contains(&strategy_id)
976 {
977 return Err(to_pyruntime_err(format!(
978 "Strategy '{strategy_id}' is already registered"
979 )));
980 }
981
982 let trader_id = self.0.kernel().config.trader_id();
983 let cache = self.0.kernel().cache.clone();
984 let portfolio = self.0.kernel().portfolio.clone();
985 let component_id = ComponentId::new(strategy_id.inner().as_str());
986 let clock = self
987 .0
988 .kernel_mut()
989 .trader
990 .borrow_mut()
991 .create_component_clock(component_id);
992
993 Python::attach(|py| -> anyhow::Result<()> {
994 let py_strategy = strategy.bind(py);
995 let mut py_strategy_ref = py_strategy
996 .extract::<PyRefMut<PyStrategy>>()
997 .map_err(Into::<PyErr>::into)
998 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
999
1000 py_strategy_ref
1001 .register(trader_id, clock, cache, portfolio)
1002 .map_err(|e| anyhow::anyhow!("Failed to register PyStrategy: {e}"))?;
1003
1004 Ok(())
1005 })
1006 .map_err(to_pyruntime_err)?;
1007
1008 Python::attach(|py| -> anyhow::Result<()> {
1009 let py_strategy = strategy.bind(py);
1010 let py_strategy_ref = py_strategy
1011 .cast::<PyStrategy>()
1012 .map_err(|e| anyhow::anyhow!("Failed to downcast to PyStrategy: {e}"))?;
1013 py_strategy_ref.borrow().register_in_global_registries();
1014 Ok(())
1015 })
1016 .map_err(to_pyruntime_err)?;
1017
1018 self.0
1019 .kernel_mut()
1020 .trader
1021 .borrow_mut()
1022 .add_strategy_id_with_subscriptions::<PyStrategyInner>(strategy_id)
1023 .map_err(to_pyruntime_err)?;
1024
1025 log::info!("Registered Python strategy {strategy_id}");
1026 Ok(())
1027 }
1028
1029 #[allow(
1035 unsafe_code,
1036 reason = "Required for Python actor component registration"
1037 )]
1038 fn add_python_actor(&mut self, actor: &Py<PyAny>) -> PyResult<()> {
1039 let actor_id = Python::attach(|py| -> anyhow::Result<ActorId> {
1040 let bound = actor.bind(py);
1041
1042 let config_instance = bound
1043 .getattr("config")
1044 .ok()
1045 .filter(|config| !config.is_none());
1046
1047 let mut py_data_actor_ref = bound
1048 .extract::<PyRefMut<PyDataActor>>()
1049 .map_err(Into::<PyErr>::into)
1050 .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
1051
1052 if let Some(config_obj) = config_instance.as_ref() {
1053 if let Ok(actor_id) = config_obj.getattr("actor_id")
1054 && !actor_id.is_none()
1055 {
1056 let actor_id_val = if let Ok(actor_id_val) = actor_id.extract::<ActorId>() {
1057 actor_id_val
1058 } else if let Ok(actor_id_str) = actor_id.extract::<String>() {
1059 ActorId::new_checked(&actor_id_str)?
1060 } else {
1061 anyhow::bail!("Invalid `actor_id` type");
1062 };
1063 py_data_actor_ref.set_actor_id(actor_id_val);
1064 }
1065
1066 if let Ok(log_events) = config_obj.getattr("log_events")
1067 && let Ok(log_events_val) = log_events.extract::<bool>()
1068 {
1069 py_data_actor_ref.set_log_events(log_events_val);
1070 }
1071
1072 if let Ok(log_commands) = config_obj.getattr("log_commands")
1073 && let Ok(log_commands_val) = log_commands.extract::<bool>()
1074 {
1075 py_data_actor_ref.set_log_commands(log_commands_val);
1076 }
1077 }
1078
1079 py_data_actor_ref.set_python_instance(actor.clone_ref(py));
1080 let actor_id = py_data_actor_ref.actor_id();
1081
1082 Ok(actor_id)
1083 })
1084 .map_err(to_pyruntime_err)?;
1085
1086 if self
1087 .0
1088 .kernel()
1089 .trader
1090 .borrow()
1091 .actor_ids()
1092 .contains(&actor_id)
1093 {
1094 return Err(to_pyruntime_err(format!(
1095 "Actor '{actor_id}' is already registered"
1096 )));
1097 }
1098
1099 let trader_id = self.0.kernel().config.trader_id();
1100 let cache = self.0.kernel().cache.clone();
1101 let component_id = ComponentId::new(actor_id.inner().as_str());
1102 let clock = self
1103 .0
1104 .kernel_mut()
1105 .trader
1106 .borrow_mut()
1107 .create_component_clock(component_id);
1108
1109 Python::attach(|py| -> anyhow::Result<()> {
1110 let py_actor = actor.bind(py);
1111 let mut py_data_actor_ref = py_actor
1112 .extract::<PyRefMut<PyDataActor>>()
1113 .map_err(Into::<PyErr>::into)
1114 .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
1115
1116 py_data_actor_ref
1117 .register(trader_id, clock, cache)
1118 .map_err(|e| anyhow::anyhow!("Failed to register PyDataActor: {e}"))?;
1119
1120 Ok(())
1121 })
1122 .map_err(to_pyruntime_err)?;
1123
1124 Python::attach(|py| -> anyhow::Result<()> {
1125 let py_actor = actor.bind(py);
1126 let py_data_actor_ref = py_actor
1127 .cast::<PyDataActor>()
1128 .map_err(|e| anyhow::anyhow!("Failed to downcast to PyDataActor: {e}"))?;
1129 py_data_actor_ref.borrow().register_in_global_registries();
1130 Ok(())
1131 })
1132 .map_err(to_pyruntime_err)?;
1133
1134 self.0
1135 .kernel_mut()
1136 .trader
1137 .borrow_mut()
1138 .add_actor_id_for_lifecycle(actor_id)
1139 .map_err(to_pyruntime_err)?;
1140
1141 log::info!("Registered Python actor {actor_id}");
1142 Ok(())
1143 }
1144
1145 #[allow(
1151 unsafe_code,
1152 reason = "Required for Python exec algorithm component registration"
1153 )]
1154 fn add_python_exec_algorithm(&mut self, exec_algorithm: &Py<PyAny>) -> PyResult<()> {
1155 self.ensure_can_add_exec_algorithm()?;
1156
1157 let actor_id = Python::attach(|py| -> anyhow::Result<ActorId> {
1158 let bound = exec_algorithm.bind(py);
1159
1160 let config_instance = bound
1161 .getattr("config")
1162 .ok()
1163 .filter(|config| !config.is_none());
1164
1165 let mut py_data_actor_ref = bound
1166 .extract::<PyRefMut<PyDataActor>>()
1167 .map_err(Into::<PyErr>::into)
1168 .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
1169
1170 if let Some(config_obj) = config_instance.as_ref() {
1171 let id_attr = config_obj
1172 .getattr("exec_algorithm_id")
1173 .ok()
1174 .filter(|v| !v.is_none())
1175 .or_else(|| config_obj.getattr("actor_id").ok().filter(|v| !v.is_none()));
1176
1177 if let Some(id_value) = id_attr {
1178 let actor_id_val = if let Ok(eaid) = id_value.extract::<ExecAlgorithmId>() {
1179 ActorId::new(eaid.inner().as_str())
1180 } else if let Ok(aid) = id_value.extract::<ActorId>() {
1181 aid
1182 } else if let Ok(aid_str) = id_value.extract::<String>() {
1183 ActorId::new_checked(&aid_str)?
1184 } else {
1185 anyhow::bail!("Invalid `exec_algorithm_id`/`actor_id` type");
1186 };
1187 py_data_actor_ref.set_actor_id(actor_id_val);
1188 }
1189
1190 if let Ok(log_events) = config_obj.getattr("log_events")
1191 && let Ok(log_events_val) = log_events.extract::<bool>()
1192 {
1193 py_data_actor_ref.set_log_events(log_events_val);
1194 }
1195
1196 if let Ok(log_commands) = config_obj.getattr("log_commands")
1197 && let Ok(log_commands_val) = log_commands.extract::<bool>()
1198 {
1199 py_data_actor_ref.set_log_commands(log_commands_val);
1200 }
1201 }
1202
1203 py_data_actor_ref.set_python_instance(exec_algorithm.clone_ref(py));
1204 let actor_id = py_data_actor_ref.actor_id();
1205
1206 Ok(actor_id)
1207 })
1208 .map_err(to_pyruntime_err)?;
1209
1210 let exec_algorithm_id = ExecAlgorithmId::from(actor_id.inner().as_str());
1211
1212 if self
1213 .0
1214 .kernel()
1215 .trader
1216 .borrow()
1217 .exec_algorithm_ids()
1218 .contains(&exec_algorithm_id)
1219 {
1220 return Err(to_pyruntime_err(format!(
1221 "Execution algorithm '{exec_algorithm_id}' is already registered"
1222 )));
1223 }
1224
1225 let trader_id = self.0.kernel().config.trader_id();
1226 let cache = self.0.kernel().cache.clone();
1227 let component_id = ComponentId::new(actor_id.inner().as_str());
1228 let clock = self
1229 .0
1230 .kernel_mut()
1231 .trader
1232 .borrow_mut()
1233 .create_component_clock(component_id);
1234
1235 Python::attach(|py| -> anyhow::Result<()> {
1236 let py_algo = exec_algorithm.bind(py);
1237 let mut py_data_actor_ref = py_algo
1238 .extract::<PyRefMut<PyDataActor>>()
1239 .map_err(Into::<PyErr>::into)
1240 .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
1241
1242 py_data_actor_ref
1243 .register(trader_id, clock, cache)
1244 .map_err(|e| anyhow::anyhow!("Failed to register PyDataActor: {e}"))?;
1245
1246 Ok(())
1247 })
1248 .map_err(to_pyruntime_err)?;
1249
1250 Python::attach(|py| -> anyhow::Result<()> {
1251 let py_algo = exec_algorithm.bind(py);
1252 let py_data_actor_ref = py_algo
1253 .cast::<PyDataActor>()
1254 .map_err(|e| anyhow::anyhow!("Failed to downcast to PyDataActor: {e}"))?;
1255 py_data_actor_ref.borrow().register_in_global_registries();
1256 Ok(())
1257 })
1258 .map_err(to_pyruntime_err)?;
1259
1260 register_python_exec_algorithm_endpoint(exec_algorithm_id);
1261
1262 self.0
1263 .kernel_mut()
1264 .trader
1265 .borrow_mut()
1266 .add_exec_algorithm_id_for_lifecycle(exec_algorithm_id)
1267 .map_err(to_pyruntime_err)?;
1268
1269 log::info!("Registered Python exec algorithm {exec_algorithm_id}");
1270 Ok(())
1271 }
1272
1273 fn ensure_can_add_exec_algorithm(&self) -> PyResult<()> {
1278 match self.0.kernel().trader.borrow().state() {
1279 ComponentState::PreInitialized | ComponentState::Ready | ComponentState::Stopped => {
1280 Ok(())
1281 }
1282 ComponentState::Running => Err(to_pyruntime_err(
1283 "Cannot add execution algorithms to running trader",
1284 )),
1285 ComponentState::Disposed => {
1286 Err(to_pyruntime_err("Cannot add components to disposed trader"))
1287 }
1288 state => Err(to_pyruntime_err(format!(
1289 "Cannot add execution algorithms in current state: {state}"
1290 ))),
1291 }
1292 }
1293}
1294
1295#[cfg(feature = "examples")]
1296type BuiltinActorRegister = for<'py> fn(&mut BacktestEngine, &Bound<'py, PyAny>) -> PyResult<()>;
1297
1298#[cfg(feature = "examples")]
1299type BuiltinStrategyRegister = for<'py> fn(&mut BacktestEngine, &Bound<'py, PyAny>) -> PyResult<()>;
1300
1301#[cfg(feature = "examples")]
1302fn builtin_actor_register(type_name: &str) -> Option<BuiltinActorRegister> {
1303 match type_name {
1304 "BookImbalanceActor" => Some(register_book_imbalance_actor),
1305 _ => None,
1306 }
1307}
1308
1309#[cfg(feature = "examples")]
1310fn builtin_strategy_register(type_name: &str) -> Option<BuiltinStrategyRegister> {
1311 match type_name {
1312 "CompositeMarketMaker" => Some(register_composite_market_maker),
1313 "DeltaNeutralVol" => Some(register_delta_neutral_vol),
1314 "EmaCross" => Some(register_ema_cross),
1315 "GridMarketMaker" => Some(register_grid_market_maker),
1316 "HurstVpinDirectional" => Some(register_hurst_vpin_directional),
1317 _ => None,
1318 }
1319}
1320
1321type NativeExecAlgorithmRegister =
1322 for<'py> fn(&mut BacktestEngine, &Bound<'py, PyAny>) -> PyResult<()>;
1323
1324fn native_exec_algorithm_register(type_name: &str) -> Option<NativeExecAlgorithmRegister> {
1325 match type_name {
1326 "TwapAlgorithm" => Some(register_twap_algorithm),
1327 _ => None,
1328 }
1329}
1330
1331fn register_twap_algorithm(engine: &mut BacktestEngine, config: &Bound<'_, PyAny>) -> PyResult<()> {
1332 let config = config.extract::<TwapAlgorithmConfig>()?;
1333 if config.exec_algorithm_id.is_none() {
1334 return Err(to_pyvalue_err(
1335 "TwapAlgorithm config requires `exec_algorithm_id`",
1336 ));
1337 }
1338 engine
1339 .add_exec_algorithm(TwapAlgorithm::new(config))
1340 .map_err(to_pyruntime_err)
1341}
1342
1343#[cfg(feature = "examples")]
1344fn register_composite_market_maker(
1345 engine: &mut BacktestEngine,
1346 config: &Bound<'_, PyAny>,
1347) -> PyResult<()> {
1348 let config = config.extract::<CompositeMarketMakerConfig>()?;
1349 engine
1350 .add_strategy(CompositeMarketMaker::new(config))
1351 .map_err(to_pyruntime_err)
1352}
1353
1354#[cfg(feature = "examples")]
1355fn register_delta_neutral_vol(
1356 engine: &mut BacktestEngine,
1357 config: &Bound<'_, PyAny>,
1358) -> PyResult<()> {
1359 let config = config.extract::<DeltaNeutralVolConfig>()?;
1360 engine
1361 .add_strategy(DeltaNeutralVol::new(config))
1362 .map_err(to_pyruntime_err)
1363}
1364
1365#[cfg(feature = "examples")]
1366fn register_ema_cross(engine: &mut BacktestEngine, config: &Bound<'_, PyAny>) -> PyResult<()> {
1367 let config = config.extract::<EmaCrossConfig>()?;
1368 engine
1369 .add_strategy(EmaCross::from_config(config))
1370 .map_err(to_pyruntime_err)
1371}
1372
1373#[cfg(feature = "examples")]
1374fn register_grid_market_maker(
1375 engine: &mut BacktestEngine,
1376 config: &Bound<'_, PyAny>,
1377) -> PyResult<()> {
1378 let config = config.extract::<GridMarketMakerConfig>()?;
1379 engine
1380 .add_strategy(GridMarketMaker::new(config))
1381 .map_err(to_pyruntime_err)
1382}
1383
1384#[cfg(feature = "examples")]
1385fn register_hurst_vpin_directional(
1386 engine: &mut BacktestEngine,
1387 config: &Bound<'_, PyAny>,
1388) -> PyResult<()> {
1389 let config = config.extract::<HurstVpinDirectionalConfig>()?;
1390 engine
1391 .add_strategy(HurstVpinDirectional::new(config))
1392 .map_err(to_pyruntime_err)
1393}
1394
1395#[cfg(feature = "examples")]
1396fn register_book_imbalance_actor(
1397 engine: &mut BacktestEngine,
1398 config: &Bound<'_, PyAny>,
1399) -> PyResult<()> {
1400 let config = config.extract::<BookImbalanceActorConfig>()?;
1401 engine
1402 .add_actor(BookImbalanceActor::from_config(config))
1403 .map_err(to_pyruntime_err)
1404}
1405
1406#[cfg(all(test, feature = "examples"))]
1407mod tests {
1408 use pyo3::{Python, types::PyDict};
1409 use rstest::rstest;
1410
1411 use crate::{config::BacktestEngineConfig, engine::BacktestEngine};
1412
1413 #[rstest]
1414 #[case("CompositeMarketMaker")]
1415 #[case("DeltaNeutralVol")]
1416 #[case("EmaCross")]
1417 #[case("GridMarketMaker")]
1418 #[case("HurstVpinDirectional")]
1419 fn test_builtin_strategy_register_accepts_supported_names(#[case] type_name: &str) {
1420 assert!(super::builtin_strategy_register(type_name).is_some());
1421 }
1422
1423 #[rstest]
1424 #[case("BookImbalanceActor")]
1425 fn test_builtin_actor_register_accepts_supported_names(#[case] type_name: &str) {
1426 assert!(super::builtin_actor_register(type_name).is_some());
1427 }
1428
1429 #[rstest]
1430 fn test_builtin_register_rejects_unknown_names() {
1431 assert!(super::builtin_strategy_register("UnknownStrategy").is_none());
1432 assert!(super::builtin_actor_register("UnknownActor").is_none());
1433 }
1434
1435 #[rstest]
1436 fn test_builtin_strategy_register_rejects_mismatched_config() {
1437 Python::initialize();
1438
1439 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
1440 Python::attach(|py| {
1441 let register = super::builtin_strategy_register("EmaCross").unwrap();
1442 let config = PyDict::new(py);
1443 let error = register(&mut engine, config.as_any()).unwrap_err();
1444
1445 assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
1446 });
1447 }
1448
1449 #[rstest]
1450 fn test_builtin_actor_register_rejects_mismatched_config() {
1451 Python::initialize();
1452
1453 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
1454 Python::attach(|py| {
1455 let register = super::builtin_actor_register("BookImbalanceActor").unwrap();
1456 let config = PyDict::new(py);
1457 let error = register(&mut engine, config.as_any()).unwrap_err();
1458
1459 assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
1460 });
1461 }
1462
1463 #[rstest]
1464 fn test_add_strategy_registers_python_instance() {
1465 use nautilus_model::identifiers::StrategyId;
1466 use nautilus_trading::python::strategy::PyStrategy;
1467 use pyo3::{ffi::c_str, types::PyAnyMethods};
1468
1469 Python::initialize();
1470
1471 let mut engine =
1472 super::PyBacktestEngine(BacktestEngine::new(BacktestEngineConfig::default()).unwrap());
1473
1474 Python::attach(|py| {
1475 let config = py
1476 .eval(
1477 c_str!("type('_Cfg', (), {'strategy_id': 'S-INSTANCE-001'})()"),
1478 None,
1479 None,
1480 )
1481 .unwrap();
1482 let instance = py
1483 .get_type::<PyStrategy>()
1484 .as_any()
1485 .call1((config,))
1486 .unwrap();
1487
1488 engine.py_add_strategy(&instance).unwrap();
1489
1490 assert!(
1491 engine
1492 .0
1493 .kernel()
1494 .trader
1495 .borrow()
1496 .strategy_ids()
1497 .contains(&StrategyId::from("S-INSTANCE-001"))
1498 );
1499 });
1500 }
1501
1502 #[rstest]
1503 fn test_add_actor_registers_python_instance() {
1504 use nautilus_common::python::actor::PyDataActor;
1505 use nautilus_model::identifiers::ActorId;
1506 use pyo3::{ffi::c_str, types::PyAnyMethods};
1507
1508 Python::initialize();
1509
1510 let mut engine =
1511 super::PyBacktestEngine(BacktestEngine::new(BacktestEngineConfig::default()).unwrap());
1512
1513 Python::attach(|py| {
1514 let config = py
1515 .eval(
1516 c_str!("type('_Cfg', (), {'actor_id': 'A-INSTANCE-001'})()"),
1517 None,
1518 None,
1519 )
1520 .unwrap();
1521 let instance = py
1522 .get_type::<PyDataActor>()
1523 .as_any()
1524 .call1((config,))
1525 .unwrap();
1526
1527 engine.py_add_actor(&instance).unwrap();
1528
1529 assert!(
1530 engine
1531 .0
1532 .kernel()
1533 .trader
1534 .borrow()
1535 .actor_ids()
1536 .contains(&ActorId::from("A-INSTANCE-001"))
1537 );
1538 });
1539 }
1540
1541 #[rstest]
1542 fn test_add_exec_algorithm_registers_python_instance() {
1543 use nautilus_common::python::actor::PyDataActor;
1544 use nautilus_model::identifiers::ExecAlgorithmId;
1545 use pyo3::{ffi::c_str, types::PyAnyMethods};
1546
1547 Python::initialize();
1548
1549 let mut engine =
1550 super::PyBacktestEngine(BacktestEngine::new(BacktestEngineConfig::default()).unwrap());
1551
1552 Python::attach(|py| {
1553 let config = py
1554 .eval(
1555 c_str!("type('_Cfg', (), {'exec_algorithm_id': 'EXEC-INSTANCE-001'})()"),
1556 None,
1557 None,
1558 )
1559 .unwrap();
1560 let instance = py
1561 .get_type::<PyDataActor>()
1562 .as_any()
1563 .call1((config,))
1564 .unwrap();
1565
1566 engine.py_add_exec_algorithm(&instance).unwrap();
1567
1568 assert!(
1569 engine
1570 .0
1571 .kernel()
1572 .trader
1573 .borrow()
1574 .exec_algorithm_ids()
1575 .contains(&ExecAlgorithmId::from("EXEC-INSTANCE-001"))
1576 );
1577 });
1578 }
1579
1580 #[rstest]
1581 fn test_add_strategies_registers_multiple_python_instances() {
1582 use nautilus_model::identifiers::StrategyId;
1583 use nautilus_trading::python::strategy::PyStrategy;
1584 use pyo3::{ffi::c_str, types::PyAnyMethods};
1585
1586 Python::initialize();
1587
1588 let mut engine =
1589 super::PyBacktestEngine(BacktestEngine::new(BacktestEngineConfig::default()).unwrap());
1590
1591 Python::attach(|py| {
1592 let strategy_type = py.get_type::<PyStrategy>();
1593 let first_config = py
1594 .eval(
1595 c_str!("type('_Cfg', (), {'strategy_id': 'S-MULTI-001'})()"),
1596 None,
1597 None,
1598 )
1599 .unwrap();
1600 let second_config = py
1601 .eval(
1602 c_str!("type('_Cfg', (), {'strategy_id': 'S-MULTI-002'})()"),
1603 None,
1604 None,
1605 )
1606 .unwrap();
1607 let instances = vec![
1608 strategy_type
1609 .as_any()
1610 .call1((first_config,))
1611 .unwrap()
1612 .unbind(),
1613 strategy_type
1614 .as_any()
1615 .call1((second_config,))
1616 .unwrap()
1617 .unbind(),
1618 ];
1619
1620 engine.py_add_strategies(instances).unwrap();
1621
1622 let trader = engine.0.kernel().trader.borrow();
1623 let strategy_ids = trader.strategy_ids();
1624 assert!(strategy_ids.contains(&StrategyId::from("S-MULTI-001")));
1625 assert!(strategy_ids.contains(&StrategyId::from("S-MULTI-002")));
1626 });
1627 }
1628
1629 #[rstest]
1630 fn test_add_exec_algorithms_registers_multiple_python_instances() {
1631 use nautilus_common::python::actor::PyDataActor;
1632 use nautilus_model::identifiers::ExecAlgorithmId;
1633 use pyo3::{ffi::c_str, types::PyAnyMethods};
1634
1635 Python::initialize();
1636
1637 let mut engine =
1638 super::PyBacktestEngine(BacktestEngine::new(BacktestEngineConfig::default()).unwrap());
1639
1640 Python::attach(|py| {
1641 let algo_type = py.get_type::<PyDataActor>();
1642 let first_config = py
1643 .eval(
1644 c_str!("type('_Cfg', (), {'exec_algorithm_id': 'EXEC-MULTI-001'})()"),
1645 None,
1646 None,
1647 )
1648 .unwrap();
1649 let second_config = py
1650 .eval(
1651 c_str!("type('_Cfg', (), {'exec_algorithm_id': 'EXEC-MULTI-002'})()"),
1652 None,
1653 None,
1654 )
1655 .unwrap();
1656 let instances = vec![
1657 algo_type.as_any().call1((first_config,)).unwrap().unbind(),
1658 algo_type.as_any().call1((second_config,)).unwrap().unbind(),
1659 ];
1660
1661 engine.py_add_exec_algorithms(instances).unwrap();
1662
1663 let trader = engine.0.kernel().trader.borrow();
1664 let exec_algorithm_ids = trader.exec_algorithm_ids();
1665 assert!(exec_algorithm_ids.contains(&ExecAlgorithmId::from("EXEC-MULTI-001")));
1666 assert!(exec_algorithm_ids.contains(&ExecAlgorithmId::from("EXEC-MULTI-002")));
1667 });
1668 }
1669}
1670
1671pub(crate) fn pyobject_to_fill_model_any(
1672 _py: Python,
1673 obj: &Bound<'_, PyAny>,
1674) -> PyResult<FillModelAny> {
1675 if let Ok(m) = obj.extract::<DefaultFillModel>() {
1676 return Ok(FillModelAny::Default(m));
1677 }
1678
1679 if let Ok(m) = obj.extract::<BestPriceFillModel>() {
1680 return Ok(FillModelAny::BestPrice(m));
1681 }
1682
1683 if let Ok(m) = obj.extract::<OneTickSlippageFillModel>() {
1684 return Ok(FillModelAny::OneTickSlippage(m));
1685 }
1686
1687 if let Ok(m) = obj.extract::<ProbabilisticFillModel>() {
1688 return Ok(FillModelAny::Probabilistic(m));
1689 }
1690
1691 if let Ok(m) = obj.extract::<TwoTierFillModel>() {
1692 return Ok(FillModelAny::TwoTier(m));
1693 }
1694
1695 if let Ok(m) = obj.extract::<ThreeTierFillModel>() {
1696 return Ok(FillModelAny::ThreeTier(m));
1697 }
1698
1699 if let Ok(m) = obj.extract::<LimitOrderPartialFillModel>() {
1700 return Ok(FillModelAny::LimitOrderPartialFill(m));
1701 }
1702
1703 if let Ok(m) = obj.extract::<SizeAwareFillModel>() {
1704 return Ok(FillModelAny::SizeAware(m));
1705 }
1706
1707 if let Ok(m) = obj.extract::<CompetitionAwareFillModel>() {
1708 return Ok(FillModelAny::CompetitionAware(m));
1709 }
1710
1711 if let Ok(m) = obj.extract::<VolumeSensitiveFillModel>() {
1712 return Ok(FillModelAny::VolumeSensitive(m));
1713 }
1714
1715 if let Ok(m) = obj.extract::<MarketHoursFillModel>() {
1716 return Ok(FillModelAny::MarketHours(m));
1717 }
1718
1719 let type_name = obj.get_type().name()?;
1720 Err(to_pytype_err(format!(
1721 "Cannot convert {type_name} to FillModel"
1722 )))
1723}
1724
1725pub(crate) fn pyobject_to_simulation_module_any(
1726 _py: Python,
1727 obj: &Bound<'_, PyAny>,
1728) -> PyResult<SimulationModuleAny> {
1729 if let Ok(cell) = obj.cast::<FXRolloverInterestModule>() {
1730 let module = cell.borrow().clone();
1731 return Ok(SimulationModuleAny::FXRolloverInterest(module));
1732 }
1733
1734 let type_name = obj.get_type().name()?;
1735 Err(to_pytype_err(format!(
1736 "Cannot convert {type_name} to SimulationModule"
1737 )))
1738}
1739
1740pub(crate) fn pyobject_to_latency_model_any(
1741 _py: Python,
1742 obj: &Bound<'_, PyAny>,
1743) -> PyResult<LatencyModelAny> {
1744 if let Ok(m) = obj.extract::<StaticLatencyModel>() {
1745 return Ok(LatencyModelAny::Static(m));
1746 }
1747
1748 let type_name = obj.get_type().name()?;
1749 Err(to_pytype_err(format!(
1750 "Cannot convert {type_name} to LatencyModel"
1751 )))
1752}
1753
1754pub(crate) fn pyobject_to_margin_model_any(
1755 _py: Python,
1756 obj: &Bound<'_, PyAny>,
1757) -> PyResult<MarginModelAny> {
1758 if let Ok(m) = obj.extract::<StandardMarginModel>() {
1759 return Ok(MarginModelAny::Standard(m));
1760 }
1761
1762 if let Ok(m) = obj.extract::<LeveragedMarginModel>() {
1763 return Ok(MarginModelAny::Leveraged(m));
1764 }
1765
1766 let type_name = obj.get_type().name()?;
1767 Err(to_pytype_err(format!(
1768 "Cannot convert {type_name} to MarginModel"
1769 )))
1770}
1771
1772fn pyobject_to_data(_py: Python, obj: &Bound<'_, PyAny>) -> PyResult<Data> {
1773 if let Ok(delta) = obj.extract::<OrderBookDelta>() {
1774 return Ok(Data::Delta(delta));
1775 }
1776
1777 if let Ok(deltas) = obj.extract::<OrderBookDeltas>() {
1778 return Ok(Data::Deltas(OrderBookDeltas_API::new(deltas)));
1779 }
1780
1781 if let Ok(quote) = obj.extract::<QuoteTick>() {
1782 return Ok(Data::Quote(quote));
1783 }
1784
1785 if let Ok(trade) = obj.extract::<TradeTick>() {
1786 return Ok(Data::Trade(trade));
1787 }
1788
1789 if let Ok(bar) = obj.extract::<Bar>() {
1790 return Ok(Data::Bar(bar));
1791 }
1792
1793 if let Ok(depth) = obj.extract::<OrderBookDepth10>() {
1794 return Ok(Data::Depth10(Box::new(depth)));
1795 }
1796
1797 if let Ok(mark) = obj.extract::<MarkPriceUpdate>() {
1798 return Ok(Data::MarkPriceUpdate(mark));
1799 }
1800
1801 if let Ok(index) = obj.extract::<IndexPriceUpdate>() {
1802 return Ok(Data::IndexPriceUpdate(index));
1803 }
1804
1805 if let Ok(funding_rate) = obj.extract::<FundingRateUpdate>() {
1806 return Ok(Data::FundingRateUpdate(funding_rate));
1807 }
1808
1809 if let Ok(greeks) = obj.extract::<OptionGreeks>() {
1810 return Ok(Data::OptionGreeks(greeks));
1811 }
1812
1813 if let Ok(status) = obj.extract::<InstrumentStatus>() {
1814 return Ok(Data::InstrumentStatus(status));
1815 }
1816
1817 if let Ok(close) = obj.extract::<InstrumentClose>() {
1818 return Ok(Data::InstrumentClose(close));
1819 }
1820
1821 #[cfg(feature = "defi")]
1822 if let Ok(defi) = obj.extract::<DefiData>() {
1823 return Ok(Data::Defi(Box::new(defi)));
1824 }
1825
1826 if let Ok(delta) = OrderBookDelta::from_pyobject(obj) {
1828 return Ok(Data::Delta(delta));
1829 }
1830
1831 if let Ok(quote) = QuoteTick::from_pyobject(obj) {
1832 return Ok(Data::Quote(quote));
1833 }
1834
1835 if let Ok(trade) = TradeTick::from_pyobject(obj) {
1836 return Ok(Data::Trade(trade));
1837 }
1838
1839 if let Ok(bar) = Bar::from_pyobject(obj) {
1840 return Ok(Data::Bar(bar));
1841 }
1842
1843 if let Ok(mark) = MarkPriceUpdate::from_pyobject(obj) {
1844 return Ok(Data::MarkPriceUpdate(mark));
1845 }
1846
1847 if let Ok(index) = IndexPriceUpdate::from_pyobject(obj) {
1848 return Ok(Data::IndexPriceUpdate(index));
1849 }
1850
1851 if let Ok(funding_rate) = FundingRateUpdate::from_pyobject(obj) {
1852 return Ok(Data::FundingRateUpdate(funding_rate));
1853 }
1854
1855 if let Ok(greeks) = OptionGreeks::from_pyobject(obj) {
1856 return Ok(Data::OptionGreeks(greeks));
1857 }
1858
1859 if let Ok(status) = InstrumentStatus::from_pyobject(obj) {
1860 return Ok(Data::InstrumentStatus(status));
1861 }
1862
1863 if let Ok(close) = InstrumentClose::from_pyobject(obj) {
1864 return Ok(Data::InstrumentClose(close));
1865 }
1866
1867 let type_name = obj.get_type().name()?;
1868 Err(to_pytype_err(format!("Cannot convert {type_name} to Data")))
1869}