#[derive(Debug, Clone)]
pub struct DrawdownResult {
pub max_drawdown: f64,
pub recovery_periods: Option<u64>,
}
pub fn max_drawdown(returns: &[f64]) -> DrawdownResult {
if returns.is_empty() {
return DrawdownResult {
max_drawdown: 0.0,
recovery_periods: None,
};
}
let mut equity = Vec::with_capacity(returns.len() + 1);
equity.push(1.0_f64);
for r in returns {
let last = *equity.last().unwrap();
equity.push(last * (1.0 + r));
}
let mut peak = equity[0];
let mut peak_idx = 0_usize;
let mut max_dd = 0.0_f64;
let mut trough_idx = 0_usize;
for (i, &val) in equity.iter().enumerate() {
if val > peak {
peak = val;
peak_idx = i;
}
let dd = (peak - val) / peak;
if dd > max_dd {
max_dd = dd;
trough_idx = i;
}
}
let recovery_periods = if max_dd > 0.0 {
let peak_val = equity[peak_idx];
equity[trough_idx..]
.iter()
.enumerate()
.skip(1)
.find(|&(_, &v)| v >= peak_val)
.map(|(offset, _)| offset as u64)
} else {
None
};
DrawdownResult {
max_drawdown: max_dd,
recovery_periods,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
let r = max_drawdown(&[]);
assert_eq!(r.max_drawdown, 0.0);
assert!(r.recovery_periods.is_none());
}
#[test]
fn test_all_positive() {
let returns = vec![0.01_f64; 100];
let r = max_drawdown(&returns);
assert_eq!(r.max_drawdown, 0.0);
}
#[test]
fn test_single_drop_recovery() {
let returns = vec![0.10_f64, -0.20, 0.15];
let r = max_drawdown(&returns);
assert!(
(r.max_drawdown - 0.20).abs() < 0.01,
"got {}",
r.max_drawdown
);
}
#[test]
fn test_no_recovery() {
let returns = vec![-0.05_f64; 10];
let r = max_drawdown(&returns);
assert!(r.max_drawdown > 0.0);
assert!(r.recovery_periods.is_none());
}
}