1use crate::stocks::returns::{mean_return, simple_returns};
24use crate::util::error::{require_finite, FinanceError, FinanceResult};
25
26pub fn volatility(returns: &[f64]) -> FinanceResult<f64> {
39 if returns.len() < 2 {
40 return Err(FinanceError::Unsolvable {
41 message: "volatility requires at least two returns",
42 });
43 }
44 let mean = mean_return(returns)?;
45 let mut sum_sq = 0.0;
46 for r in returns {
47 require_finite("returns", *r)?;
48 let d = r - mean;
49 sum_sq += d * d;
50 }
51 Ok((sum_sq / (returns.len() - 1) as f64).sqrt())
52}
53
54pub fn volatility_annualized(returns: &[f64], periods_per_year: f64) -> FinanceResult<f64> {
67 require_finite("periods_per_year", periods_per_year)?;
68 if periods_per_year <= 0.0 {
69 return Err(FinanceError::Unsolvable {
70 message: "periods_per_year must be positive",
71 });
72 }
73 Ok(volatility(returns)? * periods_per_year.sqrt())
74}
75
76pub fn sharpe_ratio(returns: &[f64], risk_free_rate: f64) -> FinanceResult<f64> {
90 require_finite("risk_free_rate", risk_free_rate)?;
91 let vol = volatility(returns)?;
92 if vol == 0.0 {
93 return Err(FinanceError::Unsolvable {
94 message: "sharpe_ratio undefined when volatility is zero",
95 });
96 }
97 let mean = mean_return(returns)?;
98 Ok((mean - risk_free_rate) / vol)
99}
100
101pub fn sortino_ratio(returns: &[f64], target: f64) -> FinanceResult<f64> {
115 require_finite("target", target)?;
116 if returns.len() < 2 {
117 return Err(FinanceError::Unsolvable {
118 message: "sortino_ratio requires at least two returns",
119 });
120 }
121 for r in returns {
122 require_finite("returns", *r)?;
123 }
124 let mut sum_sq = 0.0;
125 let mut downside_count = 0usize;
126 for &r in returns {
127 let shortfall = r - target;
128 if shortfall < 0.0 {
129 sum_sq += shortfall * shortfall;
130 downside_count += 1;
131 }
132 }
133 if downside_count == 0 {
134 return Err(FinanceError::Unsolvable {
135 message: "sortino_ratio undefined when no returns fall below target",
136 });
137 }
138 let dd = (sum_sq / (returns.len() - 1) as f64).sqrt();
140 if dd == 0.0 {
141 return Err(FinanceError::Unsolvable {
142 message: "sortino_ratio undefined when downside deviation is zero",
143 });
144 }
145 let mean = mean_return(returns)?;
146 Ok((mean - target) / dd)
147}
148
149pub fn max_drawdown(prices: &[f64]) -> FinanceResult<f64> {
163 if prices.len() < 2 {
164 return Err(FinanceError::Unsolvable {
165 message: "max_drawdown requires at least two prices",
166 });
167 }
168 let series = drawdown_series(prices)?;
169 Ok(series.into_iter().fold(0.0_f64, f64::max))
170}
171
172pub fn drawdown_series(prices: &[f64]) -> FinanceResult<Vec<f64>> {
185 if prices.is_empty() {
186 return Err(FinanceError::Unsolvable {
187 message: "drawdown_series requires at least one price",
188 });
189 }
190 let mut peak = prices[0];
191 require_finite("prices", peak)?;
192 if peak <= 0.0 {
193 return Err(FinanceError::InvalidCashflow {
194 message: "drawdown_series requires positive prices",
195 });
196 }
197 let mut out = Vec::with_capacity(prices.len());
198 for &p in prices {
199 require_finite("prices", p)?;
200 if p <= 0.0 {
201 return Err(FinanceError::InvalidCashflow {
202 message: "drawdown_series requires positive prices",
203 });
204 }
205 if p > peak {
206 peak = p;
207 }
208 out.push((peak - p) / peak);
209 }
210 Ok(out)
211}
212
213pub fn rolling_max_drawdown(prices: &[f64]) -> FinanceResult<Vec<f64>> {
227 let dd = drawdown_series(prices)?;
228 let mut out = Vec::with_capacity(dd.len());
229 let mut running = 0.0_f64;
230 for d in dd {
231 running = running.max(d);
232 out.push(running);
233 }
234 Ok(out)
235}
236
237pub fn beta(asset_returns: &[f64], market_returns: &[f64]) -> FinanceResult<f64> {
258 if asset_returns.len() != market_returns.len() {
259 return Err(FinanceError::Unsolvable {
260 message: "beta requires asset and market return series of equal length",
261 });
262 }
263 if asset_returns.len() < 2 {
264 return Err(FinanceError::Unsolvable {
265 message: "beta requires at least two paired returns",
266 });
267 }
268 for r in asset_returns.iter().chain(market_returns.iter()) {
269 require_finite("returns", *r)?;
270 }
271 let mean_a = mean_return(asset_returns)?;
272 let mean_m = mean_return(market_returns)?;
273 let n = asset_returns.len() as f64;
274 let mut cov = 0.0;
275 let mut var_m = 0.0;
276 for i in 0..asset_returns.len() {
277 let da = asset_returns[i] - mean_a;
278 let dm = market_returns[i] - mean_m;
279 cov += da * dm;
280 var_m += dm * dm;
281 }
282 cov /= n - 1.0;
283 var_m /= n - 1.0;
284 if var_m == 0.0 {
285 return Err(FinanceError::Unsolvable {
286 message: "beta undefined when market variance is zero",
287 });
288 }
289 Ok(cov / var_m)
290}
291
292pub fn price_volatility(prices: &[f64]) -> FinanceResult<f64> {
306 let rets = simple_returns(prices)?;
307 volatility(&rets)
308}
309
310pub fn cagr_from_prices(prices: &[f64], years: f64) -> FinanceResult<f64> {
320 require_finite("years", years)?;
321 if years <= 0.0 {
322 return Err(FinanceError::Unsolvable {
323 message: "years must be positive",
324 });
325 }
326 if prices.len() < 2 {
327 return Err(FinanceError::Unsolvable {
328 message: "cagr_from_prices requires at least two prices",
329 });
330 }
331 let start = prices[0];
332 let end = *prices.last().unwrap();
333 require_finite("prices", start)?;
334 require_finite("prices", end)?;
335 if start <= 0.0 || end <= 0.0 {
336 return Err(FinanceError::Unsolvable {
337 message: "cagr_from_prices requires strictly positive prices",
338 });
339 }
340 Ok((end / start).powf(1.0 / years) - 1.0)
341}
342
343pub fn cagr_from_prices_periods(prices: &[f64], periods_per_year: f64) -> FinanceResult<f64> {
345 require_finite("periods_per_year", periods_per_year)?;
346 if periods_per_year <= 0.0 {
347 return Err(FinanceError::Unsolvable {
348 message: "periods_per_year must be positive",
349 });
350 }
351 if prices.len() < 2 {
352 return Err(FinanceError::Unsolvable {
353 message: "cagr_from_prices_periods requires at least two prices",
354 });
355 }
356 let years = (prices.len() - 1) as f64 / periods_per_year;
357 cagr_from_prices(prices, years)
358}
359
360pub fn calmar_ratio(prices: &[f64], years: f64) -> FinanceResult<f64> {
375 let cagr = cagr_from_prices(prices, years)?;
376 let dd = max_drawdown(prices)?.abs();
377 if dd == 0.0 {
378 return Err(FinanceError::Unsolvable {
379 message: "calmar_ratio undefined when max drawdown is zero",
380 });
381 }
382 Ok(cagr / dd)
383}
384
385pub fn calmar_ratio_periods(prices: &[f64], periods_per_year: f64) -> FinanceResult<f64> {
387 let cagr = cagr_from_prices_periods(prices, periods_per_year)?;
388 let dd = max_drawdown(prices)?.abs();
389 if dd == 0.0 {
390 return Err(FinanceError::Unsolvable {
391 message: "calmar_ratio undefined when max drawdown is zero",
392 });
393 }
394 Ok(cagr / dd)
395}
396
397pub fn ulcer_index(prices: &[f64]) -> FinanceResult<f64> {
402 let dd = drawdown_series(prices)?;
403 if dd.is_empty() {
404 return Err(FinanceError::Unsolvable {
405 message: "ulcer_index requires prices",
406 });
407 }
408 let mut sum = 0.0;
409 for d in &dd {
410 let pct = d * 100.0;
412 sum += pct * pct;
413 }
414 Ok((sum / dd.len() as f64).sqrt())
415}
416
417pub fn correlation(x: &[f64], y: &[f64]) -> FinanceResult<f64> {
431 if x.len() != y.len() {
432 return Err(FinanceError::Unsolvable {
433 message: "correlation requires equal-length series",
434 });
435 }
436 if x.len() < 2 {
437 return Err(FinanceError::Unsolvable {
438 message: "correlation requires at least two pairs",
439 });
440 }
441 for (a, b) in x.iter().zip(y.iter()) {
442 require_finite("x", *a)?;
443 require_finite("y", *b)?;
444 }
445 let n = x.len() as f64;
446 let mean_x = x.iter().sum::<f64>() / n;
447 let mean_y = y.iter().sum::<f64>() / n;
448 let mut cov = 0.0;
449 let mut vx = 0.0;
450 let mut vy = 0.0;
451 for i in 0..x.len() {
452 let dx = x[i] - mean_x;
453 let dy = y[i] - mean_y;
454 cov += dx * dy;
455 vx += dx * dx;
456 vy += dy * dy;
457 }
458 let denom = (vx * vy).sqrt();
459 if denom == 0.0 {
460 return Err(FinanceError::Unsolvable {
461 message: "correlation undefined when a series has zero variance",
462 });
463 }
464 Ok(cov / denom)
465}
466
467pub fn information_ratio(asset_returns: &[f64], benchmark_returns: &[f64]) -> FinanceResult<f64> {
472 if asset_returns.len() != benchmark_returns.len() {
473 return Err(FinanceError::Unsolvable {
474 message: "information_ratio requires equal-length series",
475 });
476 }
477 if asset_returns.len() < 2 {
478 return Err(FinanceError::Unsolvable {
479 message: "information_ratio requires at least two returns",
480 });
481 }
482 let mut active = Vec::with_capacity(asset_returns.len());
483 for i in 0..asset_returns.len() {
484 require_finite("asset_returns", asset_returns[i])?;
485 require_finite("benchmark_returns", benchmark_returns[i])?;
486 active.push(asset_returns[i] - benchmark_returns[i]);
487 }
488 let vol = volatility(&active)?;
489 if vol == 0.0 {
490 return Err(FinanceError::Unsolvable {
491 message: "information_ratio undefined when tracking error is zero",
492 });
493 }
494 Ok(mean_return(&active)? / vol)
495}
496
497pub fn win_rate(trade_pnls: &[f64]) -> FinanceResult<f64> {
499 if trade_pnls.is_empty() {
500 return Err(FinanceError::Unsolvable {
501 message: "win_rate requires at least one trade",
502 });
503 }
504 let mut wins = 0usize;
505 for &p in trade_pnls {
506 require_finite("trade_pnls", p)?;
507 if p > 0.0 {
508 wins += 1;
509 }
510 }
511 Ok(wins as f64 / trade_pnls.len() as f64)
512}
513
514pub fn profit_factor(trade_pnls: &[f64]) -> FinanceResult<f64> {
518 if trade_pnls.is_empty() {
519 return Err(FinanceError::Unsolvable {
520 message: "profit_factor requires at least one trade",
521 });
522 }
523 let mut gp = 0.0;
524 let mut gl = 0.0;
525 for &p in trade_pnls {
526 require_finite("trade_pnls", p)?;
527 if p > 0.0 {
528 gp += p;
529 } else if p < 0.0 {
530 gl += -p;
531 }
532 }
533 if gl == 0.0 {
534 return Err(FinanceError::Unsolvable {
535 message: "profit_factor undefined when there are no losing trades",
536 });
537 }
538 Ok(gp / gl)
539}
540
541pub fn expectancy(trade_pnls: &[f64]) -> FinanceResult<f64> {
543 if trade_pnls.is_empty() {
544 return Err(FinanceError::Unsolvable {
545 message: "expectancy requires at least one trade",
546 });
547 }
548 for &p in trade_pnls {
549 require_finite("trade_pnls", p)?;
550 }
551 Ok(trade_pnls.iter().sum::<f64>() / trade_pnls.len() as f64)
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use crate::*;
558
559 #[test]
560 fn test_volatility_constant_zero() {
561 let returns = [0.01, 0.01, 0.01, 0.01];
562 assert_approx_equal!(volatility(&returns).unwrap(), 0.0);
563 }
564
565 #[test]
566 fn test_max_drawdown() {
567 let prices = [100.0, 120.0, 90.0, 95.0];
568 assert_approx_equal!(max_drawdown(&prices).unwrap(), 0.25);
569 }
570
571 #[test]
572 fn test_sharpe() {
573 let returns = [0.02, 0.01, 0.03, -0.01, 0.02];
574 let s = sharpe_ratio(&returns, 0.0).unwrap();
575 assert!(s.is_finite() && s > 0.0);
576 }
577
578 #[test]
579 fn test_sortino_has_downside() {
580 let returns = [0.02, -0.03, 0.01, -0.01, 0.02];
581 let s = sortino_ratio(&returns, 0.0).unwrap();
582 assert!(s.is_finite());
583 }
584
585 #[test]
586 fn test_sortino_no_downside_errs() {
587 assert!(sortino_ratio(&[0.01, 0.02, 0.03], 0.0).is_err());
588 }
589
590 #[test]
591 fn test_beta_double() {
592 let market = [0.01, 0.02, -0.01, 0.03];
593 let asset: Vec<f64> = market.iter().map(|r| 2.0 * r).collect();
594 assert!((beta(&asset, &market).unwrap() - 2.0).abs() < 1e-9);
595 }
596
597 #[test]
598 fn test_rolling_max_drawdown() {
599 let prices = [100.0, 120.0, 90.0, 95.0, 130.0];
600 let r = rolling_max_drawdown(&prices).unwrap();
601 assert_eq!(r.len(), 5);
602 assert_approx_equal!(r[2], 0.25);
603 assert_approx_equal!(r[4], 0.25);
604 }
605
606 #[test]
607 fn test_drawdown_series() {
608 let prices = [100.0, 120.0, 90.0];
609 let d = drawdown_series(&prices).unwrap();
610 assert_approx_equal!(d[0], 0.0);
611 assert_approx_equal!(d[1], 0.0);
612 assert_approx_equal!(d[2], 0.25);
613 }
614
615 #[test]
616 fn test_cagr_and_calmar() {
617 let prices = [100.0, 200.0];
618 let g = cagr_from_prices(&prices, 2.0).unwrap();
619 assert!((g - (2.0_f64.sqrt() - 1.0)).abs() < 1e-12);
620 let p = [100.0, 150.0, 80.0, 200.0];
622 let c = calmar_ratio(&p, 3.0).unwrap();
623 assert!(c.is_finite() && c > 0.0);
624 assert!(calmar_ratio(&[100.0, 110.0, 120.0], 1.0).is_err()); }
626
627 #[test]
628 fn test_ulcer_and_correlation() {
629 let prices = [100.0, 120.0, 90.0, 100.0];
630 let u = ulcer_index(&prices).unwrap();
631 assert!(u > 0.0);
632 let x = [1.0, 2.0, 3.0, 4.0];
633 let y = [2.0, 4.0, 6.0, 8.0];
634 assert!((correlation(&x, &y).unwrap() - 1.0).abs() < 1e-12);
635 }
636
637 #[test]
638 fn test_information_ratio_and_trades() {
639 let a = [0.02, 0.01, 0.03, -0.01];
640 let b = [0.01, 0.01, 0.01, 0.01];
641 let ir = information_ratio(&a, &b).unwrap();
642 assert!(ir.is_finite());
643 let pnls = [10.0, -5.0, 20.0, -2.0, 0.0];
644 assert!((win_rate(&pnls).unwrap() - 2.0 / 5.0).abs() < 1e-12);
645 assert!((profit_factor(&pnls).unwrap() - 30.0 / 7.0).abs() < 1e-12);
646 assert!((expectancy(&pnls).unwrap() - 23.0 / 5.0).abs() < 1e-12);
647 }
648}