quant_metrics/
drawdown.rs1use rust_decimal::Decimal;
4
5use crate::MetricsError;
6
7pub fn max_drawdown(equity: &[Decimal]) -> Result<Decimal, MetricsError> {
32 if equity.len() < 2 {
33 return Err(MetricsError::InsufficientData {
34 required: 2,
35 actual: equity.len(),
36 });
37 }
38
39 let dd_series = drawdown_series(equity)?;
40 Ok(dd_series.into_iter().min().unwrap_or(Decimal::ZERO))
41}
42
43pub fn drawdown_series(equity: &[Decimal]) -> Result<Vec<Decimal>, MetricsError> {
53 if equity.is_empty() {
54 return Err(MetricsError::InsufficientData {
55 required: 1,
56 actual: 0,
57 });
58 }
59
60 let mut peak = equity[0];
61 let mut drawdowns = Vec::with_capacity(equity.len());
62
63 for &value in equity {
64 if value > peak {
65 peak = value;
66 }
67
68 if peak == Decimal::ZERO {
69 drawdowns.push(Decimal::ZERO);
70 } else {
71 let dd = ((value - peak) / peak) * Decimal::from(100);
72 drawdowns.push(dd);
73 }
74 }
75
76 Ok(drawdowns)
77}
78
79pub fn max_drawdown_duration(equity: &[Decimal]) -> Result<usize, MetricsError> {
89 if equity.len() < 2 {
90 return Err(MetricsError::InsufficientData {
91 required: 2,
92 actual: equity.len(),
93 });
94 }
95
96 let mut peak = equity[0];
97 let mut peak_idx = 0;
98 let mut max_duration = 0;
99 let mut current_duration = 0;
100
101 for (i, &value) in equity.iter().enumerate() {
102 if value >= peak {
103 if current_duration > max_duration {
105 max_duration = current_duration;
106 }
107 peak = value;
108 peak_idx = i;
109 current_duration = 0;
110 } else {
111 current_duration = i - peak_idx;
113 }
114 }
115
116 if current_duration > max_duration {
118 max_duration = current_duration;
119 }
120
121 Ok(max_duration)
122}
123
124pub fn recovery_time(equity: &[Decimal]) -> Result<Option<usize>, MetricsError> {
135 if equity.len() < 2 {
136 return Err(MetricsError::InsufficientData {
137 required: 2,
138 actual: equity.len(),
139 });
140 }
141
142 let Some((trough_idx, trough_peak)) = find_max_drawdown_trough(equity) else {
143 return Ok(Some(0));
145 };
146
147 for (i, &value) in equity.iter().enumerate().skip(trough_idx + 1) {
149 if value >= trough_peak {
150 return Ok(Some(i - trough_idx));
151 }
152 }
153
154 Ok(None)
156}
157
158fn find_max_drawdown_trough(equity: &[Decimal]) -> Option<(usize, Decimal)> {
162 let mut peak = equity[0];
163 let mut max_dd = Decimal::ZERO;
164 let mut max_dd_trough_idx = 0;
165 let mut max_dd_peak = equity[0];
166
167 for (i, &value) in equity.iter().enumerate() {
168 if value > peak {
169 peak = value;
170 }
171 if peak != Decimal::ZERO {
172 let dd = (value - peak) / peak;
173 if dd < max_dd {
174 max_dd = dd;
175 max_dd_trough_idx = i;
176 max_dd_peak = peak;
177 }
178 }
179 }
180
181 if max_dd == Decimal::ZERO {
182 None
183 } else {
184 Some((max_dd_trough_idx, max_dd_peak))
185 }
186}
187
188#[cfg(test)]
189#[path = "drawdown_tests.rs"]
190mod tests;