wickra_core/indicators/
drawdown_duration.rs1use crate::traits::Indicator;
4
5#[derive(Debug, Clone, Default)]
34pub struct DrawdownDuration {
35 peak: f64,
36 bars_under_water: u32,
37 seen: bool,
38}
39
40impl DrawdownDuration {
41 pub const fn new() -> Self {
43 Self {
44 peak: f64::NEG_INFINITY,
45 bars_under_water: 0,
46 seen: false,
47 }
48 }
49
50 pub const fn value(&self) -> Option<u32> {
52 if self.seen {
53 Some(self.bars_under_water)
54 } else {
55 None
56 }
57 }
58}
59
60impl Indicator for DrawdownDuration {
61 type Input = f64;
62 type Output = u32;
63
64 #[inline]
65 fn update(&mut self, input: f64) -> Option<u32> {
66 if !input.is_finite() {
67 return None;
68 }
69 if !self.seen || input >= self.peak {
70 self.peak = input;
71 self.bars_under_water = 0;
72 } else {
73 self.bars_under_water = self.bars_under_water.saturating_add(1);
74 }
75 self.seen = true;
76 Some(self.bars_under_water)
77 }
78
79 fn reset(&mut self) {
80 self.peak = f64::NEG_INFINITY;
81 self.bars_under_water = 0;
82 self.seen = false;
83 }
84
85 #[inline]
86 fn warmup_period(&self) -> usize {
87 1
88 }
89
90 #[inline]
91 fn is_ready(&self) -> bool {
92 self.seen
93 }
94
95 #[inline]
96 fn name(&self) -> &'static str {
97 "DrawdownDuration"
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::traits::BatchExt;
105
106 #[test]
107 fn accessors_and_metadata() {
108 let mut d = DrawdownDuration::new();
109 assert_eq!(d.name(), "DrawdownDuration");
110 assert_eq!(d.warmup_period(), 1);
111 assert_eq!(d.value(), None);
112 d.update(100.0);
113 assert_eq!(d.value(), Some(0));
114 }
115
116 #[test]
117 fn first_bar_is_peak() {
118 let mut d = DrawdownDuration::new();
119 assert_eq!(d.update(100.0), Some(0));
120 }
121
122 #[test]
123 fn under_water_counter_increments() {
124 let mut d = DrawdownDuration::new();
125 d.update(100.0);
126 assert_eq!(d.update(90.0), Some(1));
127 assert_eq!(d.update(80.0), Some(2));
128 assert_eq!(d.update(85.0), Some(3));
129 }
130
131 #[test]
132 fn new_peak_resets_counter() {
133 let mut d = DrawdownDuration::new();
134 d.update(100.0);
135 d.update(90.0);
136 d.update(80.0);
137 assert_eq!(d.update(105.0), Some(0));
138 assert_eq!(d.update(95.0), Some(1));
139 }
140
141 #[test]
142 fn equal_value_is_treated_as_peak() {
143 let mut d = DrawdownDuration::new();
144 d.update(100.0);
145 assert_eq!(d.update(100.0), Some(0));
146 }
147
148 #[test]
149 fn ignores_non_finite_input() {
150 let mut d = DrawdownDuration::new();
151 d.update(100.0);
152 d.update(90.0);
153 let v = d.value();
154 assert_eq!(d.update(f64::NAN), None);
155 assert_eq!(d.update(f64::INFINITY), None);
156 assert_eq!(d.value(), v);
158 }
159
160 #[test]
161 fn reset_clears_state() {
162 let mut d = DrawdownDuration::new();
163 d.batch(&[100.0, 90.0, 80.0]);
164 assert!(d.is_ready());
165 d.reset();
166 assert!(!d.is_ready());
167 assert_eq!(d.update(100.0), Some(0));
168 }
169
170 #[test]
171 fn batch_equals_streaming() {
172 let prices: Vec<f64> = (0..30)
173 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
174 .collect();
175 let batch = DrawdownDuration::new().batch(&prices);
176 let mut s = DrawdownDuration::new();
177 let streamed: Vec<_> = prices.iter().map(|p| s.update(*p)).collect();
178 assert_eq!(batch, streamed);
179 }
180}