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