use crate::monte_carlo::engine::{percentile, SimulationPath};
#[derive(Debug, Clone)]
pub struct DrawdownAnalysis;
impl DrawdownAnalysis {
#[must_use]
pub fn max_drawdown(values: &[f64]) -> f64 {
if values.len() < 2 {
return 0.0;
}
let mut max_drawdown = 0.0;
let mut peak = values[0];
for &value in values.iter().skip(1) {
if value > peak {
peak = value;
} else if peak > 0.0 {
let drawdown = (peak - value) / peak;
if drawdown > max_drawdown {
max_drawdown = drawdown;
}
}
}
max_drawdown
}
#[must_use]
pub fn drawdown_series(values: &[f64]) -> Vec<f64> {
if values.is_empty() {
return Vec::new();
}
let mut drawdowns = Vec::with_capacity(values.len());
let mut peak = values[0];
for &value in values {
if value > peak {
peak = value;
}
let dd = if peak > 0.0 {
(peak - value) / peak
} else {
0.0
};
drawdowns.push(dd);
}
drawdowns
}
#[must_use]
pub fn max_drawdown_duration(values: &[f64]) -> usize {
if values.len() < 2 {
return 0;
}
let mut max_duration = 0;
let mut current_duration = 0;
let mut peak = values[0];
for &value in values.iter().skip(1) {
if value >= peak {
peak = value;
current_duration = 0;
} else {
current_duration += 1;
if current_duration > max_duration {
max_duration = current_duration;
}
}
}
max_duration
}
#[must_use]
pub fn ulcer_index(values: &[f64]) -> f64 {
let drawdowns = Self::drawdown_series(values);
if drawdowns.is_empty() {
return 0.0;
}
let sum_sq: f64 = drawdowns.iter().map(|d| d * d).sum();
(sum_sq / drawdowns.len() as f64).sqrt()
}
#[must_use]
pub fn pain_index(values: &[f64]) -> f64 {
let drawdowns = Self::drawdown_series(values);
if drawdowns.is_empty() {
return 0.0;
}
drawdowns.iter().sum::<f64>() / drawdowns.len() as f64
}
#[must_use]
pub fn from_paths(paths: &[SimulationPath]) -> DrawdownStatistics {
if paths.is_empty() {
return DrawdownStatistics::default();
}
let max_drawdowns: Vec<f64> = paths
.iter()
.map(|p| Self::max_drawdown(&p.values))
.collect();
DrawdownStatistics::from_drawdowns(&max_drawdowns)
}
#[must_use]
pub fn recovery_factor(values: &[f64]) -> f64 {
if values.len() < 2 {
return 0.0;
}
let first = values[0];
let last = values[values.len() - 1];
if first <= 0.0 {
return 0.0;
}
let total_return = (last - first) / first;
let max_dd = Self::max_drawdown(values);
if max_dd > 0.0 {
total_return / max_dd
} else if total_return > 0.0 {
f64::INFINITY
} else {
0.0
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DrawdownStatistics {
pub mean: f64,
pub median: f64,
pub std: f64,
pub p5: f64,
pub p25: f64,
pub p75: f64,
pub p95: f64,
pub p99: f64,
pub worst: f64,
pub best: f64,
}
impl DrawdownStatistics {
#[must_use]
pub fn from_drawdowns(drawdowns: &[f64]) -> Self {
if drawdowns.is_empty() {
return Self::default();
}
let n = drawdowns.len() as f64;
let mean = drawdowns.iter().sum::<f64>() / n;
let variance = drawdowns.iter().map(|d| (d - mean).powi(2)).sum::<f64>() / n;
let std = variance.sqrt();
let worst = drawdowns.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let best = drawdowns.iter().copied().fold(f64::INFINITY, f64::min);
Self {
mean,
median: percentile(drawdowns, 0.5),
std,
p5: percentile(drawdowns, 0.05),
p25: percentile(drawdowns, 0.25),
p75: percentile(drawdowns, 0.75),
p95: percentile(drawdowns, 0.95),
p99: percentile(drawdowns, 0.99),
worst,
best,
}
}
#[must_use]
pub fn exceeds_threshold(&self, threshold: f64, confidence: f64) -> bool {
let percentile_value = match confidence {
c if c >= 0.99 => self.p99,
c if c >= 0.95 => self.p95,
c if c >= 0.75 => self.p75,
c if c >= 0.50 => self.median,
_ => self.p25,
};
percentile_value > threshold
}
}
#[path = "drawdown_tests.rs"]
mod drawdown_tests;