fin_primitives/performance/
mod.rs1#[derive(Debug, Clone, PartialEq)]
9pub struct PerformanceMetrics {
10 pub sharpe_ratio: f64,
12 pub sortino_ratio: f64,
14 pub calmar_ratio: f64,
16 pub omega_ratio: f64,
18 pub information_ratio: f64,
20 pub max_drawdown: f64,
22 pub cagr: f64,
24}
25
26pub struct PerformanceCalculator;
28
29impl PerformanceCalculator {
30 pub fn sharpe_ratio(returns: &[f64], risk_free_rate: f64) -> f64 {
36 if returns.len() < 2 {
37 return 0.0;
38 }
39 let mean = mean(returns);
40 let std = std_dev(returns);
41 if std == 0.0 {
42 return 0.0;
43 }
44 (mean - risk_free_rate) / std * 252_f64.sqrt()
45 }
46
47 pub fn sortino_ratio(returns: &[f64], risk_free_rate: f64, target: f64) -> f64 {
53 if returns.is_empty() {
54 return 0.0;
55 }
56 let mean = mean(returns);
57 let downside_var: f64 = returns
58 .iter()
59 .map(|&r| {
60 let diff = r - target;
61 if diff < 0.0 { diff * diff } else { 0.0 }
62 })
63 .sum::<f64>()
64 / returns.len() as f64;
65 let downside_dev = downside_var.sqrt();
66 if downside_dev == 0.0 {
67 return 0.0;
68 }
69 (mean - risk_free_rate) / downside_dev * 252_f64.sqrt()
70 }
71
72 pub fn calmar_ratio(returns: &[f64]) -> f64 {
77 if returns.is_empty() {
78 return 0.0;
79 }
80 let ann_return = Self::cagr(returns, 252.0);
81 let md = Self::max_drawdown(returns);
82 if md == 0.0 {
83 return 0.0;
84 }
85 ann_return / md.abs()
86 }
87
88 pub fn omega_ratio(returns: &[f64], threshold: f64) -> f64 {
95 let gains: f64 = returns.iter().map(|&r| (r - threshold).max(0.0)).sum();
96 let losses: f64 = returns.iter().map(|&r| (threshold - r).max(0.0)).sum();
97 if losses == 0.0 {
98 if gains > 0.0 { f64::INFINITY } else { 0.0 }
99 } else {
100 gains / losses
101 }
102 }
103
104 pub fn information_ratio(returns: &[f64], benchmark_returns: &[f64]) -> f64 {
112 if returns.len() != benchmark_returns.len() || returns.len() < 2 {
113 return 0.0;
114 }
115 let active: Vec<f64> = returns
116 .iter()
117 .zip(benchmark_returns.iter())
118 .map(|(&r, &b)| r - b)
119 .collect();
120 let mean_active = mean(&active);
121 let te = std_dev(&active);
122 if te == 0.0 {
123 return 0.0;
124 }
125 mean_active / te
126 }
127
128 pub fn max_drawdown(returns: &[f64]) -> f64 {
134 if returns.is_empty() {
135 return 0.0;
136 }
137 let mut peak = 1.0_f64;
138 let mut cum = 1.0_f64;
139 let mut max_dd = 0.0_f64;
140 for &r in returns {
141 cum *= 1.0 + r;
142 if cum > peak {
143 peak = cum;
144 }
145 let dd = (peak - cum) / peak;
146 if dd > max_dd {
147 max_dd = dd;
148 }
149 }
150 max_dd
151 }
152
153 pub fn cagr(returns: &[f64], periods_per_year: f64) -> f64 {
159 if returns.is_empty() {
160 return 0.0;
161 }
162 let n = returns.len() as f64;
163 let total: f64 = returns.iter().fold(1.0, |acc, &r| acc * (1.0 + r));
164 total.powf(periods_per_year / n) - 1.0
165 }
166
167 pub fn compute_all(
173 returns: &[f64],
174 benchmark: Option<&[f64]>,
175 risk_free_rate: f64,
176 ) -> PerformanceMetrics {
177 let sharpe_ratio = Self::sharpe_ratio(returns, risk_free_rate);
178 let sortino_ratio = Self::sortino_ratio(returns, risk_free_rate, risk_free_rate);
179 let calmar_ratio = Self::calmar_ratio(returns);
180 let omega_ratio = Self::omega_ratio(returns, risk_free_rate);
181 let information_ratio = benchmark
182 .map(|b| Self::information_ratio(returns, b))
183 .unwrap_or(0.0);
184 let max_drawdown = Self::max_drawdown(returns);
185 let cagr = Self::cagr(returns, 252.0);
186
187 PerformanceMetrics {
188 sharpe_ratio,
189 sortino_ratio,
190 calmar_ratio,
191 omega_ratio,
192 information_ratio,
193 max_drawdown,
194 cagr,
195 }
196 }
197}
198
199fn mean(xs: &[f64]) -> f64 {
204 if xs.is_empty() {
205 return 0.0;
206 }
207 xs.iter().sum::<f64>() / xs.len() as f64
208}
209
210fn std_dev(xs: &[f64]) -> f64 {
212 if xs.len() < 2 {
213 return 0.0;
214 }
215 let m = mean(xs);
216 let var = xs.iter().map(|&x| (x - m).powi(2)).sum::<f64>() / (xs.len() - 1) as f64;
217 var.sqrt()
218}
219
220#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn sample_returns() -> Vec<f64> {
230 vec![0.01, -0.005, 0.02, -0.01, 0.015]
231 }
232
233 #[test]
234 fn test_sharpe_positive_returns() {
235 let r = sample_returns();
236 let s = PerformanceCalculator::sharpe_ratio(&r, 0.0);
237 assert!(s > 0.0, "Sharpe should be positive for net-positive returns");
239 }
240
241 #[test]
242 fn test_sharpe_empty() {
243 assert_eq!(PerformanceCalculator::sharpe_ratio(&[], 0.0), 0.0);
244 }
245
246 #[test]
247 fn test_sharpe_single_element() {
248 assert_eq!(PerformanceCalculator::sharpe_ratio(&[0.01], 0.0), 0.0);
249 }
250
251 #[test]
252 fn test_sharpe_zero_std() {
253 let r = vec![0.01, 0.01, 0.01];
255 assert_eq!(PerformanceCalculator::sharpe_ratio(&r, 0.0), 0.0);
256 }
257
258 #[test]
259 fn test_sortino_positive() {
260 let r = sample_returns();
261 let s = PerformanceCalculator::sortino_ratio(&r, 0.0, 0.0);
262 assert!(s > 0.0);
263 }
264
265 #[test]
266 fn test_sortino_empty() {
267 assert_eq!(PerformanceCalculator::sortino_ratio(&[], 0.0, 0.0), 0.0);
268 }
269
270 #[test]
271 fn test_sortino_no_downside() {
272 let r = vec![0.01, 0.02, 0.03];
274 assert_eq!(PerformanceCalculator::sortino_ratio(&r, 0.0, 0.0), 0.0);
275 }
276
277 #[test]
278 fn test_max_drawdown_known() {
279 let r = vec![0.10, -0.045_454, 0.142_857, -0.20, 0.25];
282 let md = PerformanceCalculator::max_drawdown(&r);
283 assert!(md > 0.0 && md < 1.0, "Max drawdown should be (0, 1)");
284 }
285
286 #[test]
287 fn test_max_drawdown_monotone_up() {
288 let r = vec![0.01, 0.02, 0.03];
289 assert_eq!(PerformanceCalculator::max_drawdown(&r), 0.0);
290 }
291
292 #[test]
293 fn test_max_drawdown_empty() {
294 assert_eq!(PerformanceCalculator::max_drawdown(&[]), 0.0);
295 }
296
297 #[test]
298 fn test_cagr_flat() {
299 let r = vec![0.0; 252];
301 let cagr = PerformanceCalculator::cagr(&r, 252.0);
302 assert!((cagr).abs() < 1e-10);
303 }
304
305 #[test]
306 fn test_cagr_known() {
307 let r = vec![0.001; 252];
309 let cagr = PerformanceCalculator::cagr(&r, 252.0);
310 assert!(cagr > 0.0);
311 let expected = 1.001_f64.powi(252) - 1.0;
313 assert!((cagr - expected).abs() < 1e-8);
314 }
315
316 #[test]
317 fn test_omega_all_above_threshold() {
318 let r = vec![0.01, 0.02, 0.03];
319 let o = PerformanceCalculator::omega_ratio(&r, 0.0);
320 assert!(o.is_infinite(), "Omega should be +Inf when no losses");
321 }
322
323 #[test]
324 fn test_omega_all_below_threshold() {
325 let r = vec![-0.01, -0.02, -0.03];
326 let o = PerformanceCalculator::omega_ratio(&r, 0.0);
327 assert_eq!(o, 0.0);
328 }
329
330 #[test]
331 fn test_omega_mixed() {
332 let r = vec![0.02, -0.01];
334 let o = PerformanceCalculator::omega_ratio(&r, 0.0);
335 assert!((o - 2.0).abs() < 1e-10);
336 }
337
338 #[test]
339 fn test_information_ratio_length_mismatch() {
340 let r = vec![0.01, 0.02];
341 let b = vec![0.01];
342 assert_eq!(PerformanceCalculator::information_ratio(&r, &b), 0.0);
343 }
344
345 #[test]
346 fn test_information_ratio_identical() {
347 let r = vec![0.01, 0.02, 0.03];
348 let b = r.clone();
349 assert_eq!(PerformanceCalculator::information_ratio(&r, &b), 0.0);
351 }
352
353 #[test]
354 fn test_information_ratio_positive() {
355 let r = vec![0.02, 0.03, 0.04];
356 let b = vec![0.01, 0.01, 0.01];
357 let ir = PerformanceCalculator::information_ratio(&r, &b);
358 assert!(ir > 0.0);
359 }
360
361 #[test]
362 fn test_calmar_empty() {
363 assert_eq!(PerformanceCalculator::calmar_ratio(&[]), 0.0);
364 }
365
366 #[test]
367 fn test_calmar_no_drawdown() {
368 let r = vec![0.01; 252];
370 assert_eq!(PerformanceCalculator::calmar_ratio(&r), 0.0);
371 }
372
373 #[test]
374 fn test_compute_all_returns_struct() {
375 let r = sample_returns();
376 let bench = vec![0.005, 0.005, 0.005, 0.005, 0.005];
377 let m = PerformanceCalculator::compute_all(&r, Some(&bench), 0.0001);
378 assert!(!m.sharpe_ratio.is_nan());
380 assert!(!m.sortino_ratio.is_nan());
381 assert!(!m.calmar_ratio.is_nan());
382 assert!(!m.omega_ratio.is_nan());
383 assert!(!m.information_ratio.is_nan());
384 assert!(!m.max_drawdown.is_nan());
385 assert!(!m.cagr.is_nan());
386 }
387
388 #[test]
389 fn test_compute_all_no_benchmark() {
390 let r = sample_returns();
391 let m = PerformanceCalculator::compute_all(&r, None, 0.0);
392 assert_eq!(m.information_ratio, 0.0);
393 }
394}