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    python::{actor::PyDataActor, cache::PyCache},
24};
25use nautilus_core::{
26    UUID4, UnixNanos,
27    python::{to_pyruntime_err, to_pytype_err, to_pyvalue_err},
28};
29use nautilus_execution::models::{
30    fee::{FeeModelAny, FixedFeeModel, MakerTakerFeeModel, PerContractFeeModel},
31    fill::{
32        BestPriceFillModel, CompetitionAwareFillModel, DefaultFillModel, FillModelAny,
33        LimitOrderPartialFillModel, MarketHoursFillModel, OneTickSlippageFillModel,
34        ProbabilisticFillModel, SizeAwareFillModel, ThreeTierFillModel, TwoTierFillModel,
35        VolumeSensitiveFillModel,
36    },
37    latency::{LatencyModelAny, StaticLatencyModel},
38};
39use nautilus_model::{
40    accounts::margin_model::{LeveragedMarginModel, MarginModelAny, StandardMarginModel},
41    data::{
42        Bar, Data, IndexPriceUpdate, InstrumentClose, InstrumentStatus, MarkPriceUpdate,
43        OrderBookDelta, OrderBookDeltas, OrderBookDeltas_API, OrderBookDepth10, QuoteTick,
44        TradeTick,
45    },
46    enums::{AccountType, BookType, OmsType, OtoTriggerMode},
47    identifiers::{ActorId, ClientId, ComponentId, InstrumentId, StrategyId, TraderId, Venue},
48    python::instruments::pyobject_to_instrument_any,
49    types::{Currency, Money, Price},
50};
51use nautilus_trading::{
52    ImportableStrategyConfig,
53    python::strategy::{PyStrategy, PyStrategyInner},
54};
55use pyo3::prelude::*;
56use rust_decimal::Decimal;
57
58use super::node::create_config_instance;
59use crate::{
60    config::{BacktestEngineConfig, SimulatedVenueConfig},
61    engine::BacktestEngine,
62    modules::{FXRolloverInterestModule, SimulationModuleAny},
63    result::BacktestResult,
64};
65
66/// PyO3 wrapper around [`BacktestEngine`].
67///
68/// Exposes the backtest engine to Python as `BacktestEngine`.
69/// Uses `unsendable` because the inner engine holds `Rc<RefCell<...>>`.
70#[pyo3::pyclass(
71    module = "nautilus_trader.core.nautilus_pyo3.backtest",
72    name = "BacktestEngine",
73    unsendable
74)]
75#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.backtest")]
76#[derive(Debug)]
77pub struct PyBacktestEngine(BacktestEngine);
78
79#[pyo3_stub_gen::derive::gen_stub_pymethods]
80#[pymethods]
81impl PyBacktestEngine {
82    #[new]
83    fn py_new(config: BacktestEngineConfig) -> PyResult<Self> {
84        let engine = BacktestEngine::new(config).map_err(to_pyruntime_err)?;
85        Ok(Self(engine))
86    }
87
88    /// Adds a simulated exchange with the given parameters to the engine.
89    #[pyo3(
90        name = "add_venue",
91        signature = (
92            venue,
93            oms_type,
94            account_type,
95            starting_balances,
96            base_currency = None,
97            default_leverage = None,
98            leverages = None,
99            margin_model = None,
100            fill_model = None,
101            fee_model = None,
102            latency_model = None,
103            modules = None,
104            book_type = BookType::L1_MBP,
105            routing = false,
106            reject_stop_orders = true,
107            support_gtd_orders = true,
108            support_contingent_orders = true,
109            use_position_ids = true,
110            use_random_ids = false,
111            use_reduce_only = true,
112            use_message_queue = true,
113            use_market_order_acks = false,
114            bar_execution = true,
115            bar_adaptive_high_low_ordering = false,
116            trade_execution = true,
117            liquidity_consumption = false,
118            queue_position = false,
119            allow_cash_borrowing = false,
120            frozen_account = false,
121            oto_trigger_mode = OtoTriggerMode::Partial,
122            price_protection_points = None,
123            settlement_prices = None,
124        )
125    )]
126    #[expect(clippy::too_many_arguments)]
127    fn py_add_venue(
128        &mut self,
129        venue: Venue,
130        oms_type: OmsType,
131        account_type: AccountType,
132        starting_balances: Vec<Money>,
133        base_currency: Option<Currency>,
134        default_leverage: Option<Decimal>,
135        leverages: Option<HashMap<InstrumentId, Decimal>>,
136        margin_model: Option<Py<PyAny>>,
137        fill_model: Option<Py<PyAny>>,
138        fee_model: Option<Py<PyAny>>,
139        latency_model: Option<Py<PyAny>>,
140        modules: Option<Vec<Py<PyAny>>>,
141        book_type: BookType,
142        routing: bool,
143        reject_stop_orders: bool,
144        support_gtd_orders: bool,
145        support_contingent_orders: bool,
146        use_position_ids: bool,
147        use_random_ids: bool,
148        use_reduce_only: bool,
149        use_message_queue: bool,
150        use_market_order_acks: bool,
151        bar_execution: bool,
152        bar_adaptive_high_low_ordering: bool,
153        trade_execution: bool,
154        liquidity_consumption: bool,
155        queue_position: bool,
156        allow_cash_borrowing: bool,
157        frozen_account: bool,
158        oto_trigger_mode: OtoTriggerMode,
159        price_protection_points: Option<u32>,
160        settlement_prices: Option<HashMap<InstrumentId, Price>>,
161    ) -> PyResult<()> {
162        let leverages: AHashMap<InstrumentId, Decimal> = leverages
163            .map(|m| m.into_iter().collect())
164            .unwrap_or_default();
165        let settlement_prices: AHashMap<InstrumentId, Price> = settlement_prices
166            .map(|m| m.into_iter().collect())
167            .unwrap_or_default();
168        let margin_model = margin_model
169            .map(|obj| Python::attach(|py| pyobject_to_margin_model_any(py, obj.bind(py))))
170            .transpose()?;
171        let fill_model = fill_model
172            .map(|obj| Python::attach(|py| pyobject_to_fill_model_any(py, obj.bind(py))))
173            .transpose()?
174            .unwrap_or_default();
175        let fee_model = fee_model
176            .map(|obj| Python::attach(|py| pyobject_to_fee_model_any(py, obj.bind(py))))
177            .transpose()?
178            .unwrap_or_default();
179        let latency_model = latency_model
180            .map(|obj| Python::attach(|py| pyobject_to_latency_model_any(py, obj.bind(py))))
181            .transpose()?
182            .map(Into::into);
183        let modules = modules
184            .map(|objs| {
185                objs.into_iter()
186                    .map(|obj| {
187                        Python::attach(|py| pyobject_to_simulation_module_any(py, obj.bind(py)))
188                    })
189                    .collect::<PyResult<Vec<_>>>()
190            })
191            .transpose()?
192            .unwrap_or_default()
193            .into_iter()
194            .map(Into::into)
195            .collect();
196
197        let sim_config = SimulatedVenueConfig::builder()
198            .venue(venue)
199            .oms_type(oms_type)
200            .account_type(account_type)
201            .book_type(book_type)
202            .starting_balances(starting_balances)
203            .maybe_base_currency(base_currency)
204            .maybe_default_leverage(default_leverage)
205            .leverages(leverages)
206            .maybe_margin_model(margin_model)
207            .modules(modules)
208            .fill_model(fill_model)
209            .fee_model(fee_model)
210            .maybe_latency_model(latency_model)
211            .routing(routing)
212            .reject_stop_orders(reject_stop_orders)
213            .support_gtd_orders(support_gtd_orders)
214            .support_contingent_orders(support_contingent_orders)
215            .use_position_ids(use_position_ids)
216            .use_random_ids(use_random_ids)
217            .use_reduce_only(use_reduce_only)
218            .use_message_queue(use_message_queue)
219            .use_market_order_acks(use_market_order_acks)
220            .bar_execution(bar_execution)
221            .bar_adaptive_high_low_ordering(bar_adaptive_high_low_ordering)
222            .trade_execution(trade_execution)
223            .liquidity_consumption(liquidity_consumption)
224            .allow_cash_borrowing(allow_cash_borrowing)
225            .frozen_account(frozen_account)
226            .queue_position(queue_position)
227            .oto_full_trigger(oto_trigger_mode == OtoTriggerMode::Full)
228            .maybe_price_protection_points(price_protection_points)
229            .build();
230
231        self.0.add_venue(sim_config).map_err(to_pyruntime_err)?;
232
233        for (instrument_id, price) in settlement_prices {
234            self.0
235                .set_settlement_price(venue, instrument_id, price)
236                .map_err(to_pyruntime_err)?;
237        }
238
239        Ok(())
240    }
241
242    /// Changes the fill model for a venue.
243    #[pyo3(name = "change_fill_model")]
244    #[expect(clippy::needless_pass_by_value)]
245    fn py_change_fill_model(
246        &mut self,
247        py: Python,
248        venue: Venue,
249        fill_model: Py<PyAny>,
250    ) -> PyResult<()> {
251        let fill_model = pyobject_to_fill_model_any(py, fill_model.bind(py))?;
252        self.0.change_fill_model(venue, fill_model);
253        Ok(())
254    }
255
256    /// Adds data to the engine.
257    #[pyo3(
258        name = "add_data",
259        signature = (data, client_id=None, validate=true, sort=true)
260    )]
261    fn py_add_data(
262        &mut self,
263        py: Python,
264        data: Vec<Py<PyAny>>,
265        client_id: Option<ClientId>,
266        validate: bool,
267        sort: bool,
268    ) -> PyResult<()> {
269        let rust_data: Vec<Data> = data
270            .into_iter()
271            .map(|obj| pyobject_to_data(py, obj.bind(py)))
272            .collect::<PyResult<_>>()?;
273        self.0
274            .add_data(rust_data, client_id, validate, sort)
275            .map_err(to_pyruntime_err)
276    }
277
278    /// Adds an instrument to the engine.
279    #[pyo3(name = "add_instrument")]
280    fn py_add_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
281        let instrument_any = pyobject_to_instrument_any(py, instrument)?;
282        self.0
283            .add_instrument(&instrument_any)
284            .map_err(to_pyruntime_err)
285    }
286
287    /// Adds an actor from an importable config.
288    #[allow(
289        unsafe_code,
290        reason = "Required for Python actor component registration"
291    )]
292    #[pyo3(name = "add_actor_from_config")]
293    #[expect(clippy::needless_pass_by_value)]
294    fn py_add_actor_from_config(
295        &mut self,
296        _py: Python,
297        config: ImportableActorConfig,
298    ) -> PyResult<()> {
299        log::debug!("`add_actor_from_config` with: {config:?}");
300
301        let parts: Vec<&str> = config.actor_path.split(':').collect();
302        if parts.len() != 2 {
303            return Err(to_pyvalue_err(
304                "actor_path must be in format 'module.path:ClassName'",
305            ));
306        }
307        let (module_name, class_name) = (parts[0], parts[1]);
308
309        log::info!("Importing actor from module: {module_name} class: {class_name}");
310
311        let (python_actor, actor_id) =
312            Python::attach(|py| -> anyhow::Result<(Py<PyAny>, ActorId)> {
313                let actor_module = py
314                    .import(module_name)
315                    .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
316                let actor_class = actor_module
317                    .getattr(class_name)
318                    .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
319
320                let config_instance =
321                    create_config_instance(py, &config.config_path, &config.config)?;
322
323                let python_actor = if let Some(config_obj) = config_instance.clone() {
324                    actor_class.call1((config_obj,))?
325                } else {
326                    actor_class.call0()?
327                };
328
329                let mut py_data_actor_ref = python_actor
330                    .extract::<PyRefMut<PyDataActor>>()
331                    .map_err(Into::<PyErr>::into)
332                    .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
333
334                if let Some(config_obj) = config_instance.as_ref() {
335                    if let Ok(actor_id) = config_obj.getattr("actor_id")
336                        && !actor_id.is_none()
337                    {
338                        let actor_id_val = if let Ok(actor_id_val) = actor_id.extract::<ActorId>() {
339                            actor_id_val
340                        } else if let Ok(actor_id_str) = actor_id.extract::<String>() {
341                            ActorId::new_checked(&actor_id_str)?
342                        } else {
343                            anyhow::bail!("Invalid `actor_id` type");
344                        };
345                        py_data_actor_ref.set_actor_id(actor_id_val);
346                    }
347
348                    if let Ok(log_events) = config_obj.getattr("log_events")
349                        && let Ok(log_events_val) = log_events.extract::<bool>()
350                    {
351                        py_data_actor_ref.set_log_events(log_events_val);
352                    }
353
354                    if let Ok(log_commands) = config_obj.getattr("log_commands")
355                        && let Ok(log_commands_val) = log_commands.extract::<bool>()
356                    {
357                        py_data_actor_ref.set_log_commands(log_commands_val);
358                    }
359                }
360
361                py_data_actor_ref.set_python_instance(python_actor.clone().unbind());
362                let actor_id = py_data_actor_ref.actor_id();
363
364                Ok((python_actor.unbind(), actor_id))
365            })
366            .map_err(to_pyruntime_err)?;
367
368        if self
369            .0
370            .kernel()
371            .trader
372            .borrow()
373            .actor_ids()
374            .contains(&actor_id)
375        {
376            return Err(to_pyruntime_err(format!(
377                "Actor '{actor_id}' is already registered"
378            )));
379        }
380
381        let trader_id = self.0.kernel().config.trader_id();
382        let cache = self.0.kernel().cache.clone();
383        let component_id = ComponentId::new(actor_id.inner().as_str());
384        let clock = self
385            .0
386            .kernel_mut()
387            .trader
388            .borrow_mut()
389            .create_component_clock(component_id);
390
391        Python::attach(|py| -> anyhow::Result<()> {
392            let py_actor = python_actor.bind(py);
393            let mut py_data_actor_ref = py_actor
394                .extract::<PyRefMut<PyDataActor>>()
395                .map_err(Into::<PyErr>::into)
396                .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
397
398            py_data_actor_ref
399                .register(trader_id, clock, cache)
400                .map_err(|e| anyhow::anyhow!("Failed to register PyDataActor: {e}"))?;
401
402            Ok(())
403        })
404        .map_err(to_pyruntime_err)?;
405
406        Python::attach(|py| -> anyhow::Result<()> {
407            let py_actor = python_actor.bind(py);
408            let py_data_actor_ref = py_actor
409                .cast::<PyDataActor>()
410                .map_err(|e| anyhow::anyhow!("Failed to downcast to PyDataActor: {e}"))?;
411            py_data_actor_ref.borrow().register_in_global_registries();
412            Ok(())
413        })
414        .map_err(to_pyruntime_err)?;
415
416        self.0
417            .kernel_mut()
418            .trader
419            .borrow_mut()
420            .add_actor_id_for_lifecycle(actor_id)
421            .map_err(to_pyruntime_err)?;
422
423        log::info!("Registered Python actor {actor_id}");
424        Ok(())
425    }
426
427    /// Adds a strategy from an importable config.
428    #[allow(
429        unsafe_code,
430        reason = "Required for Python strategy component registration"
431    )]
432    #[pyo3(name = "add_strategy_from_config")]
433    #[expect(clippy::needless_pass_by_value)]
434    fn py_add_strategy_from_config(
435        &mut self,
436        _py: Python,
437        config: ImportableStrategyConfig,
438    ) -> PyResult<()> {
439        log::debug!("`add_strategy_from_config` with: {config:?}");
440
441        let parts: Vec<&str> = config.strategy_path.split(':').collect();
442        if parts.len() != 2 {
443            return Err(to_pyvalue_err(
444                "strategy_path must be in format 'module.path:ClassName'",
445            ));
446        }
447        let (module_name, class_name) = (parts[0], parts[1]);
448
449        log::info!("Importing strategy from module: {module_name} class: {class_name}");
450
451        let (python_strategy, strategy_id) =
452            Python::attach(|py| -> anyhow::Result<(Py<PyAny>, StrategyId)> {
453                let strategy_module = py
454                    .import(module_name)
455                    .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
456                let strategy_class = strategy_module
457                    .getattr(class_name)
458                    .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
459
460                let config_instance =
461                    create_config_instance(py, &config.config_path, &config.config)?;
462
463                let python_strategy = if let Some(config_obj) = config_instance.clone() {
464                    strategy_class.call1((config_obj,))?
465                } else {
466                    strategy_class.call0()?
467                };
468
469                let mut py_strategy_ref = python_strategy
470                    .extract::<PyRefMut<PyStrategy>>()
471                    .map_err(Into::<PyErr>::into)
472                    .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
473
474                if let Some(config_obj) = config_instance.as_ref() {
475                    if let Ok(strategy_id) = config_obj.getattr("strategy_id")
476                        && !strategy_id.is_none()
477                    {
478                        let strategy_id_val = if let Ok(sid) = strategy_id.extract::<StrategyId>() {
479                            sid
480                        } else if let Ok(sid_str) = strategy_id.extract::<String>() {
481                            StrategyId::new_checked(&sid_str)?
482                        } else {
483                            anyhow::bail!("Invalid `strategy_id` type");
484                        };
485                        py_strategy_ref.set_strategy_id(strategy_id_val);
486                    }
487
488                    if let Ok(log_events) = config_obj.getattr("log_events")
489                        && let Ok(log_events_val) = log_events.extract::<bool>()
490                    {
491                        py_strategy_ref.set_log_events(log_events_val);
492                    }
493
494                    if let Ok(log_commands) = config_obj.getattr("log_commands")
495                        && let Ok(log_commands_val) = log_commands.extract::<bool>()
496                    {
497                        py_strategy_ref.set_log_commands(log_commands_val);
498                    }
499                }
500
501                py_strategy_ref.set_python_instance(python_strategy.clone().unbind());
502                let strategy_id = py_strategy_ref.strategy_id();
503
504                Ok((python_strategy.unbind(), strategy_id))
505            })
506            .map_err(to_pyruntime_err)?;
507
508        if self
509            .0
510            .kernel()
511            .trader
512            .borrow()
513            .strategy_ids()
514            .contains(&strategy_id)
515        {
516            return Err(to_pyruntime_err(format!(
517                "Strategy '{strategy_id}' is already registered"
518            )));
519        }
520
521        let trader_id = self.0.kernel().config.trader_id();
522        let cache = self.0.kernel().cache.clone();
523        let portfolio = self.0.kernel().portfolio.clone();
524        let component_id = ComponentId::new(strategy_id.inner().as_str());
525        let clock = self
526            .0
527            .kernel_mut()
528            .trader
529            .borrow_mut()
530            .create_component_clock(component_id);
531
532        Python::attach(|py| -> anyhow::Result<()> {
533            let py_strategy = python_strategy.bind(py);
534            let mut py_strategy_ref = py_strategy
535                .extract::<PyRefMut<PyStrategy>>()
536                .map_err(Into::<PyErr>::into)
537                .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
538
539            py_strategy_ref
540                .register(trader_id, clock, cache, portfolio)
541                .map_err(|e| anyhow::anyhow!("Failed to register PyStrategy: {e}"))?;
542
543            Ok(())
544        })
545        .map_err(to_pyruntime_err)?;
546
547        Python::attach(|py| -> anyhow::Result<()> {
548            let py_strategy = python_strategy.bind(py);
549            let py_strategy_ref = py_strategy
550                .cast::<PyStrategy>()
551                .map_err(|e| anyhow::anyhow!("Failed to downcast to PyStrategy: {e}"))?;
552            py_strategy_ref.borrow().register_in_global_registries();
553            Ok(())
554        })
555        .map_err(to_pyruntime_err)?;
556
557        self.0
558            .kernel_mut()
559            .trader
560            .borrow_mut()
561            .add_strategy_id_with_subscriptions::<PyStrategyInner>(strategy_id)
562            .map_err(to_pyruntime_err)?;
563
564        log::info!("Registered Python strategy {strategy_id}");
565        Ok(())
566    }
567
568    /// Adds a native Rust strategy from its config.
569    ///
570    /// The config type determines which built-in strategy is constructed.
571    /// All execution happens in Rust; Python is the configuration layer.
572    #[cfg(feature = "examples")]
573    #[pyo3(name = "add_native_strategy")]
574    fn py_add_native_strategy(&mut self, config: &Bound<'_, PyAny>) -> PyResult<()> {
575        use nautilus_trading::examples::strategies::{
576            DeltaNeutralVol, DeltaNeutralVolConfig, EmaCross, EmaCrossConfig, GridMarketMaker,
577            GridMarketMakerConfig, HurstVpinDirectional, HurstVpinDirectionalConfig,
578        };
579
580        if let Ok(config) = config.extract::<EmaCrossConfig>() {
581            self.0
582                .add_strategy(EmaCross::from_config(config))
583                .map_err(to_pyruntime_err)
584        } else if let Ok(config) = config.extract::<GridMarketMakerConfig>() {
585            self.0
586                .add_strategy(GridMarketMaker::new(config))
587                .map_err(to_pyruntime_err)
588        } else if let Ok(config) = config.extract::<DeltaNeutralVolConfig>() {
589            self.0
590                .add_strategy(DeltaNeutralVol::new(config))
591                .map_err(to_pyruntime_err)
592        } else if let Ok(config) = config.extract::<HurstVpinDirectionalConfig>() {
593            self.0
594                .add_strategy(HurstVpinDirectional::new(config))
595                .map_err(to_pyruntime_err)
596        } else {
597            let type_name = config.get_type().name()?;
598            Err(to_pytype_err(format!(
599                "Unsupported native strategy config type: {type_name}",
600            )))
601        }
602    }
603
604    /// Adds a native Rust actor from its config.
605    ///
606    /// The config type determines which built-in actor is constructed.
607    /// All execution happens in Rust; Python is the configuration layer.
608    #[cfg(feature = "examples")]
609    #[pyo3(name = "add_native_actor")]
610    fn py_add_native_actor(&mut self, config: &Bound<'_, PyAny>) -> PyResult<()> {
611        use nautilus_trading::examples::actors::{BookImbalanceActor, BookImbalanceActorConfig};
612
613        if let Ok(config) = config.extract::<BookImbalanceActorConfig>() {
614            self.0
615                .add_actor(BookImbalanceActor::from_config(config))
616                .map_err(to_pyruntime_err)
617        } else {
618            let type_name = config.get_type().name()?;
619            Err(to_pytype_err(format!(
620                "Unsupported native actor config type: {type_name}",
621            )))
622        }
623    }
624
625    /// Runs the backtest engine.
626    #[pyo3(
627        name = "run",
628        signature = (start=None, end=None, run_config_id=None, streaming=false)
629    )]
630    fn py_run(
631        &mut self,
632        start: Option<u64>,
633        end: Option<u64>,
634        run_config_id: Option<String>,
635        streaming: bool,
636    ) -> PyResult<()> {
637        self.0
638            .run(
639                start.map(UnixNanos::from),
640                end.map(UnixNanos::from),
641                run_config_id,
642                streaming,
643            )
644            .map_err(to_pyruntime_err)
645    }
646
647    /// Ends the backtest run, finalizing results.
648    #[pyo3(name = "end")]
649    fn py_end(&mut self) {
650        self.0.end();
651    }
652
653    /// Resets the engine state for a new run.
654    #[pyo3(name = "reset")]
655    fn py_reset(&mut self) {
656        self.0.reset();
657    }
658
659    /// Disposes of the engine, releasing all resources.
660    #[pyo3(name = "dispose")]
661    fn py_dispose(&mut self) {
662        self.0.dispose();
663    }
664
665    /// Returns the backtest result from the last run.
666    #[pyo3(name = "get_result")]
667    fn py_get_result(&self) -> BacktestResult {
668        self.0.get_result()
669    }
670
671    /// Clears all data from the engine.
672    #[pyo3(name = "clear_data")]
673    fn py_clear_data(&mut self) {
674        self.0.clear_data();
675    }
676
677    /// Clears all actors from the engine.
678    #[pyo3(name = "clear_actors")]
679    fn py_clear_actors(&mut self) -> PyResult<()> {
680        self.0.clear_actors().map_err(to_pyruntime_err)
681    }
682
683    /// Clears all strategies from the engine.
684    #[pyo3(name = "clear_strategies")]
685    fn py_clear_strategies(&mut self) -> PyResult<()> {
686        self.0.clear_strategies().map_err(to_pyruntime_err)
687    }
688
689    /// Clears all execution algorithms from the engine.
690    #[pyo3(name = "clear_exec_algorithms")]
691    fn py_clear_exec_algorithms(&mut self) -> PyResult<()> {
692        self.0.clear_exec_algorithms().map_err(to_pyruntime_err)
693    }
694
695    /// Adds multiple actors from importable configs. Stops at the first error.
696    #[pyo3(name = "add_actors_from_configs")]
697    fn py_add_actors_from_configs(
698        &mut self,
699        py: Python,
700        configs: Vec<ImportableActorConfig>,
701    ) -> PyResult<()> {
702        for config in configs {
703            self.py_add_actor_from_config(py, config)?;
704        }
705        Ok(())
706    }
707
708    /// Adds multiple strategies from importable configs. Stops at the first error.
709    #[pyo3(name = "add_strategies_from_configs")]
710    fn py_add_strategies_from_configs(
711        &mut self,
712        py: Python,
713        configs: Vec<ImportableStrategyConfig>,
714    ) -> PyResult<()> {
715        for config in configs {
716            self.py_add_strategy_from_config(py, config)?;
717        }
718        Ok(())
719    }
720
721    /// Sorts the engine's internal data stream by timestamp.
722    #[pyo3(name = "sort_data")]
723    fn py_sort_data(&mut self) {
724        self.0.sort_data();
725    }
726
727    /// Returns the trader ID for this engine.
728    #[getter]
729    #[pyo3(name = "trader_id")]
730    fn py_trader_id(&self) -> TraderId {
731        self.0.trader_id()
732    }
733
734    /// Returns the machine ID for this engine.
735    #[getter]
736    #[pyo3(name = "machine_id")]
737    fn py_machine_id(&self) -> String {
738        self.0.machine_id().to_string()
739    }
740
741    /// Returns the unique instance ID for this engine.
742    #[getter]
743    #[pyo3(name = "instance_id")]
744    fn py_instance_id(&self) -> UUID4 {
745        self.0.instance_id()
746    }
747
748    /// Returns the current iteration count.
749    #[getter]
750    #[pyo3(name = "iteration")]
751    fn py_iteration(&self) -> usize {
752        self.0.iteration()
753    }
754
755    /// Returns the last run config ID, if any.
756    #[getter]
757    #[pyo3(name = "run_config_id")]
758    fn py_run_config_id(&self) -> Option<String> {
759        self.0.run_config_id().map(str::to_string)
760    }
761
762    /// Returns the last run ID, if any.
763    #[getter]
764    #[pyo3(name = "run_id")]
765    fn py_run_id(&self) -> Option<UUID4> {
766        self.0.run_id()
767    }
768
769    /// Returns when the last run started, in nanoseconds since the UNIX epoch.
770    #[getter]
771    #[pyo3(name = "run_started")]
772    fn py_run_started(&self) -> Option<u64> {
773        self.0.run_started().map(|n| n.as_u64())
774    }
775
776    /// Returns when the last run finished, in nanoseconds since the UNIX epoch.
777    #[getter]
778    #[pyo3(name = "run_finished")]
779    fn py_run_finished(&self) -> Option<u64> {
780        self.0.run_finished().map(|n| n.as_u64())
781    }
782
783    /// Returns the last backtest range start, in nanoseconds since the UNIX epoch.
784    #[getter]
785    #[pyo3(name = "backtest_start")]
786    fn py_backtest_start(&self) -> Option<u64> {
787        self.0.backtest_start().map(|n| n.as_u64())
788    }
789
790    /// Returns the last backtest range end, in nanoseconds since the UNIX epoch.
791    #[getter]
792    #[pyo3(name = "backtest_end")]
793    fn py_backtest_end(&self) -> Option<u64> {
794        self.0.backtest_end().map(|n| n.as_u64())
795    }
796
797    /// Returns the list of registered venue identifiers.
798    #[pyo3(name = "list_venues")]
799    fn py_list_venues(&self) -> Vec<Venue> {
800        self.0.list_venues()
801    }
802
803    /// Returns the cache shared with the kernel and registered components.
804    #[getter]
805    #[pyo3(name = "cache")]
806    fn py_cache(&self) -> PyCache {
807        PyCache::from_rc(self.0.kernel().cache.clone())
808    }
809
810    fn __repr__(&self) -> String {
811        format!("{:?}", self.0)
812    }
813}
814
815impl PyBacktestEngine {
816    /// Provides access to the inner [`BacktestEngine`].
817    #[must_use]
818    pub fn inner(&self) -> &BacktestEngine {
819        &self.0
820    }
821
822    /// Provides mutable access to the inner [`BacktestEngine`].
823    pub fn inner_mut(&mut self) -> &mut BacktestEngine {
824        &mut self.0
825    }
826}
827
828pub(crate) fn pyobject_to_fill_model_any(
829    _py: Python,
830    obj: &Bound<'_, PyAny>,
831) -> PyResult<FillModelAny> {
832    if let Ok(m) = obj.extract::<DefaultFillModel>() {
833        return Ok(FillModelAny::Default(m));
834    }
835
836    if let Ok(m) = obj.extract::<BestPriceFillModel>() {
837        return Ok(FillModelAny::BestPrice(m));
838    }
839
840    if let Ok(m) = obj.extract::<OneTickSlippageFillModel>() {
841        return Ok(FillModelAny::OneTickSlippage(m));
842    }
843
844    if let Ok(m) = obj.extract::<ProbabilisticFillModel>() {
845        return Ok(FillModelAny::Probabilistic(m));
846    }
847
848    if let Ok(m) = obj.extract::<TwoTierFillModel>() {
849        return Ok(FillModelAny::TwoTier(m));
850    }
851
852    if let Ok(m) = obj.extract::<ThreeTierFillModel>() {
853        return Ok(FillModelAny::ThreeTier(m));
854    }
855
856    if let Ok(m) = obj.extract::<LimitOrderPartialFillModel>() {
857        return Ok(FillModelAny::LimitOrderPartialFill(m));
858    }
859
860    if let Ok(m) = obj.extract::<SizeAwareFillModel>() {
861        return Ok(FillModelAny::SizeAware(m));
862    }
863
864    if let Ok(m) = obj.extract::<CompetitionAwareFillModel>() {
865        return Ok(FillModelAny::CompetitionAware(m));
866    }
867
868    if let Ok(m) = obj.extract::<VolumeSensitiveFillModel>() {
869        return Ok(FillModelAny::VolumeSensitive(m));
870    }
871
872    if let Ok(m) = obj.extract::<MarketHoursFillModel>() {
873        return Ok(FillModelAny::MarketHours(m));
874    }
875
876    let type_name = obj.get_type().name()?;
877    Err(to_pytype_err(format!(
878        "Cannot convert {type_name} to FillModel"
879    )))
880}
881
882pub(crate) fn pyobject_to_fee_model_any(
883    _py: Python,
884    obj: &Bound<'_, PyAny>,
885) -> PyResult<FeeModelAny> {
886    if let Ok(m) = obj.extract::<FixedFeeModel>() {
887        return Ok(FeeModelAny::Fixed(m));
888    }
889
890    if let Ok(m) = obj.extract::<MakerTakerFeeModel>() {
891        return Ok(FeeModelAny::MakerTaker(m));
892    }
893
894    if let Ok(m) = obj.extract::<PerContractFeeModel>() {
895        return Ok(FeeModelAny::PerContract(m));
896    }
897
898    let type_name = obj.get_type().name()?;
899    Err(to_pytype_err(format!(
900        "Cannot convert {type_name} to FeeModel"
901    )))
902}
903
904pub(crate) fn pyobject_to_simulation_module_any(
905    _py: Python,
906    obj: &Bound<'_, PyAny>,
907) -> PyResult<SimulationModuleAny> {
908    if let Ok(cell) = obj.cast::<FXRolloverInterestModule>() {
909        let module = cell.borrow().clone();
910        return Ok(SimulationModuleAny::FXRolloverInterest(module));
911    }
912
913    let type_name = obj.get_type().name()?;
914    Err(to_pytype_err(format!(
915        "Cannot convert {type_name} to SimulationModule"
916    )))
917}
918
919pub(crate) fn pyobject_to_latency_model_any(
920    _py: Python,
921    obj: &Bound<'_, PyAny>,
922) -> PyResult<LatencyModelAny> {
923    if let Ok(m) = obj.extract::<StaticLatencyModel>() {
924        return Ok(LatencyModelAny::Static(m));
925    }
926
927    let type_name = obj.get_type().name()?;
928    Err(to_pytype_err(format!(
929        "Cannot convert {type_name} to LatencyModel"
930    )))
931}
932
933pub(crate) fn pyobject_to_margin_model_any(
934    _py: Python,
935    obj: &Bound<'_, PyAny>,
936) -> PyResult<MarginModelAny> {
937    if let Ok(m) = obj.extract::<StandardMarginModel>() {
938        return Ok(MarginModelAny::Standard(m));
939    }
940
941    if let Ok(m) = obj.extract::<LeveragedMarginModel>() {
942        return Ok(MarginModelAny::Leveraged(m));
943    }
944
945    let type_name = obj.get_type().name()?;
946    Err(to_pytype_err(format!(
947        "Cannot convert {type_name} to MarginModel"
948    )))
949}
950
951fn pyobject_to_data(_py: Python, obj: &Bound<'_, PyAny>) -> PyResult<Data> {
952    if let Ok(delta) = obj.extract::<OrderBookDelta>() {
953        return Ok(Data::Delta(delta));
954    }
955
956    if let Ok(deltas) = obj.extract::<OrderBookDeltas>() {
957        return Ok(Data::Deltas(OrderBookDeltas_API::new(deltas)));
958    }
959
960    if let Ok(quote) = obj.extract::<QuoteTick>() {
961        return Ok(Data::Quote(quote));
962    }
963
964    if let Ok(trade) = obj.extract::<TradeTick>() {
965        return Ok(Data::Trade(trade));
966    }
967
968    if let Ok(bar) = obj.extract::<Bar>() {
969        return Ok(Data::Bar(bar));
970    }
971
972    if let Ok(depth) = obj.extract::<OrderBookDepth10>() {
973        return Ok(Data::Depth10(Box::new(depth)));
974    }
975
976    if let Ok(mark) = obj.extract::<MarkPriceUpdate>() {
977        return Ok(Data::MarkPriceUpdate(mark));
978    }
979
980    if let Ok(index) = obj.extract::<IndexPriceUpdate>() {
981        return Ok(Data::IndexPriceUpdate(index));
982    }
983
984    if let Ok(status) = obj.extract::<InstrumentStatus>() {
985        return Ok(Data::InstrumentStatus(status));
986    }
987
988    if let Ok(close) = obj.extract::<InstrumentClose>() {
989        return Ok(Data::InstrumentClose(close));
990    }
991
992    // Fall back to from_pyobject methods for Cython objects
993    if let Ok(delta) = OrderBookDelta::from_pyobject(obj) {
994        return Ok(Data::Delta(delta));
995    }
996
997    if let Ok(quote) = QuoteTick::from_pyobject(obj) {
998        return Ok(Data::Quote(quote));
999    }
1000
1001    if let Ok(trade) = TradeTick::from_pyobject(obj) {
1002        return Ok(Data::Trade(trade));
1003    }
1004
1005    if let Ok(bar) = Bar::from_pyobject(obj) {
1006        return Ok(Data::Bar(bar));
1007    }
1008
1009    if let Ok(mark) = MarkPriceUpdate::from_pyobject(obj) {
1010        return Ok(Data::MarkPriceUpdate(mark));
1011    }
1012
1013    if let Ok(index) = IndexPriceUpdate::from_pyobject(obj) {
1014        return Ok(Data::IndexPriceUpdate(index));
1015    }
1016
1017    if let Ok(status) = InstrumentStatus::from_pyobject(obj) {
1018        return Ok(Data::InstrumentStatus(status));
1019    }
1020
1021    if let Ok(close) = InstrumentClose::from_pyobject(obj) {
1022        return Ok(Data::InstrumentClose(close));
1023    }
1024
1025    let type_name = obj.get_type().name()?;
1026    Err(to_pytype_err(format!("Cannot convert {type_name} to Data")))
1027}