Skip to main content

nautilus_backtest/
config.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//! Configuration types for the backtest engine, venues, data, and run parameters.
17
18use std::{collections::HashMap, fmt::Display, str::FromStr, time::Duration};
19
20use ahash::AHashMap;
21use nautilus_common::{
22    cache::CacheConfig, enums::Environment, logging::logger::LoggerConfig,
23    msgbus::database::MessageBusConfig,
24};
25use nautilus_core::{UUID4, UnixNanos};
26use nautilus_data::engine::config::DataEngineConfig;
27use nautilus_execution::engine::config::ExecutionEngineConfig;
28use nautilus_model::{
29    data::{BarSpecification, BarType},
30    enums::{AccountType, BookType, OmsType},
31    identifiers::{ClientId, InstrumentId, TraderId},
32    types::Currency,
33};
34use nautilus_portfolio::config::PortfolioConfig;
35use nautilus_risk::engine::config::RiskEngineConfig;
36use nautilus_system::config::{NautilusKernelConfig, StreamingConfig};
37use ustr::Ustr;
38
39/// Represents a type of market data for catalog queries.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum NautilusDataType {
42    QuoteTick,
43    TradeTick,
44    Bar,
45    OrderBookDelta,
46    OrderBookDepth10,
47    MarkPriceUpdate,
48    IndexPriceUpdate,
49    InstrumentClose,
50}
51
52impl Display for NautilusDataType {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        std::fmt::Debug::fmt(self, f)
55    }
56}
57
58impl FromStr for NautilusDataType {
59    type Err = anyhow::Error;
60
61    fn from_str(s: &str) -> anyhow::Result<Self> {
62        match s {
63            stringify!(QuoteTick) => Ok(Self::QuoteTick),
64            stringify!(TradeTick) => Ok(Self::TradeTick),
65            stringify!(Bar) => Ok(Self::Bar),
66            stringify!(OrderBookDelta) => Ok(Self::OrderBookDelta),
67            stringify!(OrderBookDepth10) => Ok(Self::OrderBookDepth10),
68            stringify!(MarkPriceUpdate) => Ok(Self::MarkPriceUpdate),
69            stringify!(IndexPriceUpdate) => Ok(Self::IndexPriceUpdate),
70            stringify!(InstrumentClose) => Ok(Self::InstrumentClose),
71            _ => anyhow::bail!("Invalid `NautilusDataType`: '{s}'"),
72        }
73    }
74}
75
76/// Configuration for ``BacktestEngine`` instances.
77#[derive(Debug, Clone)]
78#[cfg_attr(
79    feature = "python",
80    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.backtest", from_py_object)
81)]
82pub struct BacktestEngineConfig {
83    /// The kernel environment context.
84    pub environment: Environment,
85    /// The trader ID for the node.
86    pub trader_id: TraderId,
87    /// If trading strategy state should be loaded from the database on start.
88    pub load_state: bool,
89    /// If trading strategy state should be saved to the database on stop.
90    pub save_state: bool,
91    /// The logging configuration for the kernel.
92    pub logging: LoggerConfig,
93    /// The unique instance identifier for the kernel.
94    pub instance_id: Option<UUID4>,
95    /// The timeout for all clients to connect and initialize.
96    pub timeout_connection: Duration,
97    /// The timeout for execution state to reconcile.
98    pub timeout_reconciliation: Duration,
99    /// The timeout for portfolio to initialize margins and unrealized pnls.
100    pub timeout_portfolio: Duration,
101    /// The timeout for all engine clients to disconnect.
102    pub timeout_disconnection: Duration,
103    /// The delay after stopping the node to await residual events before final shutdown.
104    pub delay_post_stop: Duration,
105    /// The timeout to await pending tasks cancellation during shutdown.
106    pub timeout_shutdown: Duration,
107    /// The cache configuration.
108    pub cache: Option<CacheConfig>,
109    /// The message bus configuration.
110    pub msgbus: Option<MessageBusConfig>,
111    /// The data engine configuration.
112    pub data_engine: Option<DataEngineConfig>,
113    /// The risk engine configuration.
114    pub risk_engine: Option<RiskEngineConfig>,
115    /// The execution engine configuration.
116    pub exec_engine: Option<ExecutionEngineConfig>,
117    /// The portfolio configuration.
118    pub portfolio: Option<PortfolioConfig>,
119    /// The configuration for streaming to feather files.
120    pub streaming: Option<StreamingConfig>,
121    /// If logging should be bypassed.
122    pub bypass_logging: bool,
123    /// If post backtest performance analysis should be run.
124    pub run_analysis: bool,
125}
126
127impl BacktestEngineConfig {
128    #[must_use]
129    #[allow(clippy::too_many_arguments)]
130    pub fn new(
131        environment: Environment,
132        trader_id: TraderId,
133        load_state: Option<bool>,
134        save_state: Option<bool>,
135        bypass_logging: Option<bool>,
136        run_analysis: Option<bool>,
137        timeout_connection: Option<u64>,
138        timeout_reconciliation: Option<u64>,
139        timeout_portfolio: Option<u64>,
140        timeout_disconnection: Option<u64>,
141        delay_post_stop: Option<u64>,
142        timeout_shutdown: Option<u64>,
143        logging: Option<LoggerConfig>,
144        instance_id: Option<UUID4>,
145        cache: Option<CacheConfig>,
146        msgbus: Option<MessageBusConfig>,
147        data_engine: Option<DataEngineConfig>,
148        risk_engine: Option<RiskEngineConfig>,
149        exec_engine: Option<ExecutionEngineConfig>,
150        portfolio: Option<PortfolioConfig>,
151        streaming: Option<StreamingConfig>,
152    ) -> Self {
153        Self {
154            environment,
155            trader_id,
156            load_state: load_state.unwrap_or(false),
157            save_state: save_state.unwrap_or(false),
158            logging: logging.unwrap_or_default(),
159            instance_id,
160            timeout_connection: Duration::from_secs(timeout_connection.unwrap_or(60)),
161            timeout_reconciliation: Duration::from_secs(timeout_reconciliation.unwrap_or(30)),
162            timeout_portfolio: Duration::from_secs(timeout_portfolio.unwrap_or(10)),
163            timeout_disconnection: Duration::from_secs(timeout_disconnection.unwrap_or(10)),
164            delay_post_stop: Duration::from_secs(delay_post_stop.unwrap_or(10)),
165            timeout_shutdown: Duration::from_secs(timeout_shutdown.unwrap_or(5)),
166            cache,
167            msgbus,
168            data_engine,
169            risk_engine,
170            exec_engine,
171            portfolio,
172            streaming,
173            bypass_logging: bypass_logging.unwrap_or(false),
174            run_analysis: run_analysis.unwrap_or(true),
175        }
176    }
177}
178
179impl NautilusKernelConfig for BacktestEngineConfig {
180    fn environment(&self) -> Environment {
181        self.environment
182    }
183
184    fn trader_id(&self) -> TraderId {
185        self.trader_id
186    }
187
188    fn load_state(&self) -> bool {
189        self.load_state
190    }
191
192    fn save_state(&self) -> bool {
193        self.save_state
194    }
195
196    fn logging(&self) -> LoggerConfig {
197        self.logging.clone()
198    }
199
200    fn instance_id(&self) -> Option<UUID4> {
201        self.instance_id
202    }
203
204    fn timeout_connection(&self) -> Duration {
205        self.timeout_connection
206    }
207
208    fn timeout_reconciliation(&self) -> Duration {
209        self.timeout_reconciliation
210    }
211
212    fn timeout_portfolio(&self) -> Duration {
213        self.timeout_portfolio
214    }
215
216    fn timeout_disconnection(&self) -> Duration {
217        self.timeout_disconnection
218    }
219
220    fn delay_post_stop(&self) -> Duration {
221        self.delay_post_stop
222    }
223
224    fn timeout_shutdown(&self) -> Duration {
225        self.timeout_shutdown
226    }
227
228    fn cache(&self) -> Option<CacheConfig> {
229        self.cache.clone()
230    }
231
232    fn msgbus(&self) -> Option<MessageBusConfig> {
233        self.msgbus.clone()
234    }
235
236    fn data_engine(&self) -> Option<DataEngineConfig> {
237        self.data_engine.clone()
238    }
239
240    fn risk_engine(&self) -> Option<RiskEngineConfig> {
241        self.risk_engine.clone()
242    }
243
244    fn exec_engine(&self) -> Option<ExecutionEngineConfig> {
245        self.exec_engine.clone()
246    }
247
248    fn portfolio(&self) -> Option<PortfolioConfig> {
249        self.portfolio.clone()
250    }
251
252    fn streaming(&self) -> Option<StreamingConfig> {
253        self.streaming.clone()
254    }
255}
256
257impl Default for BacktestEngineConfig {
258    fn default() -> Self {
259        Self {
260            environment: Environment::Backtest,
261            trader_id: TraderId::default(),
262            load_state: false,
263            save_state: false,
264            logging: LoggerConfig::default(),
265            instance_id: None,
266            timeout_connection: Duration::from_secs(60),
267            timeout_reconciliation: Duration::from_secs(30),
268            timeout_portfolio: Duration::from_secs(10),
269            timeout_disconnection: Duration::from_secs(10),
270            delay_post_stop: Duration::from_secs(10),
271            timeout_shutdown: Duration::from_secs(5),
272            cache: None,
273            msgbus: None,
274            data_engine: None,
275            risk_engine: None,
276            exec_engine: None,
277            portfolio: None,
278            streaming: None,
279            bypass_logging: false,
280            run_analysis: true,
281        }
282    }
283}
284
285#[cfg(feature = "python")]
286#[pyo3::pymethods]
287impl BacktestEngineConfig {
288    #[new]
289    #[pyo3(signature = (
290        trader_id = None,
291        load_state = None,
292        save_state = None,
293        bypass_logging = None,
294        run_analysis = None,
295        timeout_connection = None,
296        timeout_reconciliation = None,
297        timeout_portfolio = None,
298        timeout_disconnection = None,
299        delay_post_stop = None,
300        timeout_shutdown = None,
301        logging = None,
302        instance_id = None,
303    ))]
304    #[allow(clippy::too_many_arguments)]
305    fn py_new(
306        trader_id: Option<TraderId>,
307        load_state: Option<bool>,
308        save_state: Option<bool>,
309        bypass_logging: Option<bool>,
310        run_analysis: Option<bool>,
311        timeout_connection: Option<u64>,
312        timeout_reconciliation: Option<u64>,
313        timeout_portfolio: Option<u64>,
314        timeout_disconnection: Option<u64>,
315        delay_post_stop: Option<u64>,
316        timeout_shutdown: Option<u64>,
317        logging: Option<LoggerConfig>,
318        instance_id: Option<UUID4>,
319    ) -> Self {
320        Self::new(
321            Environment::Backtest,
322            trader_id.unwrap_or_default(),
323            load_state,
324            save_state,
325            bypass_logging,
326            run_analysis,
327            timeout_connection,
328            timeout_reconciliation,
329            timeout_portfolio,
330            timeout_disconnection,
331            delay_post_stop,
332            timeout_shutdown,
333            logging,
334            instance_id,
335            None, // cache
336            None, // msgbus
337            None, // data_engine
338            None, // risk_engine
339            None, // exec_engine
340            None, // portfolio
341            None, // streaming
342        )
343    }
344
345    #[getter]
346    #[pyo3(name = "trader_id")]
347    fn py_trader_id(&self) -> TraderId {
348        self.trader_id
349    }
350
351    #[getter]
352    #[pyo3(name = "load_state")]
353    const fn py_load_state(&self) -> bool {
354        self.load_state
355    }
356
357    #[getter]
358    #[pyo3(name = "save_state")]
359    const fn py_save_state(&self) -> bool {
360        self.save_state
361    }
362
363    #[getter]
364    #[pyo3(name = "bypass_logging")]
365    const fn py_bypass_logging(&self) -> bool {
366        self.bypass_logging
367    }
368
369    #[getter]
370    #[pyo3(name = "run_analysis")]
371    const fn py_run_analysis(&self) -> bool {
372        self.run_analysis
373    }
374
375    fn __repr__(&self) -> String {
376        format!("{self:?}")
377    }
378}
379
380/// Represents a venue configuration for one specific backtest engine.
381#[derive(Debug, Clone)]
382#[cfg_attr(
383    feature = "python",
384    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.backtest", from_py_object)
385)]
386pub struct BacktestVenueConfig {
387    /// The name of the venue.
388    name: Ustr,
389    /// The order management system type for the exchange. If ``HEDGING`` will generate new position IDs.
390    oms_type: OmsType,
391    /// The account type for the exchange.
392    account_type: AccountType,
393    /// The default order book type.
394    book_type: BookType,
395    /// The starting account balances (specify one for a single asset account).
396    starting_balances: Vec<String>,
397    /// If multi-venue routing should be enabled for the execution client.
398    routing: bool,
399    /// If the account for this exchange is frozen (balances will not change).
400    frozen_account: bool,
401    /// If stop orders are rejected on submission if trigger price is in the market.
402    reject_stop_orders: bool,
403    /// If orders with GTD time in force will be supported by the venue.
404    support_gtd_orders: bool,
405    /// If contingent orders will be supported/respected by the venue.
406    /// If False, then it's expected the strategy will be managing any contingent orders.
407    support_contingent_orders: bool,
408    /// If venue position IDs will be generated on order fills.
409    use_position_ids: bool,
410    /// If all venue generated identifiers will be random UUID4's.
411    use_random_ids: bool,
412    /// If the `reduce_only` execution instruction on orders will be honored.
413    use_reduce_only: bool,
414    /// If bars should be processed by the matching engine(s) (and move the market).
415    bar_execution: bool,
416    /// Determines whether the processing order of bar prices is adaptive based on a heuristic.
417    /// This setting is only relevant when `bar_execution` is True.
418    /// If False, bar prices are always processed in the fixed order: Open, High, Low, Close.
419    /// If True, the processing order adapts with the heuristic:
420    /// - If High is closer to Open than Low then the processing order is Open, High, Low, Close.
421    /// - If Low is closer to Open than High then the processing order is Open, Low, High, Close.
422    bar_adaptive_high_low_ordering: bool,
423    /// If trades should be processed by the matching engine(s) (and move the market).
424    trade_execution: bool,
425    /// If `OrderAccepted` events should be generated for market orders.
426    use_market_order_acks: bool,
427    /// If order book liquidity consumption should be tracked per level.
428    liquidity_consumption: bool,
429    /// If negative cash balances are allowed (borrowing).
430    allow_cash_borrowing: bool,
431    /// The account base currency for the exchange. Use `None` for multi-currency accounts.
432    base_currency: Option<Currency>,
433    /// The account default leverage (for margin accounts).
434    default_leverage: Option<f64>,
435    /// The instrument specific leverage configuration (for margin accounts).
436    leverages: Option<AHashMap<InstrumentId, f64>>,
437    /// Defines an exchange-calculated price boundary to prevent a market order from being
438    /// filled at an extremely aggressive price.
439    price_protection_points: u32,
440}
441
442impl BacktestVenueConfig {
443    #[allow(clippy::too_many_arguments)]
444    #[must_use]
445    pub fn new(
446        name: Ustr,
447        oms_type: OmsType,
448        account_type: AccountType,
449        book_type: BookType,
450        routing: Option<bool>,
451        frozen_account: Option<bool>,
452        reject_stop_orders: Option<bool>,
453        support_gtd_orders: Option<bool>,
454        support_contingent_orders: Option<bool>,
455        use_position_ids: Option<bool>,
456        use_random_ids: Option<bool>,
457        use_reduce_only: Option<bool>,
458        bar_execution: Option<bool>,
459        bar_adaptive_high_low_ordering: Option<bool>,
460        trade_execution: Option<bool>,
461        use_market_order_acks: Option<bool>,
462        liquidity_consumption: Option<bool>,
463        allow_cash_borrowing: Option<bool>,
464        starting_balances: Vec<String>,
465        base_currency: Option<Currency>,
466        default_leverage: Option<f64>,
467        leverages: Option<AHashMap<InstrumentId, f64>>,
468        price_protection_points: Option<u32>,
469    ) -> Self {
470        Self {
471            name,
472            oms_type,
473            account_type,
474            book_type,
475            routing: routing.unwrap_or(false),
476            frozen_account: frozen_account.unwrap_or(false),
477            reject_stop_orders: reject_stop_orders.unwrap_or(true),
478            support_gtd_orders: support_gtd_orders.unwrap_or(true),
479            support_contingent_orders: support_contingent_orders.unwrap_or(true),
480            use_position_ids: use_position_ids.unwrap_or(true),
481            use_random_ids: use_random_ids.unwrap_or(false),
482            use_reduce_only: use_reduce_only.unwrap_or(true),
483            bar_execution: bar_execution.unwrap_or(true),
484            bar_adaptive_high_low_ordering: bar_adaptive_high_low_ordering.unwrap_or(false),
485            trade_execution: trade_execution.unwrap_or(true),
486            use_market_order_acks: use_market_order_acks.unwrap_or(false),
487            liquidity_consumption: liquidity_consumption.unwrap_or(false),
488            allow_cash_borrowing: allow_cash_borrowing.unwrap_or(false),
489            starting_balances,
490            base_currency,
491            default_leverage,
492            leverages,
493            price_protection_points: price_protection_points.unwrap_or(0),
494        }
495    }
496
497    #[must_use]
498    pub fn name(&self) -> Ustr {
499        self.name
500    }
501
502    #[must_use]
503    pub fn oms_type(&self) -> OmsType {
504        self.oms_type
505    }
506
507    #[must_use]
508    pub fn account_type(&self) -> AccountType {
509        self.account_type
510    }
511
512    #[must_use]
513    pub fn book_type(&self) -> BookType {
514        self.book_type
515    }
516
517    #[must_use]
518    pub fn starting_balances(&self) -> &[String] {
519        &self.starting_balances
520    }
521
522    #[must_use]
523    pub fn routing(&self) -> bool {
524        self.routing
525    }
526
527    #[must_use]
528    pub fn frozen_account(&self) -> bool {
529        self.frozen_account
530    }
531
532    #[must_use]
533    pub fn reject_stop_orders(&self) -> bool {
534        self.reject_stop_orders
535    }
536
537    #[must_use]
538    pub fn support_gtd_orders(&self) -> bool {
539        self.support_gtd_orders
540    }
541
542    #[must_use]
543    pub fn support_contingent_orders(&self) -> bool {
544        self.support_contingent_orders
545    }
546
547    #[must_use]
548    pub fn use_position_ids(&self) -> bool {
549        self.use_position_ids
550    }
551
552    #[must_use]
553    pub fn use_random_ids(&self) -> bool {
554        self.use_random_ids
555    }
556
557    #[must_use]
558    pub fn use_reduce_only(&self) -> bool {
559        self.use_reduce_only
560    }
561
562    #[must_use]
563    pub fn bar_execution(&self) -> bool {
564        self.bar_execution
565    }
566
567    #[must_use]
568    pub fn bar_adaptive_high_low_ordering(&self) -> bool {
569        self.bar_adaptive_high_low_ordering
570    }
571
572    #[must_use]
573    pub fn trade_execution(&self) -> bool {
574        self.trade_execution
575    }
576
577    #[must_use]
578    pub fn use_market_order_acks(&self) -> bool {
579        self.use_market_order_acks
580    }
581
582    #[must_use]
583    pub fn liquidity_consumption(&self) -> bool {
584        self.liquidity_consumption
585    }
586
587    #[must_use]
588    pub fn allow_cash_borrowing(&self) -> bool {
589        self.allow_cash_borrowing
590    }
591
592    #[must_use]
593    pub fn base_currency(&self) -> Option<Currency> {
594        self.base_currency
595    }
596
597    #[must_use]
598    pub fn default_leverage(&self) -> Option<f64> {
599        self.default_leverage
600    }
601
602    #[must_use]
603    pub fn leverages(&self) -> Option<&AHashMap<InstrumentId, f64>> {
604        self.leverages.as_ref()
605    }
606
607    #[must_use]
608    pub fn price_protection_points(&self) -> u32 {
609        self.price_protection_points
610    }
611}
612
613#[cfg(feature = "python")]
614#[pyo3::pymethods]
615impl BacktestVenueConfig {
616    #[new]
617    #[pyo3(signature = (
618        name,
619        oms_type,
620        account_type,
621        book_type,
622        starting_balances,
623        routing = None,
624        frozen_account = None,
625        reject_stop_orders = None,
626        support_gtd_orders = None,
627        support_contingent_orders = None,
628        use_position_ids = None,
629        use_random_ids = None,
630        use_reduce_only = None,
631        bar_execution = None,
632        bar_adaptive_high_low_ordering = None,
633        trade_execution = None,
634        use_market_order_acks = None,
635        liquidity_consumption = None,
636        allow_cash_borrowing = None,
637        base_currency = None,
638        default_leverage = None,
639        leverages = None,
640        price_protection_points = None,
641    ))]
642    #[allow(clippy::too_many_arguments)]
643    fn py_new(
644        name: &str,
645        oms_type: OmsType,
646        account_type: AccountType,
647        book_type: BookType,
648        starting_balances: Vec<String>,
649        routing: Option<bool>,
650        frozen_account: Option<bool>,
651        reject_stop_orders: Option<bool>,
652        support_gtd_orders: Option<bool>,
653        support_contingent_orders: Option<bool>,
654        use_position_ids: Option<bool>,
655        use_random_ids: Option<bool>,
656        use_reduce_only: Option<bool>,
657        bar_execution: Option<bool>,
658        bar_adaptive_high_low_ordering: Option<bool>,
659        trade_execution: Option<bool>,
660        use_market_order_acks: Option<bool>,
661        liquidity_consumption: Option<bool>,
662        allow_cash_borrowing: Option<bool>,
663        base_currency: Option<Currency>,
664        default_leverage: Option<f64>,
665        leverages: Option<HashMap<InstrumentId, f64>>,
666        price_protection_points: Option<u32>,
667    ) -> Self {
668        let leverages = leverages.map(|m| m.into_iter().collect());
669        Self::new(
670            Ustr::from(name),
671            oms_type,
672            account_type,
673            book_type,
674            routing,
675            frozen_account,
676            reject_stop_orders,
677            support_gtd_orders,
678            support_contingent_orders,
679            use_position_ids,
680            use_random_ids,
681            use_reduce_only,
682            bar_execution,
683            bar_adaptive_high_low_ordering,
684            trade_execution,
685            use_market_order_acks,
686            liquidity_consumption,
687            allow_cash_borrowing,
688            starting_balances,
689            base_currency,
690            default_leverage,
691            leverages,
692            price_protection_points,
693        )
694    }
695
696    #[getter]
697    #[pyo3(name = "name")]
698    fn py_name(&self) -> &str {
699        self.name.as_str()
700    }
701
702    #[getter]
703    #[pyo3(name = "oms_type")]
704    const fn py_oms_type(&self) -> OmsType {
705        self.oms_type
706    }
707
708    #[getter]
709    #[pyo3(name = "account_type")]
710    const fn py_account_type(&self) -> AccountType {
711        self.account_type
712    }
713
714    #[getter]
715    #[pyo3(name = "book_type")]
716    const fn py_book_type(&self) -> BookType {
717        self.book_type
718    }
719
720    #[getter]
721    #[pyo3(name = "starting_balances")]
722    fn py_starting_balances(&self) -> Vec<String> {
723        self.starting_balances.clone()
724    }
725
726    #[getter]
727    #[pyo3(name = "bar_execution")]
728    const fn py_bar_execution(&self) -> bool {
729        self.bar_execution
730    }
731
732    #[getter]
733    #[pyo3(name = "trade_execution")]
734    const fn py_trade_execution(&self) -> bool {
735        self.trade_execution
736    }
737
738    fn __repr__(&self) -> String {
739        format!("{self:?}")
740    }
741}
742
743/// Represents the data configuration for one specific backtest run.
744#[derive(Debug, Clone)]
745#[cfg_attr(
746    feature = "python",
747    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.backtest", from_py_object)
748)]
749pub struct BacktestDataConfig {
750    /// The type of data to query from the catalog.
751    data_type: NautilusDataType,
752    /// The path to the data catalog.
753    catalog_path: String,
754    /// The `fsspec` filesystem protocol for the catalog.
755    catalog_fs_protocol: Option<String>,
756    /// The filesystem storage options for the catalog (e.g. cloud auth credentials).
757    catalog_fs_storage_options: Option<AHashMap<String, String>>,
758    /// The instrument ID for the data configuration (single).
759    instrument_id: Option<InstrumentId>,
760    /// Multiple instrument IDs for the data configuration.
761    instrument_ids: Option<Vec<InstrumentId>>,
762    /// The start time for the data configuration.
763    start_time: Option<UnixNanos>,
764    /// The end time for the data configuration.
765    end_time: Option<UnixNanos>,
766    /// The additional filter expressions for the data catalog query.
767    filter_expr: Option<String>,
768    /// The client ID for the data configuration.
769    client_id: Option<ClientId>,
770    /// The metadata for the data catalog query.
771    #[allow(dead_code)]
772    metadata: Option<AHashMap<String, String>>,
773    /// The bar specification for the data catalog query.
774    bar_spec: Option<BarSpecification>,
775    /// Explicit bar type strings for the data catalog query (e.g. "EUR/USD.SIM-1-MINUTE-LAST-EXTERNAL").
776    bar_types: Option<Vec<String>>,
777    /// If directory-based file registration should be used for more efficient loading.
778    optimize_file_loading: bool,
779}
780
781impl BacktestDataConfig {
782    #[allow(clippy::too_many_arguments)]
783    #[must_use]
784    pub fn new(
785        data_type: NautilusDataType,
786        catalog_path: String,
787        catalog_fs_protocol: Option<String>,
788        catalog_fs_storage_options: Option<AHashMap<String, String>>,
789        instrument_id: Option<InstrumentId>,
790        instrument_ids: Option<Vec<InstrumentId>>,
791        start_time: Option<UnixNanos>,
792        end_time: Option<UnixNanos>,
793        filter_expr: Option<String>,
794        client_id: Option<ClientId>,
795        metadata: Option<AHashMap<String, String>>,
796        bar_spec: Option<BarSpecification>,
797        bar_types: Option<Vec<String>>,
798        optimize_file_loading: Option<bool>,
799    ) -> Self {
800        Self {
801            data_type,
802            catalog_path,
803            catalog_fs_protocol,
804            catalog_fs_storage_options,
805            instrument_id,
806            instrument_ids,
807            start_time,
808            end_time,
809            filter_expr,
810            client_id,
811            metadata,
812            bar_spec,
813            bar_types,
814            optimize_file_loading: optimize_file_loading.unwrap_or(false),
815        }
816    }
817
818    #[must_use]
819    pub const fn data_type(&self) -> NautilusDataType {
820        self.data_type
821    }
822
823    #[must_use]
824    pub fn catalog_path(&self) -> &str {
825        &self.catalog_path
826    }
827
828    #[must_use]
829    pub fn catalog_fs_protocol(&self) -> Option<&str> {
830        self.catalog_fs_protocol.as_deref()
831    }
832
833    #[must_use]
834    pub fn catalog_fs_storage_options(&self) -> Option<&AHashMap<String, String>> {
835        self.catalog_fs_storage_options.as_ref()
836    }
837
838    #[must_use]
839    pub fn instrument_id(&self) -> Option<InstrumentId> {
840        self.instrument_id
841    }
842
843    #[must_use]
844    pub fn instrument_ids(&self) -> Option<&[InstrumentId]> {
845        self.instrument_ids.as_deref()
846    }
847
848    #[must_use]
849    pub fn start_time(&self) -> Option<UnixNanos> {
850        self.start_time
851    }
852
853    #[must_use]
854    pub fn end_time(&self) -> Option<UnixNanos> {
855        self.end_time
856    }
857
858    #[must_use]
859    pub fn filter_expr(&self) -> Option<&str> {
860        self.filter_expr.as_deref()
861    }
862
863    #[must_use]
864    pub fn client_id(&self) -> Option<ClientId> {
865        self.client_id
866    }
867
868    #[must_use]
869    pub fn bar_spec(&self) -> Option<BarSpecification> {
870        self.bar_spec
871    }
872
873    #[must_use]
874    pub fn bar_types(&self) -> Option<&[String]> {
875        self.bar_types.as_deref()
876    }
877
878    #[must_use]
879    pub fn optimize_file_loading(&self) -> bool {
880        self.optimize_file_loading
881    }
882
883    /// Constructs identifier strings for catalog queries.
884    ///
885    /// Follows the same logic as Python's `BacktestDataConfig.query`:
886    /// - For bars: prefer `bar_types`, else construct from instrument(s) + bar_spec + "-EXTERNAL"
887    /// - For other types: use `instrument_id` or `instrument_ids`
888    #[must_use]
889    pub fn query_identifiers(&self) -> Option<Vec<String>> {
890        if self.data_type == NautilusDataType::Bar {
891            if let Some(bar_types) = &self.bar_types
892                && !bar_types.is_empty()
893            {
894                return Some(bar_types.clone());
895            }
896
897            // Construct from instrument_id + bar_spec
898            if let Some(bar_spec) = &self.bar_spec {
899                if let Some(id) = self.instrument_id {
900                    return Some(vec![format!("{id}-{bar_spec}-EXTERNAL")]);
901                }
902
903                if let Some(ids) = &self.instrument_ids {
904                    let bar_types: Vec<String> = ids
905                        .iter()
906                        .map(|id| format!("{id}-{bar_spec}-EXTERNAL"))
907                        .collect();
908
909                    if !bar_types.is_empty() {
910                        return Some(bar_types);
911                    }
912                }
913            }
914        }
915
916        // Fallback: instrument_id or instrument_ids
917        if let Some(id) = self.instrument_id {
918            return Some(vec![id.to_string()]);
919        }
920
921        if let Some(ids) = &self.instrument_ids {
922            let strs: Vec<String> = ids.iter().map(ToString::to_string).collect();
923            if !strs.is_empty() {
924                return Some(strs);
925            }
926        }
927
928        None
929    }
930
931    /// Returns all instrument IDs referenced by this config.
932    ///
933    /// For bar_types, extracts the instrument ID from each bar type string.
934    ///
935    /// # Errors
936    ///
937    /// Returns an error if any bar type string cannot be parsed.
938    pub fn get_instrument_ids(&self) -> anyhow::Result<Vec<InstrumentId>> {
939        if let Some(id) = self.instrument_id {
940            return Ok(vec![id]);
941        }
942
943        if let Some(ids) = &self.instrument_ids {
944            return Ok(ids.clone());
945        }
946
947        if let Some(bar_types) = &self.bar_types {
948            let ids = bar_types
949                .iter()
950                .map(|bt| {
951                    bt.parse::<BarType>()
952                        .map(|b| b.instrument_id())
953                        .map_err(|_| anyhow::anyhow!("Invalid bar type string: '{bt}'"))
954                })
955                .collect::<anyhow::Result<Vec<_>>>()?;
956            return Ok(ids);
957        }
958        Ok(Vec::new())
959    }
960}
961
962#[cfg(feature = "python")]
963#[pyo3::pymethods]
964impl BacktestDataConfig {
965    #[new]
966    #[pyo3(signature = (
967        data_type,
968        catalog_path,
969        catalog_fs_protocol = None,
970        catalog_fs_storage_options = None,
971        instrument_id = None,
972        instrument_ids = None,
973        start_time = None,
974        end_time = None,
975        filter_expr = None,
976        client_id = None,
977        metadata = None,
978        bar_spec = None,
979        bar_types = None,
980        optimize_file_loading = None,
981    ))]
982    #[allow(clippy::too_many_arguments)]
983    fn py_new(
984        data_type: &str,
985        catalog_path: String,
986        catalog_fs_protocol: Option<String>,
987        catalog_fs_storage_options: Option<HashMap<String, String>>,
988        instrument_id: Option<InstrumentId>,
989        instrument_ids: Option<Vec<InstrumentId>>,
990        start_time: Option<u64>,
991        end_time: Option<u64>,
992        filter_expr: Option<String>,
993        client_id: Option<ClientId>,
994        metadata: Option<HashMap<String, String>>,
995        bar_spec: Option<BarSpecification>,
996        bar_types: Option<Vec<String>>,
997        optimize_file_loading: Option<bool>,
998    ) -> pyo3::PyResult<Self> {
999        let data_type = data_type
1000            .parse::<NautilusDataType>()
1001            .map_err(nautilus_core::python::to_pyvalue_err)?;
1002        let catalog_fs_storage_options =
1003            catalog_fs_storage_options.map(|m| m.into_iter().collect());
1004        let metadata = metadata.map(|m| m.into_iter().collect());
1005        Ok(Self::new(
1006            data_type,
1007            catalog_path,
1008            catalog_fs_protocol,
1009            catalog_fs_storage_options,
1010            instrument_id,
1011            instrument_ids,
1012            start_time.map(UnixNanos::from),
1013            end_time.map(UnixNanos::from),
1014            filter_expr,
1015            client_id,
1016            metadata,
1017            bar_spec,
1018            bar_types,
1019            optimize_file_loading,
1020        ))
1021    }
1022
1023    #[getter]
1024    #[pyo3(name = "data_type")]
1025    fn py_data_type(&self) -> String {
1026        self.data_type.to_string()
1027    }
1028
1029    #[getter]
1030    #[pyo3(name = "catalog_path")]
1031    fn py_catalog_path(&self) -> &str {
1032        &self.catalog_path
1033    }
1034
1035    #[getter]
1036    #[pyo3(name = "instrument_id")]
1037    fn py_instrument_id(&self) -> Option<InstrumentId> {
1038        self.instrument_id
1039    }
1040
1041    fn __repr__(&self) -> String {
1042        format!("{self:?}")
1043    }
1044}
1045
1046/// Represents the configuration for one specific backtest run.
1047/// This includes a backtest engine with its actors and strategies, with the external inputs of venues and data.
1048#[derive(Debug, Clone)]
1049#[cfg_attr(
1050    feature = "python",
1051    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.backtest", from_py_object)
1052)]
1053pub struct BacktestRunConfig {
1054    /// The unique identifier for this run configuration.
1055    id: String,
1056    /// The venue configurations for the backtest run.
1057    venues: Vec<BacktestVenueConfig>,
1058    /// The data configurations for the backtest run.
1059    data: Vec<BacktestDataConfig>,
1060    /// The backtest engine configuration (the core system kernel).
1061    engine: BacktestEngineConfig,
1062    /// The number of data points to process in each chunk during streaming mode.
1063    /// If `None`, the backtest will run without streaming, loading all data at once.
1064    chunk_size: Option<usize>,
1065    /// If the backtest engine should be disposed on completion of the run.
1066    /// If `True`, then will drop data and all state.
1067    /// If `False`, then will *only* drop data.
1068    dispose_on_completion: bool,
1069    /// The start datetime (UTC) for the backtest run.
1070    /// If `None` engine runs from the start of the data.
1071    start: Option<UnixNanos>,
1072    /// The end datetime (UTC) for the backtest run.
1073    /// If `None` engine runs to the end of the data.
1074    end: Option<UnixNanos>,
1075}
1076
1077impl BacktestRunConfig {
1078    #[allow(clippy::too_many_arguments)]
1079    #[must_use]
1080    pub fn new(
1081        id: Option<String>,
1082        venues: Vec<BacktestVenueConfig>,
1083        data: Vec<BacktestDataConfig>,
1084        engine: BacktestEngineConfig,
1085        chunk_size: Option<usize>,
1086        dispose_on_completion: Option<bool>,
1087        start: Option<UnixNanos>,
1088        end: Option<UnixNanos>,
1089    ) -> Self {
1090        Self {
1091            id: id.unwrap_or_else(|| UUID4::new().to_string()),
1092            venues,
1093            data,
1094            engine,
1095            chunk_size,
1096            dispose_on_completion: dispose_on_completion.unwrap_or(true),
1097            start,
1098            end,
1099        }
1100    }
1101
1102    #[must_use]
1103    pub fn id(&self) -> &str {
1104        &self.id
1105    }
1106
1107    #[must_use]
1108    pub fn venues(&self) -> &[BacktestVenueConfig] {
1109        &self.venues
1110    }
1111
1112    #[must_use]
1113    pub fn data(&self) -> &[BacktestDataConfig] {
1114        &self.data
1115    }
1116
1117    #[must_use]
1118    pub fn engine(&self) -> &BacktestEngineConfig {
1119        &self.engine
1120    }
1121
1122    #[must_use]
1123    pub fn chunk_size(&self) -> Option<usize> {
1124        self.chunk_size
1125    }
1126
1127    #[must_use]
1128    pub fn dispose_on_completion(&self) -> bool {
1129        self.dispose_on_completion
1130    }
1131
1132    #[must_use]
1133    pub fn start(&self) -> Option<UnixNanos> {
1134        self.start
1135    }
1136
1137    #[must_use]
1138    pub fn end(&self) -> Option<UnixNanos> {
1139        self.end
1140    }
1141}
1142
1143#[cfg(feature = "python")]
1144#[pyo3::pymethods]
1145impl BacktestRunConfig {
1146    #[new]
1147    #[pyo3(signature = (
1148        venues,
1149        data,
1150        engine = None,
1151        id = None,
1152        chunk_size = None,
1153        dispose_on_completion = None,
1154        start = None,
1155        end = None,
1156    ))]
1157    #[allow(clippy::too_many_arguments)]
1158    fn py_new(
1159        venues: Vec<BacktestVenueConfig>,
1160        data: Vec<BacktestDataConfig>,
1161        engine: Option<BacktestEngineConfig>,
1162        id: Option<String>,
1163        chunk_size: Option<usize>,
1164        dispose_on_completion: Option<bool>,
1165        start: Option<u64>,
1166        end: Option<u64>,
1167    ) -> Self {
1168        Self::new(
1169            id,
1170            venues,
1171            data,
1172            engine.unwrap_or_default(),
1173            chunk_size,
1174            dispose_on_completion,
1175            start.map(UnixNanos::from),
1176            end.map(UnixNanos::from),
1177        )
1178    }
1179
1180    #[getter]
1181    #[pyo3(name = "id")]
1182    fn py_id(&self) -> &str {
1183        &self.id
1184    }
1185
1186    fn __repr__(&self) -> String {
1187        format!("{self:?}")
1188    }
1189}