wickra_core/indicators/
average_drawdown.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
30pub struct AverageDrawdown {
31 period: usize,
32 window: VecDeque<f64>,
33}
34
35impl AverageDrawdown {
36 pub fn new(period: usize) -> Result<Self> {
41 if period == 0 {
42 return Err(Error::PeriodZero);
43 }
44 if period > crate::error::MAX_PERIOD {
45 return Err(Error::InvalidPeriod {
46 message: crate::error::PERIOD_ABOVE_MAX,
47 });
48 }
49 Ok(Self {
50 period,
51 window: VecDeque::with_capacity(period),
52 })
53 }
54
55 pub const fn period(&self) -> usize {
57 self.period
58 }
59}
60
61impl Indicator for AverageDrawdown {
62 type Input = f64;
63 type Output = f64;
64
65 fn update(&mut self, input: f64) -> Option<f64> {
66 if !input.is_finite() {
67 return None;
68 }
69 if self.window.len() == self.period {
70 self.window.pop_front();
71 }
72 self.window.push_back(input);
73 if self.window.len() < self.period {
74 return None;
75 }
76 let mut peak = f64::NEG_INFINITY;
77 let mut sum_depth = 0.0_f64;
78 let mut episodes = 0_u32;
79 let mut in_dd = false;
80 let mut episode_peak = 0.0_f64;
81 let mut episode_trough = 0.0_f64;
82 for &v in &self.window {
83 if v >= peak {
84 if in_dd {
85 if episode_peak > 0.0 {
86 sum_depth += (episode_peak - episode_trough) / episode_peak;
87 episodes += 1;
88 }
89 in_dd = false;
90 }
91 peak = v;
92 } else if in_dd {
93 if v < episode_trough {
94 episode_trough = v;
95 }
96 } else {
97 in_dd = true;
98 episode_peak = peak;
99 episode_trough = v;
100 }
101 }
102 if in_dd && episode_peak > 0.0 {
103 sum_depth += (episode_peak - episode_trough) / episode_peak;
104 episodes += 1;
105 }
106 Some(if episodes == 0 {
107 0.0
108 } else {
109 sum_depth / f64::from(episodes)
110 })
111 }
112
113 fn reset(&mut self) {
114 self.window.clear();
115 }
116
117 #[inline]
118 fn warmup_period(&self) -> usize {
119 self.period
120 }
121
122 #[inline]
123 fn is_ready(&self) -> bool {
124 self.window.len() == self.period
125 }
126
127 #[inline]
128 fn name(&self) -> &'static str {
129 "AverageDrawdown"
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::traits::BatchExt;
137 use approx::assert_relative_eq;
138
139 #[test]
140 fn rejects_zero_period() {
141 assert!(matches!(AverageDrawdown::new(0), Err(Error::PeriodZero)));
142 }
143
144 #[test]
145 fn accessors_and_metadata() {
146 let a = AverageDrawdown::new(10).unwrap();
147 assert_eq!(a.period(), 10);
148 assert_eq!(a.name(), "AverageDrawdown");
149 assert_eq!(a.warmup_period(), 10);
150 }
151
152 #[test]
153 fn pure_uptrend_yields_zero() {
154 let mut a = AverageDrawdown::new(5).unwrap();
155 let out = a.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
156 for v in out.into_iter().flatten() {
157 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
158 }
159 }
160
161 #[test]
162 fn reference_value() {
163 let mut a = AverageDrawdown::new(4).unwrap();
168 let out = a.batch(&[100.0, 120.0, 90.0, 110.0]);
169 assert_relative_eq!(out[3].unwrap(), 0.25, epsilon = 1e-12);
170 }
171
172 #[test]
173 fn averages_distinct_episodes() {
174 let mut a = AverageDrawdown::new(5).unwrap();
179 let out = a.batch(&[100.0, 90.0, 100.0, 80.0, 100.0]);
180 assert_relative_eq!(out[4].unwrap(), 0.15, epsilon = 1e-12);
181 }
182
183 #[test]
184 fn ignores_non_finite_input() {
185 let mut a = AverageDrawdown::new(3).unwrap();
186 assert_eq!(a.update(f64::NAN), None);
187 assert_eq!(a.update(f64::INFINITY), None);
188 }
189
190 #[test]
191 fn reset_clears_state() {
192 let mut a = AverageDrawdown::new(3).unwrap();
193 a.batch(&[100.0, 90.0, 110.0]);
194 assert!(a.is_ready());
195 a.reset();
196 assert!(!a.is_ready());
197 assert_eq!(a.update(100.0), None);
198 }
199
200 #[test]
201 fn batch_equals_streaming() {
202 let prices: Vec<f64> = (0..40)
203 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 8.0)
204 .collect();
205 let batch = AverageDrawdown::new(10).unwrap().batch(&prices);
206 let mut s = AverageDrawdown::new(10).unwrap();
207 let streamed: Vec<_> = prices.iter().map(|p| s.update(*p)).collect();
208 assert_eq!(batch, streamed);
209 }
210
211 #[test]
212 fn non_positive_peak_yields_zero() {
213 let mut a = AverageDrawdown::new(3).unwrap();
214 let out = a.batch(&[0.0_f64; 6]);
215 for v in out.into_iter().flatten() {
216 assert_eq!(v, 0.0);
217 }
218 }
219}