1use 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#[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#[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 #[builder(default = Environment::Backtest)]
106 pub environment: Environment,
107 #[builder(default)]
109 pub trader_id: TraderId,
110 #[builder(default)]
112 pub load_state: bool,
113 #[builder(default)]
115 pub save_state: bool,
116 #[builder(default)]
118 pub logging: LoggerConfig,
119 pub instance_id: Option<UUID4>,
121 #[builder(default = Duration::from_secs(60))]
123 pub timeout_connection: Duration,
124 #[builder(default = Duration::from_secs(30))]
126 pub timeout_reconciliation: Duration,
127 #[builder(default = Duration::from_secs(10))]
129 pub timeout_portfolio: Duration,
130 #[builder(default = Duration::from_secs(10))]
132 pub timeout_disconnection: Duration,
133 #[builder(default = Duration::from_secs(10))]
135 pub delay_post_stop: Duration,
136 #[builder(default = Duration::from_secs(5))]
138 pub timeout_shutdown: Duration,
139 pub cache: Option<CacheConfig>,
145 pub msgbus: Option<MessageBusConfig>,
147 pub data_engine: Option<DataEngineConfig>,
149 pub risk_engine: Option<RiskEngineConfig>,
151 pub exec_engine: Option<ExecutionEngineConfig>,
153 pub portfolio: Option<PortfolioConfig>,
155 pub streaming: Option<StreamingConfig>,
157 #[builder(default)]
159 pub bypass_logging: bool,
160 #[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#[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 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#[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 name: Ustr,
334 oms_type: OmsType,
336 account_type: AccountType,
338 book_type: BookType,
340 #[builder(default)]
342 starting_balances: Vec<String>,
343 #[builder(default)]
345 routing: bool,
346 #[builder(default)]
348 frozen_account: bool,
349 #[builder(default = true)]
351 reject_stop_orders: bool,
352 #[builder(default = true)]
354 support_gtd_orders: bool,
355 #[builder(default = true)]
358 support_contingent_orders: bool,
359 #[builder(default = true)]
361 use_position_ids: bool,
362 #[builder(default)]
365 use_random_ids: bool,
366 #[builder(default = true)]
368 use_reduce_only: bool,
369 #[builder(default = true)]
371 bar_execution: bool,
372 #[builder(default)]
379 bar_adaptive_high_low_ordering: bool,
380 #[builder(default = true)]
382 trade_execution: bool,
383 #[builder(default)]
385 use_market_order_acks: bool,
386 #[builder(default)]
388 liquidity_consumption: bool,
389 #[builder(default)]
391 allow_cash_borrowing: bool,
392 #[builder(default)]
394 queue_position: bool,
395 #[builder(default)]
397 oto_trigger_mode: OtoTriggerMode,
398 base_currency: Option<Currency>,
400 #[builder(default = Decimal::ONE)]
402 default_leverage: Decimal,
403 leverages: Option<AHashMap<InstrumentId, Decimal>>,
405 margin_model: Option<MarginModelAny>,
407 #[builder(default)]
409 modules: Vec<SimulationModuleAny>,
410 fill_model: Option<FillModelAny>,
412 latency_model: Option<LatencyModelAny>,
414 fee_model: Option<FeeModelAny>,
416 #[builder(default)]
419 price_protection_points: u32,
420 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#[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 data_type: NautilusDataType,
598 catalog_path: String,
600 catalog_fs_protocol: Option<String>,
602 catalog_fs_storage_options: Option<AHashMap<String, String>>,
604 catalog_fs_rust_storage_options: Option<AHashMap<String, String>>,
606 instrument_id: Option<InstrumentId>,
608 instrument_ids: Option<Vec<InstrumentId>>,
610 start_time: Option<UnixNanos>,
612 end_time: Option<UnixNanos>,
614 filter_expr: Option<String>,
616 client_id: Option<ClientId>,
618 #[allow(dead_code)]
620 metadata: Option<AHashMap<String, String>>,
621 bar_spec: Option<BarSpecification>,
623 bar_types: Option<Vec<String>>,
625 #[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 #[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 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 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 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#[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 #[builder(default = UUID4::new().to_string())]
798 id: String,
799 venues: Vec<BacktestVenueConfig>,
801 data: Vec<BacktestDataConfig>,
803 #[builder(default)]
805 engine: BacktestEngineConfig,
806 chunk_size: Option<usize>,
809 #[builder(default)]
811 raise_exception: bool,
812 #[builder(default = true)]
816 dispose_on_completion: bool,
817 start: Option<UnixNanos>,
820 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}