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    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}