#[inline]
pub fn compute_max_drawdown(window: &[f64]) -> f64 {
if window.len() < 2 {
return f64::EPSILON;
}
let mut running_max = window[0];
let mut max_drawdown = 0.0;
for &val in window.iter().skip(1) {
if val > running_max {
running_max = val;
}
if running_max > 0.0 && val.is_finite() {
let drawdown = 1.0 - val / running_max;
if drawdown > max_drawdown {
max_drawdown = drawdown;
}
}
}
max_drawdown.max(f64::EPSILON)
}
#[inline]
pub fn compute_max_drawdown_and_runup(window: &[f64]) -> (f64, f64) {
if window.len() < 2 {
return (f64::EPSILON, f64::EPSILON);
}
let mut running_max = window[0];
let mut running_min = window[0];
let mut max_drawdown = 0.0;
let mut max_runup = 0.0;
for &val in window.iter().skip(1) {
if val > running_max {
running_max = val;
}
if val < running_min {
running_min = val;
}
if running_max > 0.0 && val.is_finite() {
let drawdown = 1.0 - val / running_max;
if drawdown > max_drawdown {
max_drawdown = drawdown;
}
}
if val > 0.0 && running_min > 0.0 && val.is_finite() {
let runup = 1.0 - running_min / val;
if runup > max_runup {
max_runup = runup;
}
}
}
(max_drawdown.max(f64::EPSILON), max_runup.max(f64::EPSILON))
}
#[inline]
pub fn compute_max_runup(window: &[f64]) -> f64 {
if window.len() < 2 {
return f64::EPSILON;
}
let mut running_min = window[0];
let mut max_runup = 0.0;
for &val in window.iter().skip(1) {
if val < running_min {
running_min = val;
}
if val > 0.0 && running_min > 0.0 && val.is_finite() {
let runup = 1.0 - running_min / val;
if runup > max_runup {
max_runup = runup;
}
}
}
max_runup.max(f64::EPSILON)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_max_drawdown_uptrend() {
let prices = vec![1.0, 1.01, 1.02, 1.03, 1.04, 1.05];
let dd = compute_max_drawdown(&prices);
assert!(dd < 0.001, "Pure uptrend should have near-zero drawdown");
}
#[test]
fn test_max_drawdown_downtrend() {
let prices = vec![1.0, 0.95, 0.9, 0.85, 0.8];
let dd = compute_max_drawdown(&prices);
assert!((dd - 0.2).abs() < 0.01, "Expected 20% drawdown, got {}", dd);
}
#[test]
fn test_max_drawdown_recovery() {
let prices = vec![1.0, 1.1, 1.0, 0.9, 1.0, 1.1];
let dd = compute_max_drawdown(&prices);
assert!(
(dd - 0.182).abs() < 0.01,
"Expected ~18.2% drawdown, got {}",
dd
);
}
#[test]
fn test_max_runup_downtrend() {
let prices = vec![1.0, 0.99, 0.98, 0.97, 0.96, 0.95];
let ru = compute_max_runup(&prices);
assert!(ru < 0.001, "Pure downtrend should have near-zero runup");
}
#[test]
fn test_max_runup_uptrend() {
let prices = vec![1.0, 1.05, 1.1, 1.15, 1.2, 1.25];
let ru = compute_max_runup(&prices);
assert!((ru - 0.2).abs() < 0.01, "Expected 20% runup, got {}", ru);
}
#[test]
fn test_max_runup_recovery() {
let prices = vec![1.0, 0.9, 0.95, 1.0, 1.05, 1.1];
let ru = compute_max_runup(&prices);
assert!(
(ru - 0.182).abs() < 0.01,
"Expected ~18.2% runup, got {}",
ru
);
}
#[test]
fn test_empty_window() {
assert_eq!(compute_max_drawdown(&[]), f64::EPSILON);
assert_eq!(compute_max_runup(&[]), f64::EPSILON);
}
#[test]
fn test_single_element() {
assert_eq!(compute_max_drawdown(&[1.0]), f64::EPSILON);
assert_eq!(compute_max_runup(&[1.0]), f64::EPSILON);
}
#[test]
fn test_identical_prices_flat() {
let prices = vec![1.0, 1.0, 1.0, 1.0, 1.0];
let dd = compute_max_drawdown(&prices);
let ru = compute_max_runup(&prices);
assert_eq!(dd, f64::EPSILON, "Flat prices should have epsilon drawdown");
assert_eq!(ru, f64::EPSILON, "Flat prices should have epsilon runup");
}
#[test]
fn test_combined_pure_uptrend() {
let prices = vec![1.0, 1.1, 1.2, 1.3, 1.4, 1.5];
let (dd, ru) = compute_max_drawdown_and_runup(&prices);
assert!(dd < 0.001, "Pure uptrend drawdown should be ~0");
assert!((ru - 0.333).abs() < 0.01, "Expected ~33% runup, got {}", ru);
}
#[test]
fn test_combined_pure_downtrend() {
let prices = vec![1.5, 1.4, 1.3, 1.2, 1.1, 1.0];
let (dd, ru) = compute_max_drawdown_and_runup(&prices);
assert!(
(dd - 0.333).abs() < 0.01,
"Expected ~33% drawdown, got {}",
dd
);
assert!(ru < 0.001, "Pure downtrend runup should be ~0");
}
#[test]
fn test_combined_identical_prices() {
let prices = vec![100.0, 100.0, 100.0];
let (dd, ru) = compute_max_drawdown_and_runup(&prices);
assert_eq!(dd, f64::EPSILON);
assert_eq!(ru, f64::EPSILON);
}
#[test]
fn test_combined_empty_and_single() {
assert_eq!(
compute_max_drawdown_and_runup(&[]),
(f64::EPSILON, f64::EPSILON)
);
assert_eq!(
compute_max_drawdown_and_runup(&[42.0]),
(f64::EPSILON, f64::EPSILON)
);
}
#[test]
fn test_combined_matches_individual() {
let prices = vec![1.0, 1.1, 0.9, 1.05, 0.85, 1.15];
let dd_individual = compute_max_drawdown(&prices);
let ru_individual = compute_max_runup(&prices);
let (dd_combined, ru_combined) = compute_max_drawdown_and_runup(&prices);
assert!(
(dd_individual - dd_combined).abs() < f64::EPSILON * 10.0,
"Drawdown mismatch: {} vs {}",
dd_individual,
dd_combined
);
assert!(
(ru_individual - ru_combined).abs() < f64::EPSILON * 10.0,
"Runup mismatch: {} vs {}",
ru_individual,
ru_combined
);
}
#[test]
fn test_two_element_window() {
let up = vec![1.0, 1.5];
let dd = compute_max_drawdown(&up);
let ru = compute_max_runup(&up);
assert_eq!(dd, f64::EPSILON, "Upward 2-element: no drawdown");
assert!((ru - 0.333).abs() < 0.01, "Upward 2-element: 33% runup");
let down = vec![1.5, 1.0];
let dd = compute_max_drawdown(&down);
let ru = compute_max_runup(&down);
assert!(
(dd - 0.333).abs() < 0.01,
"Downward 2-element: 33% drawdown"
);
assert_eq!(ru, f64::EPSILON, "Downward 2-element: no runup");
}
}