Skip to main content

nautilus_backtest/python/
engine.rs

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