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