1use polars::prelude::*;
6use quantwave_backtest::{
7 monte_carlo_return_paths, monte_carlo_trade_bootstrap, run_cross_sectional_backtest,
8 run_param_sweep, run_walk_forward, run_walk_forward_optimize, single_param_variants,
9 BacktestConfig, BacktestEngine, BacktestError, BacktestReport, BacktestResult, CostModel,
10 CrossSectionalConfig, ExecutionDelay, MonteCarloConfig, MonteCarloPathSummary,
11 MonteCarloReturnConfig, MonteCarloSummary, StopConfig, SweepVariant, WalkForwardConfig,
12};
13
14pub trait QuantWaveBtExt {
16 fn bt(&self) -> BtNamespace<'_>;
17}
18
19pub struct BtNamespace<'a>(pub(crate) &'a LazyFrame);
21
22#[derive(Debug, Clone)]
24pub struct BtOptions {
25 pub signal_col: String,
26 pub timestamp_col: String,
27 pub close_col: String,
28 pub symbol_col: Option<String>,
29 pub entry_filter_col: Option<String>,
30 pub size_multiplier_col: Option<String>,
31 pub commission_bps: f64,
32 pub slippage_bps: f64,
33 pub initial_cash: f64,
34 pub execution_delay: ExecutionDelay,
35 pub stop_loss_pct: Option<f64>,
36 pub take_profit_pct: Option<f64>,
37 pub trailing_stop_pct: Option<f64>,
38}
39
40impl Default for BtOptions {
41 fn default() -> Self {
42 Self {
43 signal_col: "signal".to_string(),
44 timestamp_col: "timestamp".to_string(),
45 close_col: "close".to_string(),
46 symbol_col: None,
47 entry_filter_col: None,
48 size_multiplier_col: None,
49 commission_bps: 5.0,
50 slippage_bps: 2.0,
51 initial_cash: 100_000.0,
52 execution_delay: ExecutionDelay::SameBar,
53 stop_loss_pct: None,
54 take_profit_pct: None,
55 trailing_stop_pct: None,
56 }
57 }
58}
59
60impl BtOptions {
61 pub fn signal(signal_col: impl Into<String>) -> Self {
62 Self {
63 signal_col: signal_col.into(),
64 ..Default::default()
65 }
66 }
67
68 pub fn into_config(self) -> BacktestConfig {
69 let costs = CostModel {
70 commission_bps: self.commission_bps,
71 slippage_bps: self.slippage_bps,
72 initial_cash: self.initial_cash,
73 };
74 BacktestConfig {
75 cost_model: costs.clone(),
76 execution_model: quantwave_backtest::ExecutionModel::Simple(costs),
77 timestamp_col: self.timestamp_col,
78 symbol_col: self.symbol_col,
79 close_col: self.close_col,
80 signal_col: self.signal_col,
81 entry_filter_col: self.entry_filter_col,
82 size_multiplier_col: self.size_multiplier_col,
83 execution_delay: self.execution_delay,
84 stop_config: StopConfig {
85 stop_loss_pct: self.stop_loss_pct,
86 take_profit_pct: self.take_profit_pct,
87 trailing_stop_pct: self.trailing_stop_pct,
88 },
89 ..Default::default()
90 }
91 }
92}
93
94impl<'a> BtNamespace<'a> {
95 pub fn backtest(self, options: BtOptions) -> Result<BacktestResult, BacktestError> {
97 BacktestEngine::new(options.into_config()).run(self.0.clone())
98 }
99
100 pub fn backtest_with_report(self, options: BtOptions) -> Result<BacktestReport, BacktestError> {
102 BacktestEngine::new(options.into_config()).backtest_with_report(self.0.clone())
103 }
104
105 pub fn sweep(
107 self,
108 variants: &[SweepVariant],
109 options: BtOptions,
110 ) -> Result<DataFrame, BacktestError> {
111 run_param_sweep(self.0.clone(), variants, &options.into_config())
112 }
113
114 pub fn sweep_single_param(
116 self,
117 param_name: &str,
118 param_values: &[f64],
119 signal_cols: &[&str],
120 options: BtOptions,
121 ) -> Result<DataFrame, BacktestError> {
122 let variants = single_param_variants(param_name, param_values, signal_cols)?;
123 self.sweep(&variants, options)
124 }
125
126 pub fn walk_forward(
128 self,
129 wf: WalkForwardConfig,
130 options: BtOptions,
131 ) -> Result<DataFrame, BacktestError> {
132 run_walk_forward(self.0.clone(), &options.into_config(), &wf)
133 }
134
135 pub fn cross_sectional_backtest(
137 self,
138 cs: CrossSectionalConfig,
139 options: BtOptions,
140 ) -> Result<BacktestReport, BacktestError> {
141 run_cross_sectional_backtest(self.0.clone(), &cs, options.into_config())
142 }
143
144 pub fn walk_forward_optimize(
146 self,
147 wf: WalkForwardConfig,
148 variants: &[SweepVariant],
149 objective_metric: &str,
150 options: BtOptions,
151 ) -> Result<DataFrame, BacktestError> {
152 run_walk_forward_optimize(
153 self.0.clone(),
154 &options.into_config(),
155 &wf,
156 variants,
157 objective_metric,
158 )
159 }
160
161 pub fn monte_carlo_trade_bootstrap(
163 self,
164 options: BtOptions,
165 mc: MonteCarloConfig,
166 ) -> Result<MonteCarloSummary, BacktestError> {
167 let initial_cash = options.initial_cash;
168 let result = self.backtest(options)?;
169 monte_carlo_trade_bootstrap(&result, initial_cash, &mc)
170 }
171
172 pub fn monte_carlo_return_paths(
174 self,
175 options: BtOptions,
176 mc: MonteCarloReturnConfig,
177 ) -> Result<MonteCarloPathSummary, BacktestError> {
178 let result = self.backtest(options)?;
179 monte_carlo_return_paths(&result, &mc)
180 }
181}
182
183impl QuantWaveBtExt for LazyFrame {
184 fn bt(&self) -> BtNamespace<'_> {
185 BtNamespace(self)
186 }
187}