velora_ta/volatility/
std_dev.rs

1//! Standard Deviation
2//!
3//! Standard deviation measures the dispersion of prices around their mean.
4//! Higher values indicate more volatile prices.
5//!
6//! Formula:
7//! StdDev = sqrt(sum((price - mean)^2) / period)
8
9use chrono::{DateTime, Utc};
10
11use crate::{
12    traits::{Indicator, SingleIndicator},
13    utils::CircularBuffer,
14    IndicatorError, IndicatorResult,
15};
16
17/// Standard Deviation indicator.
18///
19/// Measures price dispersion around the mean.
20/// Higher values indicate higher volatility.
21///
22/// # Examples
23///
24/// ```
25/// use velora_ta::{StdDev, SingleIndicator};
26/// use chrono::Utc;
27///
28/// let mut std_dev = StdDev::new(20).unwrap();
29/// let timestamp = Utc::now();
30///
31/// for price in vec![100.0, 105.0, 95.0, 110.0, 90.0] {
32///     if let Some(value) = std_dev.update(price, timestamp).unwrap() {
33///         println!("StdDev: {:.2}", value);
34///     }
35/// }
36/// ```
37#[derive(Debug, Clone)]
38pub struct StdDev {
39    period: usize,
40    buffer: CircularBuffer<f64>,
41    name: String,
42}
43
44impl StdDev {
45    /// Creates a new Standard Deviation indicator.
46    ///
47    /// # Arguments
48    ///
49    /// * `period` - Number of periods for calculation (must be > 1)
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if period is 0 or 1.
54    pub fn new(period: usize) -> IndicatorResult<Self> {
55        if period <= 1 {
56            return Err(IndicatorError::InvalidParameter(
57                "Period must be greater than 1".to_string(),
58            ));
59        }
60
61        Ok(StdDev {
62            period,
63            buffer: CircularBuffer::new(period),
64            name: format!("StdDev({period})"),
65        })
66    }
67
68    /// Get current standard deviation value.
69    fn calculate_std_dev(&self) -> Option<f64> {
70        self.buffer.std_dev()
71    }
72}
73
74impl Indicator for StdDev {
75    fn name(&self) -> &str {
76        &self.name
77    }
78
79    fn warmup_period(&self) -> usize {
80        self.period
81    }
82
83    fn is_ready(&self) -> bool {
84        self.buffer.is_full()
85    }
86
87    fn reset(&mut self) {
88        self.buffer.clear();
89    }
90}
91
92impl SingleIndicator for StdDev {
93    fn update(&mut self, price: f64, _timestamp: DateTime<Utc>) -> IndicatorResult<Option<f64>> {
94        if !price.is_finite() {
95            return Err(IndicatorError::InvalidPrice(
96                "Price must be a finite number".to_string(),
97            ));
98        }
99
100        self.buffer.push(price);
101        Ok(self.calculate_std_dev())
102    }
103
104    fn current(&self) -> Option<f64> {
105        self.calculate_std_dev()
106    }
107
108    fn calculate(&self, prices: &[f64]) -> IndicatorResult<Vec<Option<f64>>> {
109        if prices.is_empty() {
110            return Ok(Vec::new());
111        }
112
113        let mut std_dev = Self::new(self.period)?;
114        let mut result = Vec::with_capacity(prices.len());
115        let timestamp = Utc::now();
116
117        for &price in prices {
118            result.push(std_dev.update(price, timestamp)?);
119        }
120
121        Ok(result)
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_std_dev_creation() {
131        let std_dev = StdDev::new(20).unwrap();
132        assert_eq!(std_dev.warmup_period(), 20);
133        assert!(!std_dev.is_ready());
134        assert_eq!(std_dev.name(), "StdDev(20)");
135    }
136
137    #[test]
138    fn test_std_dev_invalid_period() {
139        assert!(StdDev::new(0).is_err());
140        assert!(StdDev::new(1).is_err());
141    }
142
143    #[test]
144    fn test_std_dev_no_volatility() {
145        let mut std_dev = StdDev::new(5).unwrap();
146        let timestamp = Utc::now();
147
148        // All same price = no volatility
149        for _ in 0..5 {
150            std_dev.update(100.0, timestamp).unwrap();
151        }
152
153        let value = std_dev.current().unwrap();
154        assert_eq!(value, 0.0);
155    }
156
157    #[test]
158    fn test_std_dev_high_volatility() {
159        let mut std_dev = StdDev::new(5).unwrap();
160        let timestamp = Utc::now();
161
162        // High variation in prices
163        let prices = vec![90.0, 110.0, 85.0, 115.0, 95.0];
164
165        for &price in &prices {
166            std_dev.update(price, timestamp).unwrap();
167        }
168
169        let value = std_dev.current().unwrap();
170        // Should have significant standard deviation
171        assert!(value > 10.0);
172    }
173
174    #[test]
175    fn test_std_dev_batch_calculation() {
176        let std_dev = StdDev::new(5).unwrap();
177        let prices = vec![100.0, 102.0, 104.0, 106.0, 108.0, 110.0];
178        let values = std_dev.calculate(&prices).unwrap();
179
180        assert_eq!(values.len(), 6);
181
182        // CircularBuffer can calculate std_dev before being full,
183        // so we just verify we get values eventually
184        // The last values should definitely have results
185        assert!(values[4].is_some());
186        assert!(values[5].is_some());
187    }
188
189    #[test]
190    fn test_std_dev_reset() {
191        let mut std_dev = StdDev::new(5).unwrap();
192        let timestamp = Utc::now();
193
194        for i in 1..=10 {
195            std_dev.update(i as f64, timestamp).unwrap();
196        }
197
198        assert!(std_dev.is_ready());
199
200        std_dev.reset();
201        assert!(!std_dev.is_ready());
202    }
203}