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