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::{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::{
28    engine::config::ExecutionEngineConfig,
29    models::{
30        fee::FeeModelAny,
31        fill::FillModelAny,
32        latency::{LatencyModel, LatencyModelAny},
33    },
34};
35use nautilus_model::{
36    accounts::margin_model::MarginModelAny,
37    data::{BarSpecification, BarType},
38    enums::{AccountType, BookType, OmsType, OtoTriggerMode},
39    identifiers::{ClientId, InstrumentId, TraderId, Venue},
40    types::{Currency, Money},
41};
42use nautilus_portfolio::config::PortfolioConfig;
43use nautilus_risk::engine::config::RiskEngineConfig;
44use nautilus_system::config::{NautilusKernelConfig, StreamingConfig};
45use rust_decimal::Decimal;
46use ustr::Ustr;
47
48use crate::modules::{SimulationModule, SimulationModuleAny};
49
50/// Represents a type of market data for catalog queries.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum NautilusDataType {
53    QuoteTick,
54    TradeTick,
55    Bar,
56    OrderBookDelta,
57    OrderBookDepth10,
58    MarkPriceUpdate,
59    IndexPriceUpdate,
60    InstrumentStatus,
61    InstrumentClose,
62}
63
64impl Display for NautilusDataType {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        std::fmt::Debug::fmt(self, f)
67    }
68}
69
70impl FromStr for NautilusDataType {
71    type Err = anyhow::Error;
72
73    fn from_str(s: &str) -> anyhow::Result<Self> {
74        match s {
75            stringify!(QuoteTick) => Ok(Self::QuoteTick),
76            stringify!(TradeTick) => Ok(Self::TradeTick),
77            stringify!(Bar) => Ok(Self::Bar),
78            stringify!(OrderBookDelta) => Ok(Self::OrderBookDelta),
79            stringify!(OrderBookDepth10) => Ok(Self::OrderBookDepth10),
80            stringify!(MarkPriceUpdate) => Ok(Self::MarkPriceUpdate),
81            stringify!(IndexPriceUpdate) => Ok(Self::IndexPriceUpdate),
82            stringify!(InstrumentStatus) => Ok(Self::InstrumentStatus),
83            stringify!(InstrumentClose) => Ok(Self::InstrumentClose),
84            _ => anyhow::bail!("Invalid `NautilusDataType`: '{s}'"),
85        }
86    }
87}
88
89/// Configuration for ``BacktestEngine`` instances.
90#[derive(Debug, Clone, bon::Builder)]
91#[cfg_attr(
92    feature = "python",
93    pyo3::pyclass(
94        module = "nautilus_trader.core.nautilus_pyo3.backtest",
95        from_py_object,
96        unsendable
97    )
98)]
99#[cfg_attr(
100    feature = "python",
101    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.backtest")
102)]
103pub struct BacktestEngineConfig {
104    /// The kernel environment context.
105    #[builder(default = Environment::Backtest)]
106    pub environment: Environment,
107    /// The trader ID for the node.
108    #[builder(default)]
109    pub trader_id: TraderId,
110    /// If trading strategy state should be loaded from the database on start.
111    #[builder(default)]
112    pub load_state: bool,
113    /// If trading strategy state should be saved to the database on stop.
114    #[builder(default)]
115    pub save_state: bool,
116    /// The logging configuration for the kernel.
117    #[builder(default)]
118    pub logging: LoggerConfig,
119    /// The unique instance identifier for the kernel.
120    pub instance_id: Option<UUID4>,
121    /// The timeout for all clients to connect and initialize.
122    #[builder(default = Duration::from_secs(60))]
123    pub timeout_connection: Duration,
124    /// The timeout for execution state to reconcile.
125    #[builder(default = Duration::from_secs(30))]
126    pub timeout_reconciliation: Duration,
127    /// The timeout for portfolio to initialize margins and unrealized pnls.
128    #[builder(default = Duration::from_secs(10))]
129    pub timeout_portfolio: Duration,
130    /// The timeout for all engine clients to disconnect.
131    #[builder(default = Duration::from_secs(10))]
132    pub timeout_disconnection: Duration,
133    /// The delay after stopping the node to await residual events before final shutdown.
134    #[builder(default = Duration::from_secs(10))]
135    pub delay_post_stop: Duration,
136    /// The timeout to await pending tasks cancellation during shutdown.
137    #[builder(default = Duration::from_secs(5))]
138    pub timeout_shutdown: Duration,
139    /// The cache configuration.
140    ///
141    /// [`crate::engine::BacktestEngine`] always overrides
142    /// `drop_instruments_on_reset` to `false` on this config so that
143    /// successive runs can reuse the same dataset.
144    pub cache: Option<CacheConfig>,
145    /// The message bus configuration.
146    pub msgbus: Option<MessageBusConfig>,
147    /// The data engine configuration.
148    pub data_engine: Option<DataEngineConfig>,
149    /// The risk engine configuration.
150    pub risk_engine: Option<RiskEngineConfig>,
151    /// The execution engine configuration.
152    pub exec_engine: Option<ExecutionEngineConfig>,
153    /// The portfolio configuration.
154    pub portfolio: Option<PortfolioConfig>,
155    /// The configuration for streaming to feather files.
156    pub streaming: Option<StreamingConfig>,
157    /// If logging should be bypassed.
158    #[builder(default)]
159    pub bypass_logging: bool,
160    /// If post backtest performance analysis should be run.
161    #[builder(default = true)]
162    pub run_analysis: bool,
163}
164
165impl NautilusKernelConfig for BacktestEngineConfig {
166    fn environment(&self) -> Environment {
167        self.environment
168    }
169
170    fn trader_id(&self) -> TraderId {
171        self.trader_id
172    }
173
174    fn load_state(&self) -> bool {
175        self.load_state
176    }
177
178    fn save_state(&self) -> bool {
179        self.save_state
180    }
181
182    fn logging(&self) -> LoggerConfig {
183        self.logging.clone()
184    }
185
186    fn instance_id(&self) -> Option<UUID4> {
187        self.instance_id
188    }
189
190    fn timeout_connection(&self) -> Duration {
191        self.timeout_connection
192    }
193
194    fn timeout_reconciliation(&self) -> Duration {
195        self.timeout_reconciliation
196    }
197
198    fn timeout_portfolio(&self) -> Duration {
199        self.timeout_portfolio
200    }
201
202    fn timeout_disconnection(&self) -> Duration {
203        self.timeout_disconnection
204    }
205
206    fn delay_post_stop(&self) -> Duration {
207        self.delay_post_stop
208    }
209
210    fn timeout_shutdown(&self) -> Duration {
211        self.timeout_shutdown
212    }
213
214    fn cache(&self) -> Option<CacheConfig> {
215        self.cache.clone()
216    }
217
218    fn msgbus(&self) -> Option<MessageBusConfig> {
219        self.msgbus.clone()
220    }
221
222    fn data_engine(&self) -> Option<DataEngineConfig> {
223        self.data_engine.clone()
224    }
225
226    fn risk_engine(&self) -> Option<RiskEngineConfig> {
227        self.risk_engine.clone()
228    }
229
230    fn exec_engine(&self) -> Option<ExecutionEngineConfig> {
231        self.exec_engine.clone()
232    }
233
234    fn portfolio(&self) -> Option<PortfolioConfig> {
235        self.portfolio
236    }
237
238    fn streaming(&self) -> Option<StreamingConfig> {
239        self.streaming.clone()
240    }
241}
242
243impl Default for BacktestEngineConfig {
244    fn default() -> Self {
245        Self::builder().build()
246    }
247}
248
249/// Imperative-API configuration for registering a simulated venue on
250/// [`crate::engine::BacktestEngine`].
251///
252/// Constructed via [`bon::Builder`] so callers only specify what differs from
253/// the documented defaults. Field types mirror the internal
254/// `SimulatedExchange` shapes (trait objects for modules/latency, typed
255/// `Money` balances), which is why this is distinct from the YAML-friendly
256/// [`BacktestVenueConfig`] used by `BacktestNode`.
257#[derive(bon::Builder)]
258#[allow(missing_debug_implementations)]
259pub struct SimulatedVenueConfig {
260    pub venue: Venue,
261    pub oms_type: OmsType,
262    pub account_type: AccountType,
263    pub book_type: BookType,
264    pub starting_balances: Vec<Money>,
265    pub base_currency: Option<Currency>,
266    // Left optional so the engine can fall back to an account-type-appropriate
267    // default (10x for margin, 1x otherwise) when the caller has no preference.
268    pub default_leverage: Option<Decimal>,
269    #[builder(default)]
270    pub leverages: AHashMap<InstrumentId, Decimal>,
271    pub margin_model: Option<MarginModelAny>,
272    #[builder(default)]
273    pub modules: Vec<Box<dyn SimulationModule>>,
274    #[builder(default)]
275    pub fill_model: FillModelAny,
276    #[builder(default)]
277    pub fee_model: FeeModelAny,
278    pub latency_model: Option<Box<dyn LatencyModel>>,
279    #[builder(default = false)]
280    pub routing: bool,
281    #[builder(default = true)]
282    pub reject_stop_orders: bool,
283    #[builder(default = true)]
284    pub support_gtd_orders: bool,
285    #[builder(default = true)]
286    pub support_contingent_orders: bool,
287    #[builder(default = true)]
288    pub use_position_ids: bool,
289    #[builder(default = false)]
290    pub use_random_ids: bool,
291    #[builder(default = true)]
292    pub use_reduce_only: bool,
293    #[builder(default = true)]
294    pub use_message_queue: bool,
295    #[builder(default = false)]
296    pub use_market_order_acks: bool,
297    #[builder(default = true)]
298    pub bar_execution: bool,
299    #[builder(default = false)]
300    pub bar_adaptive_high_low_ordering: bool,
301    #[builder(default = true)]
302    pub trade_execution: bool,
303    #[builder(default = false)]
304    pub liquidity_consumption: bool,
305    #[builder(default = false)]
306    pub allow_cash_borrowing: bool,
307    #[builder(default = false)]
308    pub frozen_account: bool,
309    #[builder(default = false)]
310    pub queue_position: bool,
311    #[builder(default = false)]
312    pub oto_full_trigger: bool,
313    #[builder(default = 0)]
314    pub price_protection_points: u32,
315}
316
317/// Represents a venue configuration for one specific backtest engine.
318#[derive(Debug, Clone, bon::Builder)]
319#[cfg_attr(
320    feature = "python",
321    pyo3::pyclass(
322        module = "nautilus_trader.core.nautilus_pyo3.backtest",
323        from_py_object,
324        unsendable
325    )
326)]
327#[cfg_attr(
328    feature = "python",
329    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.backtest")
330)]
331pub struct BacktestVenueConfig {
332    /// The name of the venue.
333    name: Ustr,
334    /// The order management system type for the exchange. If ``HEDGING`` will generate new position IDs.
335    oms_type: OmsType,
336    /// The account type for the exchange.
337    account_type: AccountType,
338    /// The default order book type.
339    book_type: BookType,
340    /// The starting account balances (specify one for a single asset account).
341    #[builder(default)]
342    starting_balances: Vec<String>,
343    /// If multi-venue routing should be enabled for the execution client.
344    #[builder(default)]
345    routing: bool,
346    /// If the account for this exchange is frozen (balances will not change).
347    #[builder(default)]
348    frozen_account: bool,
349    /// If stop orders are rejected on submission if trigger price is in the market.
350    #[builder(default = true)]
351    reject_stop_orders: bool,
352    /// If orders with GTD time in force will be supported by the venue.
353    #[builder(default = true)]
354    support_gtd_orders: bool,
355    /// If contingent orders will be supported/respected by the venue.
356    /// If False, then it's expected the strategy will be managing any contingent orders.
357    #[builder(default = true)]
358    support_contingent_orders: bool,
359    /// If venue position IDs will be generated on order fills.
360    #[builder(default = true)]
361    use_position_ids: bool,
362    /// If venue order IDs and position IDs will be random UUID4's.
363    /// Trade IDs are always deterministic and not affected by this flag.
364    #[builder(default)]
365    use_random_ids: bool,
366    /// If the `reduce_only` execution instruction on orders will be honored.
367    #[builder(default = true)]
368    use_reduce_only: bool,
369    /// If bars should be processed by the matching engine(s) (and move the market).
370    #[builder(default = true)]
371    bar_execution: bool,
372    /// Determines whether the processing order of bar prices is adaptive based on a heuristic.
373    /// This setting is only relevant when `bar_execution` is True.
374    /// If False, bar prices are always processed in the fixed order: Open, High, Low, Close.
375    /// If True, the processing order adapts with the heuristic:
376    /// - If High is closer to Open than Low then the processing order is Open, High, Low, Close.
377    /// - If Low is closer to Open than High then the processing order is Open, Low, High, Close.
378    #[builder(default)]
379    bar_adaptive_high_low_ordering: bool,
380    /// If trades should be processed by the matching engine(s) (and move the market).
381    #[builder(default = true)]
382    trade_execution: bool,
383    /// If `OrderAccepted` events should be generated for market orders.
384    #[builder(default)]
385    use_market_order_acks: bool,
386    /// If order book liquidity consumption should be tracked per level.
387    #[builder(default)]
388    liquidity_consumption: bool,
389    /// If negative cash balances are allowed (borrowing).
390    #[builder(default)]
391    allow_cash_borrowing: bool,
392    /// If limit order queue position tracking is enabled during trade execution.
393    #[builder(default)]
394    queue_position: bool,
395    /// When OTO child orders are released relative to parent fills.
396    #[builder(default)]
397    oto_trigger_mode: OtoTriggerMode,
398    /// The account base currency for the exchange. Use `None` for multi-currency accounts.
399    base_currency: Option<Currency>,
400    /// The account default leverage (for margin accounts).
401    #[builder(default = Decimal::ONE)]
402    default_leverage: Decimal,
403    /// The instrument specific leverage configuration (for margin accounts).
404    leverages: Option<AHashMap<InstrumentId, Decimal>>,
405    /// The margin model for the venue.
406    margin_model: Option<MarginModelAny>,
407    /// The simulation modules for the venue.
408    #[builder(default)]
409    modules: Vec<SimulationModuleAny>,
410    /// The fill model for the venue.
411    fill_model: Option<FillModelAny>,
412    /// The latency model for the venue.
413    latency_model: Option<LatencyModelAny>,
414    /// The fee model for the venue.
415    fee_model: Option<FeeModelAny>,
416    /// Defines an exchange-calculated price boundary to prevent a market order from being
417    /// filled at an extremely aggressive price.
418    #[builder(default)]
419    price_protection_points: u32,
420    /// Settlement prices for expiring instruments keyed by instrument ID.
421    settlement_prices: Option<AHashMap<InstrumentId, f64>>,
422}
423
424impl BacktestVenueConfig {
425    #[must_use]
426    pub fn name(&self) -> Ustr {
427        self.name
428    }
429
430    #[must_use]
431    pub fn oms_type(&self) -> OmsType {
432        self.oms_type
433    }
434
435    #[must_use]
436    pub fn account_type(&self) -> AccountType {
437        self.account_type
438    }
439
440    #[must_use]
441    pub fn book_type(&self) -> BookType {
442        self.book_type
443    }
444
445    #[must_use]
446    pub fn starting_balances(&self) -> &[String] {
447        &self.starting_balances
448    }
449
450    #[must_use]
451    pub fn routing(&self) -> bool {
452        self.routing
453    }
454
455    #[must_use]
456    pub fn frozen_account(&self) -> bool {
457        self.frozen_account
458    }
459
460    #[must_use]
461    pub fn reject_stop_orders(&self) -> bool {
462        self.reject_stop_orders
463    }
464
465    #[must_use]
466    pub fn support_gtd_orders(&self) -> bool {
467        self.support_gtd_orders
468    }
469
470    #[must_use]
471    pub fn support_contingent_orders(&self) -> bool {
472        self.support_contingent_orders
473    }
474
475    #[must_use]
476    pub fn use_position_ids(&self) -> bool {
477        self.use_position_ids
478    }
479
480    #[must_use]
481    pub fn use_random_ids(&self) -> bool {
482        self.use_random_ids
483    }
484
485    #[must_use]
486    pub fn use_reduce_only(&self) -> bool {
487        self.use_reduce_only
488    }
489
490    #[must_use]
491    pub fn bar_execution(&self) -> bool {
492        self.bar_execution
493    }
494
495    #[must_use]
496    pub fn bar_adaptive_high_low_ordering(&self) -> bool {
497        self.bar_adaptive_high_low_ordering
498    }
499
500    #[must_use]
501    pub fn trade_execution(&self) -> bool {
502        self.trade_execution
503    }
504
505    #[must_use]
506    pub fn use_market_order_acks(&self) -> bool {
507        self.use_market_order_acks
508    }
509
510    #[must_use]
511    pub fn liquidity_consumption(&self) -> bool {
512        self.liquidity_consumption
513    }
514
515    #[must_use]
516    pub fn allow_cash_borrowing(&self) -> bool {
517        self.allow_cash_borrowing
518    }
519
520    #[must_use]
521    pub fn queue_position(&self) -> bool {
522        self.queue_position
523    }
524
525    #[must_use]
526    pub fn oto_trigger_mode(&self) -> OtoTriggerMode {
527        self.oto_trigger_mode
528    }
529
530    #[must_use]
531    pub fn base_currency(&self) -> Option<Currency> {
532        self.base_currency
533    }
534
535    #[must_use]
536    pub fn default_leverage(&self) -> Decimal {
537        self.default_leverage
538    }
539
540    #[must_use]
541    pub fn leverages(&self) -> Option<&AHashMap<InstrumentId, Decimal>> {
542        self.leverages.as_ref()
543    }
544
545    #[must_use]
546    pub fn margin_model(&self) -> Option<&MarginModelAny> {
547        self.margin_model.as_ref()
548    }
549
550    #[must_use]
551    pub fn modules(&self) -> &[SimulationModuleAny] {
552        &self.modules
553    }
554
555    #[must_use]
556    pub fn fill_model(&self) -> Option<&FillModelAny> {
557        self.fill_model.as_ref()
558    }
559
560    #[must_use]
561    pub fn latency_model(&self) -> Option<&LatencyModelAny> {
562        self.latency_model.as_ref()
563    }
564
565    #[must_use]
566    pub fn fee_model(&self) -> Option<&FeeModelAny> {
567        self.fee_model.as_ref()
568    }
569
570    #[must_use]
571    pub fn price_protection_points(&self) -> u32 {
572        self.price_protection_points
573    }
574
575    #[must_use]
576    pub fn settlement_prices(&self) -> Option<&AHashMap<InstrumentId, f64>> {
577        self.settlement_prices.as_ref()
578    }
579}
580
581/// Represents the data configuration for one specific backtest run.
582#[derive(Debug, Clone, bon::Builder)]
583#[cfg_attr(
584    feature = "python",
585    pyo3::pyclass(
586        module = "nautilus_trader.core.nautilus_pyo3.backtest",
587        from_py_object,
588        unsendable
589    )
590)]
591#[cfg_attr(
592    feature = "python",
593    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.backtest")
594)]
595pub struct BacktestDataConfig {
596    /// The type of data to query from the catalog.
597    data_type: NautilusDataType,
598    /// The path to the data catalog.
599    catalog_path: String,
600    /// The `fsspec` filesystem protocol for the catalog.
601    catalog_fs_protocol: Option<String>,
602    /// The filesystem storage options for the catalog (e.g. cloud auth credentials).
603    catalog_fs_storage_options: Option<AHashMap<String, String>>,
604    /// Rust-specific storage options for the catalog backend.
605    catalog_fs_rust_storage_options: Option<AHashMap<String, String>>,
606    /// The instrument ID for the data configuration (single).
607    instrument_id: Option<InstrumentId>,
608    /// Multiple instrument IDs for the data configuration.
609    instrument_ids: Option<Vec<InstrumentId>>,
610    /// The start time for the data configuration.
611    start_time: Option<UnixNanos>,
612    /// The end time for the data configuration.
613    end_time: Option<UnixNanos>,
614    /// The additional filter expressions for the data catalog query.
615    filter_expr: Option<String>,
616    /// The client ID for the data configuration.
617    client_id: Option<ClientId>,
618    /// The metadata for the data catalog query.
619    #[allow(dead_code)]
620    metadata: Option<AHashMap<String, String>>,
621    /// The bar specification for the data catalog query.
622    bar_spec: Option<BarSpecification>,
623    /// Explicit bar type strings for the data catalog query (e.g. "EUR/USD.SIM-1-MINUTE-LAST-EXTERNAL").
624    bar_types: Option<Vec<String>>,
625    /// If directory-based file registration should be used for more efficient loading.
626    #[builder(default)]
627    optimize_file_loading: bool,
628}
629
630impl BacktestDataConfig {
631    #[must_use]
632    pub const fn data_type(&self) -> NautilusDataType {
633        self.data_type
634    }
635
636    #[must_use]
637    pub fn catalog_path(&self) -> &str {
638        &self.catalog_path
639    }
640
641    #[must_use]
642    pub fn catalog_fs_protocol(&self) -> Option<&str> {
643        self.catalog_fs_protocol.as_deref()
644    }
645
646    #[must_use]
647    pub fn catalog_fs_storage_options(&self) -> Option<&AHashMap<String, String>> {
648        self.catalog_fs_storage_options.as_ref()
649    }
650
651    #[must_use]
652    pub fn catalog_fs_rust_storage_options(&self) -> Option<&AHashMap<String, String>> {
653        self.catalog_fs_rust_storage_options.as_ref()
654    }
655
656    #[must_use]
657    pub fn instrument_id(&self) -> Option<InstrumentId> {
658        self.instrument_id
659    }
660
661    #[must_use]
662    pub fn instrument_ids(&self) -> Option<&[InstrumentId]> {
663        self.instrument_ids.as_deref()
664    }
665
666    #[must_use]
667    pub fn start_time(&self) -> Option<UnixNanos> {
668        self.start_time
669    }
670
671    #[must_use]
672    pub fn end_time(&self) -> Option<UnixNanos> {
673        self.end_time
674    }
675
676    #[must_use]
677    pub fn filter_expr(&self) -> Option<&str> {
678        self.filter_expr.as_deref()
679    }
680
681    #[must_use]
682    pub fn client_id(&self) -> Option<ClientId> {
683        self.client_id
684    }
685
686    #[must_use]
687    pub fn bar_spec(&self) -> Option<BarSpecification> {
688        self.bar_spec
689    }
690
691    #[must_use]
692    pub fn bar_types(&self) -> Option<&[String]> {
693        self.bar_types.as_deref()
694    }
695
696    #[must_use]
697    pub fn optimize_file_loading(&self) -> bool {
698        self.optimize_file_loading
699    }
700
701    /// Constructs identifier strings for catalog queries.
702    ///
703    /// Follows the same logic as Python's `BacktestDataConfig.query`:
704    /// - For bars: prefer `bar_types`, else construct from instrument(s) + bar_spec + "-EXTERNAL"
705    /// - For other types: use `instrument_id` or `instrument_ids`
706    #[must_use]
707    pub fn query_identifiers(&self) -> Option<Vec<String>> {
708        if self.data_type == NautilusDataType::Bar {
709            if let Some(bar_types) = &self.bar_types
710                && !bar_types.is_empty()
711            {
712                return Some(bar_types.clone());
713            }
714
715            // Construct from instrument_id + bar_spec
716            if let Some(bar_spec) = &self.bar_spec {
717                if let Some(id) = self.instrument_id {
718                    return Some(vec![format!("{id}-{bar_spec}-EXTERNAL")]);
719                }
720
721                if let Some(ids) = &self.instrument_ids {
722                    let bar_types: Vec<String> = ids
723                        .iter()
724                        .map(|id| format!("{id}-{bar_spec}-EXTERNAL"))
725                        .collect();
726
727                    if !bar_types.is_empty() {
728                        return Some(bar_types);
729                    }
730                }
731            }
732        }
733
734        // Fallback: instrument_id or instrument_ids
735        if let Some(id) = self.instrument_id {
736            return Some(vec![id.to_string()]);
737        }
738
739        if let Some(ids) = &self.instrument_ids {
740            let strs: Vec<String> = ids.iter().map(ToString::to_string).collect();
741            if !strs.is_empty() {
742                return Some(strs);
743            }
744        }
745
746        None
747    }
748
749    /// Returns all instrument IDs referenced by this config.
750    ///
751    /// For bar_types, extracts the instrument ID from each bar type string.
752    ///
753    /// # Errors
754    ///
755    /// Returns an error if any bar type string cannot be parsed.
756    pub fn get_instrument_ids(&self) -> anyhow::Result<Vec<InstrumentId>> {
757        if let Some(id) = self.instrument_id {
758            return Ok(vec![id]);
759        }
760
761        if let Some(ids) = &self.instrument_ids {
762            return Ok(ids.clone());
763        }
764
765        if let Some(bar_types) = &self.bar_types {
766            let ids = bar_types
767                .iter()
768                .map(|bt| {
769                    bt.parse::<BarType>()
770                        .map(|b| b.instrument_id())
771                        .map_err(|_| anyhow::anyhow!("Invalid bar type string: '{bt}'"))
772                })
773                .collect::<anyhow::Result<Vec<_>>>()?;
774            return Ok(ids);
775        }
776        Ok(Vec::new())
777    }
778}
779
780/// Represents the configuration for one specific backtest run.
781/// This includes a backtest engine with its actors and strategies, with the external inputs of venues and data.
782#[derive(Debug, Clone, bon::Builder)]
783#[cfg_attr(
784    feature = "python",
785    pyo3::pyclass(
786        module = "nautilus_trader.core.nautilus_pyo3.backtest",
787        from_py_object,
788        unsendable
789    )
790)]
791#[cfg_attr(
792    feature = "python",
793    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.backtest")
794)]
795pub struct BacktestRunConfig {
796    /// The unique identifier for this run configuration.
797    #[builder(default = UUID4::new().to_string())]
798    id: String,
799    /// The venue configurations for the backtest run.
800    venues: Vec<BacktestVenueConfig>,
801    /// The data configurations for the backtest run.
802    data: Vec<BacktestDataConfig>,
803    /// The backtest engine configuration (the core system kernel).
804    #[builder(default)]
805    engine: BacktestEngineConfig,
806    /// The number of data points to process in each chunk during streaming mode.
807    /// If `None`, the backtest will run without streaming, loading all data at once.
808    chunk_size: Option<usize>,
809    /// If exceptions during build or run should interrupt processing.
810    #[builder(default)]
811    raise_exception: bool,
812    /// If the backtest engine should be disposed on completion of the run.
813    /// If `True`, then will drop data and all state.
814    /// If `False`, then will *only* drop data.
815    #[builder(default = true)]
816    dispose_on_completion: bool,
817    /// The start datetime (UTC) for the backtest run.
818    /// If `None` engine runs from the start of the data.
819    start: Option<UnixNanos>,
820    /// The end datetime (UTC) for the backtest run.
821    /// If `None` engine runs to the end of the data.
822    end: Option<UnixNanos>,
823}
824
825impl BacktestRunConfig {
826    #[must_use]
827    pub fn id(&self) -> &str {
828        &self.id
829    }
830
831    #[must_use]
832    pub fn venues(&self) -> &[BacktestVenueConfig] {
833        &self.venues
834    }
835
836    #[must_use]
837    pub fn data(&self) -> &[BacktestDataConfig] {
838        &self.data
839    }
840
841    #[must_use]
842    pub fn engine(&self) -> &BacktestEngineConfig {
843        &self.engine
844    }
845
846    #[must_use]
847    pub fn chunk_size(&self) -> Option<usize> {
848        self.chunk_size
849    }
850
851    #[must_use]
852    pub fn raise_exception(&self) -> bool {
853        self.raise_exception
854    }
855
856    #[must_use]
857    pub fn dispose_on_completion(&self) -> bool {
858        self.dispose_on_completion
859    }
860
861    #[must_use]
862    pub fn start(&self) -> Option<UnixNanos> {
863        self.start
864    }
865
866    #[must_use]
867    pub fn end(&self) -> Option<UnixNanos> {
868        self.end
869    }
870}