rustyqlib/risk/
performance.rs1pub fn max_drawdown(values: &[f64]) -> (f64, usize, usize) {
8 assert!(!values.is_empty());
9 let mut peak = values[0];
10 let mut peak_idx = 0;
11 let mut best = 0.0;
12 let mut best_pair = (0, 0);
13 for (i, &v) in values.iter().enumerate() {
14 if v > peak {
15 peak = v;
16 peak_idx = i;
17 }
18 let dd = (peak - v) / peak;
19 if dd > best {
20 best = dd;
21 best_pair = (peak_idx, i);
22 }
23 }
24 (best, best_pair.0, best_pair.1)
25}
26
27pub fn sharpe_ratio(returns: &[f64], risk_free_per_period: f64, periods_per_year: f64) -> f64 {
30 let n = returns.len();
31 assert!(n >= 2);
32 let excess: Vec<f64> = returns.iter().map(|r| r - risk_free_per_period).collect();
33 let mean = excess.iter().sum::<f64>() / n as f64;
34 let var = excess.iter().map(|e| (e - mean) * (e - mean)).sum::<f64>() / (n as f64 - 1.0);
35 mean / var.sqrt() * periods_per_year.sqrt()
36}
37
38pub fn sortino_ratio(returns: &[f64], risk_free_per_period: f64, periods_per_year: f64) -> f64 {
41 let n = returns.len();
42 assert!(n >= 2);
43 let mean_excess =
44 returns.iter().map(|r| r - risk_free_per_period).sum::<f64>() / n as f64;
45 let downside_sq = returns
46 .iter()
47 .map(|r| (r - risk_free_per_period).min(0.0).powi(2))
48 .sum::<f64>()
49 / n as f64;
50 assert!(downside_sq > 0.0, "no downside observations");
51 mean_excess / downside_sq.sqrt() * periods_per_year.sqrt()
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn drawdown_finds_the_peak_to_trough() {
60 let nav = [100.0, 110.0, 105.0, 120.0, 90.0, 95.0, 130.0];
61 let (dd, peak, trough) = max_drawdown(&nav);
62 assert!((dd - 0.25).abs() < 1e-12, "{dd}"); assert_eq!((peak, trough), (3, 4));
64 assert_eq!(max_drawdown(&[1.0, 2.0, 3.0]).0, 0.0);
66 }
67
68 #[test]
69 fn ratios_are_hand_checkable_and_ordered() {
70 let returns = [0.02, -0.01, 0.03, -0.005, 0.015, -0.02, 0.025, 0.01];
73 let sharpe = sharpe_ratio(&returns, 0.0, 252.0);
74 let sortino = sortino_ratio(&returns, 0.0, 252.0);
75 assert!(sharpe > 0.0 && sortino > sharpe, "{sharpe} vs {sortino}");
76 let scaled: Vec<f64> = returns.iter().map(|r| r * 3.0).collect();
78 assert!((sharpe_ratio(&scaled, 0.0, 252.0) - sharpe).abs() < 1e-12);
79 }
80}