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