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