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 fn update(&mut self, input: f64) -> Option<u32> {
65 if !input.is_finite() {
66 return self.value();
67 }
68 if !self.seen || input >= self.peak {
69 self.peak = input;
70 self.bars_under_water = 0;
71 } else {
72 self.bars_under_water = self.bars_under_water.saturating_add(1);
73 }
74 self.seen = true;
75 Some(self.bars_under_water)
76 }
77
78 fn reset(&mut self) {
79 self.peak = f64::NEG_INFINITY;
80 self.bars_under_water = 0;
81 self.seen = false;
82 }
83
84 fn warmup_period(&self) -> usize {
85 1
86 }
87
88 fn is_ready(&self) -> bool {
89 self.seen
90 }
91
92 fn name(&self) -> &'static str {
93 "DrawdownDuration"
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::traits::BatchExt;
101
102 #[test]
103 fn accessors_and_metadata() {
104 let mut d = DrawdownDuration::new();
105 assert_eq!(d.name(), "DrawdownDuration");
106 assert_eq!(d.warmup_period(), 1);
107 assert_eq!(d.value(), None);
108 d.update(100.0);
109 assert_eq!(d.value(), Some(0));
110 }
111
112 #[test]
113 fn first_bar_is_peak() {
114 let mut d = DrawdownDuration::new();
115 assert_eq!(d.update(100.0), Some(0));
116 }
117
118 #[test]
119 fn under_water_counter_increments() {
120 let mut d = DrawdownDuration::new();
121 d.update(100.0);
122 assert_eq!(d.update(90.0), Some(1));
123 assert_eq!(d.update(80.0), Some(2));
124 assert_eq!(d.update(85.0), Some(3));
125 }
126
127 #[test]
128 fn new_peak_resets_counter() {
129 let mut d = DrawdownDuration::new();
130 d.update(100.0);
131 d.update(90.0);
132 d.update(80.0);
133 assert_eq!(d.update(105.0), Some(0));
134 assert_eq!(d.update(95.0), Some(1));
135 }
136
137 #[test]
138 fn equal_value_is_treated_as_peak() {
139 let mut d = DrawdownDuration::new();
140 d.update(100.0);
141 assert_eq!(d.update(100.0), Some(0));
142 }
143
144 #[test]
145 fn ignores_non_finite_input() {
146 let mut d = DrawdownDuration::new();
147 d.update(100.0);
148 d.update(90.0);
149 let v = d.value();
150 assert_eq!(d.update(f64::NAN), v);
151 assert_eq!(d.update(f64::INFINITY), v);
152 }
153
154 #[test]
155 fn reset_clears_state() {
156 let mut d = DrawdownDuration::new();
157 d.batch(&[100.0, 90.0, 80.0]);
158 assert!(d.is_ready());
159 d.reset();
160 assert!(!d.is_ready());
161 assert_eq!(d.update(100.0), Some(0));
162 }
163
164 #[test]
165 fn batch_equals_streaming() {
166 let prices: Vec<f64> = (0..30)
167 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
168 .collect();
169 let batch = DrawdownDuration::new().batch(&prices);
170 let mut s = DrawdownDuration::new();
171 let streamed: Vec<_> = prices.iter().map(|p| s.update(*p)).collect();
172 assert_eq!(batch, streamed);
173 }
174}