wickra_core/indicators/
running_drawdown.rs1use crate::traits::Indicator;
4
5#[derive(Debug, Clone)]
45pub struct RunningDrawdown {
46 peak: f64,
47 current: f64,
48 max: f64,
49 seen: bool,
50}
51
52impl RunningDrawdown {
53 pub const fn new() -> Self {
55 Self {
56 peak: f64::NEG_INFINITY,
57 current: 0.0,
58 max: 0.0,
59 seen: false,
60 }
61 }
62
63 pub const fn max_drawdown(&self) -> Option<f64> {
65 if self.seen {
66 Some(self.max)
67 } else {
68 None
69 }
70 }
71
72 pub const fn current_drawdown(&self) -> Option<f64> {
75 if self.seen {
76 Some(self.current)
77 } else {
78 None
79 }
80 }
81
82 pub const fn peak(&self) -> Option<f64> {
84 if self.seen {
85 Some(self.peak)
86 } else {
87 None
88 }
89 }
90}
91
92impl Default for RunningDrawdown {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98impl Indicator for RunningDrawdown {
99 type Input = f64;
100 type Output = f64;
101
102 #[inline]
103 fn update(&mut self, input: f64) -> Option<f64> {
104 if !input.is_finite() {
105 return None;
106 }
107 if input > self.peak {
108 self.peak = input;
109 }
110 self.current = if self.peak > 0.0 {
113 (self.peak - input) / self.peak
114 } else {
115 0.0
116 };
117 if self.current > self.max {
118 self.max = self.current;
119 }
120 self.seen = true;
121 Some(self.max)
122 }
123
124 fn reset(&mut self) {
125 *self = Self::new();
126 }
127
128 #[inline]
129 fn warmup_period(&self) -> usize {
130 1
131 }
132
133 #[inline]
134 fn is_ready(&self) -> bool {
135 self.seen
136 }
137
138 #[inline]
139 fn name(&self) -> &'static str {
140 "RunningDrawdown"
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::indicators::MaxDrawdown;
148 use crate::traits::BatchExt;
149 use approx::assert_relative_eq;
150
151 #[test]
152 fn accessors_and_metadata() {
153 let mut dd = RunningDrawdown::new();
154 assert_eq!(dd.name(), "RunningDrawdown");
155 assert_eq!(dd.warmup_period(), 1);
156 assert_eq!(dd.max_drawdown(), None);
157 assert_eq!(dd.current_drawdown(), None);
158 assert_eq!(dd.peak(), None);
159 dd.update(100.0);
160 assert_eq!(dd.max_drawdown(), Some(0.0));
161 assert_eq!(dd.current_drawdown(), Some(0.0));
162 assert_eq!(dd.peak(), Some(100.0));
163 }
164
165 #[test]
166 fn default_matches_new() {
167 let mut a = RunningDrawdown::default();
168 let mut b = RunningDrawdown::new();
169 assert_eq!(a.warmup_period(), b.warmup_period());
170 assert_eq!(a.is_ready(), b.is_ready());
171 assert_eq!(a.peak(), b.peak());
172 assert_eq!(a.update(100.0), b.update(100.0));
173 assert_eq!(a.update(80.0), b.update(80.0));
174 }
175
176 #[test]
177 fn first_value_emits_immediately() {
178 let mut dd = RunningDrawdown::new();
179 assert_eq!(dd.update(100.0), Some(0.0));
180 }
181
182 #[test]
183 fn reference_drawdown() {
184 let mut dd = RunningDrawdown::new();
187 let out = dd.batch(&[100.0, 120.0, 90.0, 110.0]);
188 assert_eq!(out[0], Some(0.0));
189 assert_eq!(out[1], Some(0.0));
190 assert_relative_eq!(out[2].unwrap(), 0.25, epsilon = 1e-12);
191 assert_relative_eq!(out[3].unwrap(), 0.25, epsilon = 1e-12);
192 assert_relative_eq!(
193 dd.current_drawdown().unwrap(),
194 10.0 / 120.0,
195 epsilon = 1e-12
196 );
197 assert_eq!(dd.peak(), Some(120.0));
198 }
199
200 #[test]
201 fn remembers_a_drawdown_older_than_any_window() {
202 let mut dd = RunningDrawdown::new();
203 dd.batch(&[100.0, 50.0]);
204 for v in 0..1000 {
205 dd.update(100.0 + f64::from(v));
206 }
207 assert_relative_eq!(dd.max_drawdown().unwrap(), 0.5, epsilon = 1e-12);
208 assert_relative_eq!(dd.current_drawdown().unwrap(), 0.0, epsilon = 1e-12);
209 }
210
211 #[test]
212 fn pure_uptrend_yields_zero() {
213 let mut dd = RunningDrawdown::new();
214 let out = dd.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
215 for v in out.into_iter().flatten() {
216 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
217 }
218 }
219
220 #[test]
221 fn non_positive_peak_yields_zero() {
222 let mut dd = RunningDrawdown::new();
225 let out = dd.batch(&[-1.0, -5.0, -3.0, 0.0, -2.0]);
226 for v in out.into_iter().flatten() {
227 assert_eq!(v, 0.0);
228 }
229 assert_eq!(dd.current_drawdown(), Some(0.0));
230 }
231
232 #[test]
233 fn ignores_non_finite_input() {
234 let mut dd = RunningDrawdown::new();
235 dd.batch(&[100.0, 90.0, 80.0]);
236 let (max, cur, peak) = (dd.max_drawdown(), dd.current_drawdown(), dd.peak());
237 assert_eq!(dd.update(f64::NAN), None);
238 assert_eq!(dd.update(f64::INFINITY), None);
239 assert_eq!(dd.update(f64::NEG_INFINITY), None);
240 assert_eq!(dd.max_drawdown(), max);
242 assert_eq!(dd.current_drawdown(), cur);
243 assert_eq!(dd.peak(), peak);
244 }
245
246 #[test]
247 fn non_finite_first_input_leaves_it_unseeded() {
248 let mut dd = RunningDrawdown::new();
249 assert_eq!(dd.update(f64::NAN), None);
250 assert!(!dd.is_ready());
251 assert_eq!(dd.peak(), None);
252 }
253
254 #[test]
255 fn reset_clears_state() {
256 let mut dd = RunningDrawdown::new();
257 dd.batch(&[100.0, 90.0, 80.0]);
258 assert!(dd.is_ready());
259 dd.reset();
260 assert!(!dd.is_ready());
261 assert_eq!(dd.max_drawdown(), None);
262 assert_eq!(dd.peak(), None);
263 assert_eq!(dd.update(100.0), Some(0.0));
264 }
265
266 #[test]
267 fn batch_equals_streaming() {
268 let prices: Vec<f64> = (0..60)
269 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 10.0)
270 .collect();
271 let batch = RunningDrawdown::new().batch(&prices);
272 let mut s = RunningDrawdown::new();
273 let streamed: Vec<_> = prices.iter().map(|p| s.update(*p)).collect();
274 assert_eq!(batch, streamed);
275 }
276
277 #[test]
278 fn matches_max_drawdown_with_a_period_covering_the_stream() {
279 let prices: Vec<f64> = (0..200)
282 .map(|i| 50.0 + (f64::from(i) * 0.17).sin() * 60.0 + f64::from(i) * 0.1)
283 .collect();
284 let period = prices.len();
285 let mut windowed = MaxDrawdown::new(period).unwrap();
286 let mut running = RunningDrawdown::new();
287 for (i, &p) in prices.iter().enumerate() {
288 let w = windowed.update(p);
289 let r = running.update(p);
290 if let Some(w) = w {
293 assert_relative_eq!(r.unwrap(), w, epsilon = 1e-12);
294 } else {
295 assert!(i + 1 < period);
296 }
297 }
298 assert!(windowed.value().is_some());
299 }
300
301 #[test]
302 fn matches_max_drawdown_at_every_bar_via_growing_periods() {
303 let prices: Vec<f64> = (0..40)
306 .map(|i| 100.0 + (f64::from(i) * 0.5).sin() * 25.0)
307 .collect();
308 let mut running = RunningDrawdown::new();
309 for (i, &p) in prices.iter().enumerate() {
310 let r = running.update(p).unwrap();
311 let w = MaxDrawdown::new(i + 1)
312 .unwrap()
313 .batch(&prices[..=i])
314 .pop()
315 .flatten()
316 .unwrap();
317 assert_relative_eq!(r, w, epsilon = 1e-12);
318 }
319 }
320}