Skip to main content

quantwave_polars/
bt.rs

1//! Polars `.bt` backtest namespace (quantwave-cr6v.5).
2//!
3//! Usage: `df.lazy().bt().backtest(BtOptions { signal_col: "exposure".into(), .. })`
4
5use 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
14/// Extension trait: `LazyFrame::bt()`.
15pub trait QuantWaveBtExt {
16    fn bt(&self) -> BtNamespace<'_>;
17}
18
19/// Namespace handle returned by [`QuantWaveBtExt::bt`].
20pub struct BtNamespace<'a>(pub(crate) &'a LazyFrame);
21
22/// Column names + cost knobs for a vectorized backtest run.
23#[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    /// Run vectorized backtest on this LazyFrame (collected internally).
96    pub fn backtest(self, options: BtOptions) -> Result<BacktestResult, BacktestError> {
97        BacktestEngine::new(options.into_config()).run(self.0.clone())
98    }
99
100    /// Run backtest and attach [`BacktestReport`] metrics.
101    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    /// Parameter sweep: one backtest per variant → param × metrics DataFrame (cr6v.12).
106    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    /// Convenience: single-param grid via parallel `param_values` and `signal_cols` slices.
115    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    /// Walk-forward OOS validation → fold × metrics DataFrame (cr6v.14).
127    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    /// Cross-sectional factor rank → long/short backtest report (cr6v.15).
136    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    /// Walk-forward with in-fold parameter optimization (quantwave-dk61).
145    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    /// Trade PnL bootstrap Monte Carlo after backtest (quantwave-fsg3 / dk61).
162    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    /// Return-path resampling Monte Carlo (VaR/CVaR) after backtest.
173    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}