wickra_core/indicators/
max_drawdown.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
42pub struct MaxDrawdown {
43 period: usize,
44 count: u64,
45 peak_dq: VecDeque<(u64, f64)>,
48 window: VecDeque<f64>,
49 last: Option<f64>,
50}
51
52impl MaxDrawdown {
53 pub fn new(period: usize) -> Result<Self> {
58 if period == 0 {
59 return Err(Error::PeriodZero);
60 }
61 if period > crate::error::MAX_PERIOD {
62 return Err(Error::InvalidPeriod {
63 message: crate::error::PERIOD_ABOVE_MAX,
64 });
65 }
66 Ok(Self {
67 period,
68 count: 0,
69 peak_dq: VecDeque::with_capacity(period),
70 window: VecDeque::with_capacity(period),
71 last: None,
72 })
73 }
74
75 pub const fn period(&self) -> usize {
77 self.period
78 }
79
80 pub const fn value(&self) -> Option<f64> {
82 self.last
83 }
84}
85
86impl Indicator for MaxDrawdown {
87 type Input = f64;
88 type Output = f64;
89
90 fn update(&mut self, input: f64) -> Option<f64> {
91 if !input.is_finite() {
92 return None;
93 }
94 self.count += 1;
95 while let Some(&(_, back)) = self.peak_dq.back() {
98 if back <= input {
99 self.peak_dq.pop_back();
100 } else {
101 break;
102 }
103 }
104 self.peak_dq.push_back((self.count, input));
105 if self.window.len() == self.period {
107 self.window.pop_front();
108 }
109 self.window.push_back(input);
110 let window_lo = self.count.saturating_sub(self.period as u64 - 1);
111 while let Some(&(idx, _)) = self.peak_dq.front() {
112 if idx < window_lo {
113 self.peak_dq.pop_front();
114 } else {
115 break;
116 }
117 }
118 if self.window.len() < self.period {
119 return None;
120 }
121 let mut peak = f64::NEG_INFINITY;
123 let mut worst = 0.0_f64;
124 for &v in &self.window {
125 if v > peak {
126 peak = v;
127 }
128 if peak > 0.0 {
129 let dd = (peak - v) / peak;
130 if dd > worst {
131 worst = dd;
132 }
133 }
134 }
135 self.last = Some(worst);
136 Some(worst)
137 }
138
139 fn reset(&mut self) {
140 self.count = 0;
141 self.peak_dq.clear();
142 self.window.clear();
143 self.last = None;
144 }
145
146 #[inline]
147 fn warmup_period(&self) -> usize {
148 self.period
149 }
150
151 #[inline]
152 fn is_ready(&self) -> bool {
153 self.last.is_some()
154 }
155
156 #[inline]
157 fn name(&self) -> &'static str {
158 "MaxDrawdown"
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::traits::BatchExt;
166 use approx::assert_relative_eq;
167
168 #[test]
169 fn new_rejects_zero_period() {
170 assert!(matches!(MaxDrawdown::new(0), Err(Error::PeriodZero)));
171 }
172
173 #[test]
174 fn accessors_and_metadata() {
175 let mut mdd = MaxDrawdown::new(10).unwrap();
176 assert_eq!(mdd.period(), 10);
177 assert_eq!(mdd.name(), "MaxDrawdown");
178 assert_eq!(mdd.value(), None);
179 assert_eq!(mdd.warmup_period(), 10);
180 for v in 1..=10 {
181 mdd.update(f64::from(v));
182 }
183 assert!(mdd.value().is_some());
184 }
185
186 #[test]
187 fn pure_uptrend_yields_zero() {
188 let mut mdd = MaxDrawdown::new(5).unwrap();
189 let out = mdd.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
190 for v in out.into_iter().flatten() {
191 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
192 }
193 }
194
195 #[test]
196 fn reference_drawdown() {
197 let mut mdd = MaxDrawdown::new(3).unwrap();
199 let out = mdd.batch(&[100.0, 120.0, 90.0]);
200 assert_eq!(out[0], None);
201 assert_eq!(out[1], None);
202 assert_relative_eq!(out[2].unwrap(), 0.25, epsilon = 1e-12);
203 }
204
205 #[test]
206 fn constant_series_yields_zero() {
207 let mut mdd = MaxDrawdown::new(4).unwrap();
208 let out = mdd.batch(&[50.0; 12]);
209 for v in out.into_iter().flatten() {
210 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
211 }
212 }
213
214 #[test]
215 fn ignores_non_finite_input() {
216 let mut mdd = MaxDrawdown::new(3).unwrap();
217 mdd.batch(&[100.0, 90.0, 80.0]);
218 let last = mdd.value();
219 assert_eq!(mdd.update(f64::NAN), None);
220 assert_eq!(mdd.update(f64::INFINITY), None);
221 assert_eq!(mdd.value(), last);
223 }
224
225 #[test]
226 fn reset_clears_state() {
227 let mut mdd = MaxDrawdown::new(3).unwrap();
228 mdd.batch(&[100.0, 90.0, 80.0]);
229 assert!(mdd.is_ready());
230 mdd.reset();
231 assert!(!mdd.is_ready());
232 assert_eq!(mdd.update(100.0), None);
233 }
234
235 #[test]
236 fn batch_equals_streaming() {
237 let prices: Vec<f64> = (0..60)
238 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 10.0)
239 .collect();
240 let batch = MaxDrawdown::new(10).unwrap().batch(&prices);
241 let mut s = MaxDrawdown::new(10).unwrap();
242 let streamed: Vec<_> = prices.iter().map(|p| s.update(*p)).collect();
243 assert_eq!(batch, streamed);
244 }
245
246 #[test]
247 fn non_positive_peak_yields_zero() {
248 let mut mdd = MaxDrawdown::new(3).unwrap();
250 let out = mdd.batch(&[0.0_f64; 6]);
251 for v in out.into_iter().flatten() {
252 assert_eq!(v, 0.0);
253 }
254 }
255}