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    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
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                ..Default::default()
89            },
90            ..Default::default()
91        }
92    }
93}
94
95impl<'a> BtNamespace<'a> {
96    /// Run vectorized backtest on this LazyFrame (collected internally).
97    pub fn backtest(self, options: BtOptions) -> Result<BacktestResult, BacktestError> {
98        BacktestEngine::new(options.into_config()).run(self.0.clone())
99    }
100
101    /// Run backtest and attach [`BacktestReport`] metrics.
102    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    /// Parameter sweep: one backtest per variant → param × metrics DataFrame (cr6v.12).
107    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    /// Convenience: single-param grid via parallel `param_values` and `signal_cols` slices.
116    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    /// Walk-forward OOS validation → fold × metrics DataFrame (cr6v.14).
128    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    /// Cross-sectional factor rank → long/short backtest report (cr6v.15).
137    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    /// Walk-forward with in-fold parameter optimization (quantwave-dk61).
146    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    /// Trade PnL bootstrap Monte Carlo after backtest (quantwave-fsg3 / dk61).
163    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    /// Return-path resampling Monte Carlo (VaR/CVaR) after backtest.
174    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}