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