finance_query/backtesting/result/
rolling.rs1use super::BacktestResult;
2use super::stats::{calculate_periodic_returns, calculate_risk_ratios};
3
4impl BacktestResult {
5 pub fn rolling_sharpe(&self, window: usize) -> Vec<f64> {
25 if window == 0 {
26 return vec![];
27 }
28 let returns = calculate_periodic_returns(&self.equity_curve);
29 if returns.len() < window {
30 return vec![];
31 }
32 let rf = self.config.risk_free_rate;
33 let bpy = self.config.bars_per_year;
34 returns
35 .windows(window)
36 .map(|w| {
37 let (sharpe, _) = calculate_risk_ratios(w, rf, bpy);
38 sharpe
39 })
40 .collect()
41 }
42
43 pub fn drawdown_series(&self) -> Vec<f64> {
60 self.equity_curve.iter().map(|p| p.drawdown_pct).collect()
61 }
62
63 pub fn rolling_win_rate(&self, window: usize) -> Vec<f64> {
77 if window == 0 || self.trades.len() < window {
78 return vec![];
79 }
80 self.trades
81 .windows(window)
82 .map(|w| {
83 let wins = w.iter().filter(|t| t.is_profitable()).count();
84 wins as f64 / window as f64
85 })
86 .collect()
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::super::EquityPoint;
93 use super::super::fixtures::{equity_point, make_result, make_trade};
94 use crate::backtesting::position::Trade;
95
96 #[test]
101 fn rolling_sharpe_window_zero_returns_empty() {
102 let result = make_result(
103 vec![],
104 vec![equity_point(0, 10000.0, 0.0), equity_point(1, 10100.0, 0.0)],
105 );
106 assert!(result.rolling_sharpe(0).is_empty());
107 }
108
109 #[test]
110 fn rolling_sharpe_insufficient_bars_returns_empty() {
111 let result = make_result(
113 vec![],
114 vec![
115 equity_point(0, 10000.0, 0.0),
116 equity_point(1, 10100.0, 0.0),
117 equity_point(2, 10200.0, 0.0),
118 ],
119 );
120 assert!(result.rolling_sharpe(3).is_empty());
121 }
122
123 #[test]
124 fn rolling_sharpe_correct_length() {
125 let pts: Vec<EquityPoint> = (0..5)
127 .map(|i| equity_point(i, 10000.0 + i as f64 * 100.0, 0.0))
128 .collect();
129 let result = make_result(vec![], pts);
130 assert_eq!(result.rolling_sharpe(2).len(), 3);
131 }
132
133 #[test]
134 fn rolling_sharpe_monotone_increase_positive() {
135 let pts: Vec<EquityPoint> = (0..10)
137 .map(|i| equity_point(i, 10000.0 + i as f64 * 100.0, 0.0))
138 .collect();
139 let result = make_result(vec![], pts);
140 let sharpes = result.rolling_sharpe(3);
141 assert!(!sharpes.is_empty());
142 for s in &sharpes {
143 assert!(
144 *s > 0.0 || *s == f64::MAX,
145 "expected positive Sharpe, got {s}"
146 );
147 }
148 }
149
150 #[test]
153 fn drawdown_series_mirrors_equity_curve() {
154 let pts = vec![
155 equity_point(0, 10000.0, 0.00),
156 equity_point(1, 9500.0, 0.05),
157 equity_point(2, 9000.0, 0.10),
158 equity_point(3, 9200.0, 0.08),
159 equity_point(4, 10000.0, 0.00),
160 ];
161 let result = make_result(vec![], pts.clone());
162 let dd = result.drawdown_series();
163 assert_eq!(dd.len(), pts.len());
164 for (got, ep) in dd.iter().zip(pts.iter()) {
165 assert!(
166 (got - ep.drawdown_pct).abs() < f64::EPSILON,
167 "expected {}, got {}",
168 ep.drawdown_pct,
169 got
170 );
171 }
172 }
173
174 #[test]
175 fn drawdown_series_empty_curve() {
176 let result = make_result(vec![], vec![]);
177 assert!(result.drawdown_series().is_empty());
178 }
179
180 #[test]
183 fn rolling_win_rate_window_zero_returns_empty() {
184 let result = make_result(vec![make_trade(50.0, 5.0, true)], vec![]);
185 assert!(result.rolling_win_rate(0).is_empty());
186 }
187
188 #[test]
189 fn rolling_win_rate_window_exceeds_trades_returns_empty() {
190 let result = make_result(vec![make_trade(50.0, 5.0, true)], vec![]);
191 assert!(result.rolling_win_rate(2).is_empty());
192 }
193
194 #[test]
195 fn rolling_win_rate_all_wins() {
196 let trades = vec![
197 make_trade(10.0, 1.0, true),
198 make_trade(20.0, 2.0, true),
199 make_trade(15.0, 1.5, true),
200 ];
201 let result = make_result(trades, vec![]);
202 let wr = result.rolling_win_rate(2);
203 assert_eq!(wr, vec![1.0, 1.0]);
205 }
206
207 #[test]
208 fn rolling_win_rate_alternating() {
209 let trades = vec![
211 make_trade(10.0, 1.0, true),
212 make_trade(-10.0, -1.0, true),
213 make_trade(10.0, 1.0, true),
214 make_trade(-10.0, -1.0, true),
215 ];
216 let result = make_result(trades, vec![]);
217 let wr = result.rolling_win_rate(2);
218 assert_eq!(wr.len(), 3);
219 for v in &wr {
220 assert!((v - 0.5).abs() < f64::EPSILON, "expected 0.5, got {v}");
221 }
222 }
223
224 #[test]
225 fn rolling_win_rate_correct_length() {
226 let trades: Vec<Trade> = (0..5)
227 .map(|i| make_trade(i as f64, i as f64, true))
228 .collect();
229 let result = make_result(trades, vec![]);
230 assert_eq!(result.rolling_win_rate(3).len(), 3);
232 }
233
234 #[test]
235 fn rolling_win_rate_window_equals_trade_count_returns_one_element() {
236 let trades = vec![
238 make_trade(10.0, 1.0, true),
239 make_trade(-5.0, -0.5, true),
240 make_trade(8.0, 0.8, true),
241 ];
242 let result = make_result(trades, vec![]);
243 let wr = result.rolling_win_rate(3);
244 assert_eq!(wr.len(), 1);
245 assert!((wr[0] - 2.0 / 3.0).abs() < f64::EPSILON);
247 }
248}