Skip to main content

wickra_core/indicators/
drawdown_duration.rs

1//! Drawdown Duration — bars since the last all-time peak ("time under water").
2
3use crate::traits::Indicator;
4
5/// Cumulative drawdown duration in bars.
6///
7/// Each `update` receives one equity-curve sample. The indicator tracks the
8/// **running all-time peak** seen since construction (or last `reset`) and
9/// reports how many bars have elapsed since that peak was set:
10///
11/// ```text
12/// peak_t        = max(input over [0..=t])
13/// duration_t    = bars elapsed since peak_t was first set
14/// ```
15///
16/// A new peak resets the duration to `0`. As long as the series stays under
17/// water the duration grows linearly with each bar.
18///
19/// The indicator emits a value on every bar (no warmup beyond the first
20/// input) and runs in O(1) per `update`.
21///
22/// # Example
23///
24/// ```
25/// use wickra_core::{DrawdownDuration, Indicator};
26///
27/// let mut dd = DrawdownDuration::new();
28/// assert_eq!(dd.update(100.0), Some(0));        // first bar -> new peak
29/// assert_eq!(dd.update(95.0), Some(1));         // 1 bar under water
30/// assert_eq!(dd.update(90.0), Some(2));         // 2 bars under water
31/// assert_eq!(dd.update(110.0), Some(0));        // new peak -> reset
32/// ```
33#[derive(Debug, Clone, Default)]
34pub struct DrawdownDuration {
35    peak: f64,
36    bars_under_water: u32,
37    seen: bool,
38}
39
40impl DrawdownDuration {
41    /// Construct a new Drawdown Duration tracker.
42    pub const fn new() -> Self {
43        Self {
44            peak: f64::NEG_INFINITY,
45            bars_under_water: 0,
46            seen: false,
47        }
48    }
49
50    /// Bars elapsed since the running all-time peak was set.
51    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        // The rejected input must not have disturbed the state.
157        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}