Skip to main content

quant_indicators/
stddev.rs

1//! Standard Deviation indicator.
2
3use quant_primitives::Candle;
4use rust_decimal::Decimal;
5
6use crate::error::IndicatorError;
7use crate::indicator::Indicator;
8use crate::series::Series;
9
10/// Standard Deviation indicator.
11///
12/// Computes the population standard deviation of closing prices
13/// over the specified period.
14///
15/// # Formula
16///
17/// StdDev = sqrt(sum((x - mean)^2) / n)
18///
19/// where x is each closing price, mean is the average, and n is the period.
20///
21/// # Example
22///
23/// ```
24/// use quant_indicators::{Indicator, StdDev};
25/// use quant_primitives::Candle;
26/// use chrono::Utc;
27/// use rust_decimal_macros::dec;
28///
29/// let ts = Utc::now();
30/// let candles: Vec<Candle> = (0..20).map(|i| {
31///     Candle::new(dec!(100), dec!(110), dec!(90), dec!(100) + rust_decimal::Decimal::from(i), dec!(1000), ts).unwrap()
32/// }).collect();
33/// let stddev = StdDev::new(20).unwrap();
34/// let series = stddev.compute(&candles).unwrap();
35/// ```
36#[derive(Debug, Clone)]
37pub struct StdDev {
38    period: usize,
39    name: String,
40}
41
42impl StdDev {
43    /// Create a new StdDev indicator with the specified period.
44    ///
45    /// # Errors
46    ///
47    /// Returns `InvalidParameter` if period is 0.
48    pub fn new(period: usize) -> Result<Self, IndicatorError> {
49        if period == 0 {
50            return Err(IndicatorError::InvalidParameter {
51                message: "StdDev period must be > 0".to_string(),
52            });
53        }
54        Ok(Self {
55            period,
56            name: format!("StdDev({})", period),
57        })
58    }
59
60    /// Get the period.
61    pub fn period(&self) -> usize {
62        self.period
63    }
64}
65
66impl Indicator for StdDev {
67    fn name(&self) -> &str {
68        &self.name
69    }
70
71    fn warmup_period(&self) -> usize {
72        self.period
73    }
74
75    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
76        if candles.len() < self.period {
77            return Err(IndicatorError::InsufficientData {
78                required: self.period,
79                actual: candles.len(),
80            });
81        }
82
83        let mut values = Vec::with_capacity(candles.len() - self.period + 1);
84        let period_dec = Decimal::from(self.period as u64);
85
86        for window in candles.windows(self.period) {
87            // Calculate mean
88            let sum: Decimal = window.iter().map(|c| c.close()).sum();
89            let mean = sum / period_dec;
90
91            // Calculate sum of squared differences
92            let variance_sum: Decimal = window
93                .iter()
94                .map(|c| {
95                    let diff = c.close() - mean;
96                    diff * diff
97                })
98                .sum();
99
100            // Population variance
101            let variance = variance_sum / period_dec;
102
103            // Standard deviation (sqrt of variance)
104            let stddev = decimal_sqrt(variance);
105
106            // Safe: windows(n) always yields slices of length n when n > 0
107            let ts = window[self.period - 1].timestamp();
108            values.push((ts, stddev));
109        }
110
111        Ok(Series::new(values))
112    }
113}
114
115/// Compute square root of a Decimal using Newton-Raphson method.
116pub(crate) fn decimal_sqrt(n: Decimal) -> Decimal {
117    if n.is_zero() {
118        return Decimal::ZERO;
119    }
120
121    if n.is_sign_negative() {
122        return Decimal::ZERO; // Invalid, but return 0 for safety
123    }
124
125    // Newton-Raphson: x_new = (x + n/x) / 2
126    let two = Decimal::TWO;
127    let epsilon = Decimal::new(1, 10); // 0.0000000001
128    let mut x = n; // Initial guess
129
130    // Iterate until convergence
131    for _ in 0..20 {
132        let x_new = (x + n / x) / two;
133        if (x_new - x).abs() < epsilon {
134            return x_new.round_dp(10);
135        }
136        x = x_new;
137    }
138
139    x.round_dp(10)
140}
141
142#[cfg(test)]
143#[path = "stddev_tests.rs"]
144mod tests;