wickra_core/indicators/
average_drawdown.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
26pub struct AverageDrawdown {
27 period: usize,
28 window: VecDeque<f64>,
29}
30
31impl AverageDrawdown {
32 pub fn new(period: usize) -> Result<Self> {
37 if period == 0 {
38 return Err(Error::PeriodZero);
39 }
40 Ok(Self {
41 period,
42 window: VecDeque::with_capacity(period),
43 })
44 }
45
46 pub const fn period(&self) -> usize {
48 self.period
49 }
50}
51
52impl Indicator for AverageDrawdown {
53 type Input = f64;
54 type Output = f64;
55
56 fn update(&mut self, input: f64) -> Option<f64> {
57 if !input.is_finite() {
58 return None;
59 }
60 if self.window.len() == self.period {
61 self.window.pop_front();
62 }
63 self.window.push_back(input);
64 if self.window.len() < self.period {
65 return None;
66 }
67 let mut peak = f64::NEG_INFINITY;
68 let mut sum_dd = 0.0_f64;
69 for &v in &self.window {
70 if v > peak {
71 peak = v;
72 }
73 if peak > 0.0 {
74 sum_dd += (peak - v) / peak;
75 }
76 }
77 Some(sum_dd / self.period as f64)
78 }
79
80 fn reset(&mut self) {
81 self.window.clear();
82 }
83
84 fn warmup_period(&self) -> usize {
85 self.period
86 }
87
88 fn is_ready(&self) -> bool {
89 self.window.len() == self.period
90 }
91
92 fn name(&self) -> &'static str {
93 "AverageDrawdown"
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::traits::BatchExt;
101 use approx::assert_relative_eq;
102
103 #[test]
104 fn rejects_zero_period() {
105 assert!(matches!(AverageDrawdown::new(0), Err(Error::PeriodZero)));
106 }
107
108 #[test]
109 fn accessors_and_metadata() {
110 let a = AverageDrawdown::new(10).unwrap();
111 assert_eq!(a.period(), 10);
112 assert_eq!(a.name(), "AverageDrawdown");
113 assert_eq!(a.warmup_period(), 10);
114 }
115
116 #[test]
117 fn pure_uptrend_yields_zero() {
118 let mut a = AverageDrawdown::new(5).unwrap();
119 let out = a.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
120 for v in out.into_iter().flatten() {
121 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
122 }
123 }
124
125 #[test]
126 fn reference_value() {
127 let mut a = AverageDrawdown::new(4).unwrap();
131 let out = a.batch(&[100.0, 120.0, 90.0, 110.0]);
132 let expected = (0.25 + (10.0 / 120.0)) / 4.0;
133 assert_relative_eq!(out[3].unwrap(), expected, epsilon = 1e-12);
134 }
135
136 #[test]
137 fn ignores_non_finite_input() {
138 let mut a = AverageDrawdown::new(3).unwrap();
139 assert_eq!(a.update(f64::NAN), None);
140 assert_eq!(a.update(f64::INFINITY), None);
141 }
142
143 #[test]
144 fn reset_clears_state() {
145 let mut a = AverageDrawdown::new(3).unwrap();
146 a.batch(&[100.0, 90.0, 110.0]);
147 assert!(a.is_ready());
148 a.reset();
149 assert!(!a.is_ready());
150 assert_eq!(a.update(100.0), None);
151 }
152
153 #[test]
154 fn batch_equals_streaming() {
155 let prices: Vec<f64> = (0..40)
156 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 8.0)
157 .collect();
158 let batch = AverageDrawdown::new(10).unwrap().batch(&prices);
159 let mut s = AverageDrawdown::new(10).unwrap();
160 let streamed: Vec<_> = prices.iter().map(|p| s.update(*p)).collect();
161 assert_eq!(batch, streamed);
162 }
163
164 #[test]
165 fn non_positive_peak_yields_zero() {
166 let mut a = AverageDrawdown::new(3).unwrap();
167 let out = a.batch(&[0.0_f64; 6]);
168 for v in out.into_iter().flatten() {
169 assert_eq!(v, 0.0);
170 }
171 }
172}