Skip to main content

mantis_ta/indicators/volatility/
stddev.rs

1use crate::indicators::Indicator;
2use crate::types::Candle;
3use crate::utils::ringbuf::RingBuf;
4
5/// Standard Deviation of closing prices.
6///
7/// Measures the volatility of price movements over a specified period.
8///
9/// # Examples
10/// ```rust
11/// use mantis_ta::indicators::{Indicator, StdDev};
12/// use mantis_ta::types::Candle;
13///
14/// let candles: Vec<Candle> = [1.0, 2.0, 3.0, 4.0]
15///     .iter()
16///     .enumerate()
17///     .map(|(i, c)| Candle {
18///         timestamp: i as i64,
19///         open: *c,
20///         high: *c,
21///         low: *c,
22///         close: *c,
23///         volume: 0.0,
24///     })
25///     .collect();
26///
27/// let values = StdDev::new(3).calculate(&candles);
28/// assert_eq!(values[0], None);
29/// assert_eq!(values[1], None);
30/// assert!(values[2].is_some());
31/// assert!(values[3].is_some());
32/// ```
33#[derive(Debug, Clone)]
34pub struct StdDev {
35    period: usize,
36    window: RingBuf<f64>,
37}
38
39impl StdDev {
40    pub fn new(period: usize) -> Self {
41        assert!(period > 0, "period must be > 0");
42        Self {
43            period,
44            window: RingBuf::new(period, 0.0),
45        }
46    }
47
48    #[inline]
49    fn update(&mut self, value: f64) -> Option<f64> {
50        self.window.push(value);
51
52        if self.window.len() < self.period {
53            return None;
54        }
55
56        let mean = self.window.iter().sum::<f64>() / self.period as f64;
57        let variance = self
58            .window
59            .iter()
60            .map(|v| {
61                let diff = v - mean;
62                diff * diff
63            })
64            .sum::<f64>()
65            / self.period as f64;
66
67        Some(variance.sqrt())
68    }
69}
70
71impl Indicator for StdDev {
72    type Output = f64;
73
74    fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
75        self.update(candle.close)
76    }
77
78    fn reset(&mut self) {
79        self.window = RingBuf::new(self.period, 0.0);
80    }
81
82    fn warmup_period(&self) -> usize {
83        self.period
84    }
85
86    fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
87        Box::new(self.clone())
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn computes_stddev_after_warmup() {
97        let mut stddev = StdDev::new(3);
98        let candles = [1.0, 2.0, 3.0, 4.0]
99            .iter()
100            .map(|c| Candle {
101                timestamp: 0,
102                open: *c,
103                high: *c,
104                low: *c,
105                close: *c,
106                volume: 0.0,
107            })
108            .collect::<Vec<_>>();
109
110        let mut outputs = Vec::new();
111        for c in &candles {
112            outputs.push(stddev.next(c));
113        }
114
115        assert_eq!(outputs[0], None);
116        assert_eq!(outputs[1], None);
117        // StdDev([1,2,3]): mean=2, variance=((1-2)^2 + (2-2)^2 + (3-2)^2)/3 = 2/3, stddev=sqrt(2/3)≈0.8165
118        assert!(outputs[2].is_some());
119        assert!((outputs[2].unwrap() - 0.8165).abs() < 0.001);
120        assert!(outputs[3].is_some());
121    }
122
123    #[test]
124    fn stddev_reset_clears_state() {
125        let mut stddev = StdDev::new(3);
126        let candle = Candle {
127            timestamp: 0,
128            open: 1.0,
129            high: 1.0,
130            low: 1.0,
131            close: 1.0,
132            volume: 0.0,
133        };
134
135        stddev.next(&candle);
136        stddev.next(&candle);
137        stddev.next(&candle);
138        assert!(stddev.next(&candle).is_some());
139
140        stddev.reset();
141        assert_eq!(stddev.next(&candle), None);
142    }
143
144    #[test]
145    fn stddev_with_constant_values() {
146        let mut stddev = StdDev::new(3);
147        let candles = [5.0, 5.0, 5.0]
148            .iter()
149            .map(|c| Candle {
150                timestamp: 0,
151                open: *c,
152                high: *c,
153                low: *c,
154                close: *c,
155                volume: 0.0,
156            })
157            .collect::<Vec<_>>();
158
159        let outputs: Vec<_> = candles.iter().map(|c| stddev.next(c)).collect();
160        assert_eq!(outputs[0], None);
161        assert_eq!(outputs[1], None);
162        assert_eq!(outputs[2], Some(0.0));
163    }
164
165    #[test]
166    fn stddev_warmup_period() {
167        let stddev = StdDev::new(5);
168        assert_eq!(stddev.warmup_period(), 5);
169    }
170}