1use polars::prelude::*;
6use quantwave_backtest::{
7 BacktestConfig, BacktestEngine, BacktestError, BacktestReport, BacktestResult, CostModel,
8 CrossSectionalConfig, ExecutionDelay, MonteCarloConfig, MonteCarloPathSummary,
9 MonteCarloReturnConfig, MonteCarloSummary, StopConfig, SweepVariant, WalkForwardConfig,
10 monte_carlo_return_paths, monte_carlo_trade_bootstrap, run_cross_sectional_backtest,
11 run_param_sweep, run_walk_forward, run_walk_forward_optimize, single_param_variants,
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 ..Default::default()
89 },
90 ..Default::default()
91 }
92 }
93}
94
95impl<'a> BtNamespace<'a> {
96 pub fn backtest(self, options: BtOptions) -> Result<BacktestResult, BacktestError> {
98 BacktestEngine::new(options.into_config()).run(self.0.clone())
99 }
100
101 pub fn backtest_with_report(self, options: BtOptions) -> Result<BacktestReport, BacktestError> {
103 BacktestEngine::new(options.into_config()).backtest_with_report(self.0.clone())
104 }
105
106 pub fn sweep(
108 self,
109 variants: &[SweepVariant],
110 options: BtOptions,
111 ) -> Result<DataFrame, BacktestError> {
112 run_param_sweep(self.0.clone(), variants, &options.into_config())
113 }
114
115 pub fn sweep_single_param(
117 self,
118 param_name: &str,
119 param_values: &[f64],
120 signal_cols: &[&str],
121 options: BtOptions,
122 ) -> Result<DataFrame, BacktestError> {
123 let variants = single_param_variants(param_name, param_values, signal_cols)?;
124 self.sweep(&variants, options)
125 }
126
127 pub fn walk_forward(
129 self,
130 wf: WalkForwardConfig,
131 options: BtOptions,
132 ) -> Result<DataFrame, BacktestError> {
133 run_walk_forward(self.0.clone(), &options.into_config(), &wf)
134 }
135
136 pub fn cross_sectional_backtest(
138 self,
139 cs: CrossSectionalConfig,
140 options: BtOptions,
141 ) -> Result<BacktestReport, BacktestError> {
142 run_cross_sectional_backtest(self.0.clone(), &cs, options.into_config())
143 }
144
145 pub fn walk_forward_optimize(
147 self,
148 wf: WalkForwardConfig,
149 variants: &[SweepVariant],
150 objective_metric: &str,
151 options: BtOptions,
152 ) -> Result<DataFrame, BacktestError> {
153 run_walk_forward_optimize(
154 self.0.clone(),
155 &options.into_config(),
156 &wf,
157 variants,
158 objective_metric,
159 )
160 }
161
162 pub fn monte_carlo_trade_bootstrap(
164 self,
165 options: BtOptions,
166 mc: MonteCarloConfig,
167 ) -> Result<MonteCarloSummary, BacktestError> {
168 let initial_cash = options.initial_cash;
169 let result = self.backtest(options)?;
170 monte_carlo_trade_bootstrap(&result, initial_cash, &mc)
171 }
172
173 pub fn monte_carlo_return_paths(
175 self,
176 options: BtOptions,
177 mc: MonteCarloReturnConfig,
178 ) -> Result<MonteCarloPathSummary, BacktestError> {
179 let result = self.backtest(options)?;
180 monte_carlo_return_paths(&result, &mc)
181 }
182}
183
184impl QuantWaveBtExt for LazyFrame {
185 fn bt(&self) -> BtNamespace<'_> {
186 BtNamespace(self)
187 }
188}