use crate::deflated_sharpe::sharpe_ratio;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RollingSharpe {
pub min_sharpe: f64,
pub frac_positive: f64,
}
pub fn rolling_sharpe(returns: &[f64], window: usize) -> Option<RollingSharpe> {
if window < 2 || returns.len() < window {
return None;
}
let n_windows = returns.len() - window + 1;
let mut min_sharpe = f64::INFINITY;
let mut positive = 0usize;
for start in 0..n_windows {
let s = sharpe_ratio(&returns[start..start + window]);
if s < min_sharpe {
min_sharpe = s;
}
if s > 0.0 {
positive += 1;
}
}
Some(RollingSharpe {
min_sharpe,
frac_positive: positive as f64 / n_windows as f64,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn too_short_returns_none() {
assert!(rolling_sharpe(&[0.1, 0.2], 5).is_none());
assert!(rolling_sharpe(&[0.1, 0.2, 0.3], 1).is_none());
}
#[test]
fn steady_edge_is_all_positive() {
let r: Vec<f64> = (0..60)
.map(|i| 0.002 + 0.0005 * (i as f64 * 0.7).sin())
.collect();
let rs = rolling_sharpe(&r, 21).expect("long enough");
assert!(
(rs.frac_positive - 1.0).abs() < 1e-12,
"frac_positive={}",
rs.frac_positive
);
assert!(rs.min_sharpe > 0.0, "worst window should be positive");
}
#[test]
fn one_lucky_window_drags_min_and_frac() {
let mut r = vec![0.05; 21]; r.extend(vec![-0.001; 60]); let rs = rolling_sharpe(&r, 21).expect("long enough");
assert!(rs.min_sharpe < 0.0, "min_sharpe={}", rs.min_sharpe);
assert!(rs.frac_positive < 0.5, "frac_positive={}", rs.frac_positive);
}
}