quant-indicators 0.7.0

Pure indicator math library for trading — MA, RSI, Bollinger, MACD, ATR, HRP
Documentation
//! Standard Deviation indicator.

use quant_primitives::Candle;
use rust_decimal::Decimal;

use crate::error::IndicatorError;
use crate::indicator::Indicator;
use crate::series::Series;

/// Standard Deviation indicator.
///
/// Computes the population standard deviation of closing prices
/// over the specified period.
///
/// # Formula
///
/// StdDev = sqrt(sum((x - mean)^2) / n)
///
/// where x is each closing price, mean is the average, and n is the period.
///
/// # Example
///
/// ```
/// use quant_indicators::{Indicator, StdDev};
/// use quant_primitives::Candle;
/// use chrono::Utc;
/// use rust_decimal_macros::dec;
///
/// let ts = Utc::now();
/// let candles: Vec<Candle> = (0..20).map(|i| {
///     Candle::new(dec!(100), dec!(110), dec!(90), dec!(100) + rust_decimal::Decimal::from(i), dec!(1000), ts).unwrap()
/// }).collect();
/// let stddev = StdDev::new(20).unwrap();
/// let series = stddev.compute(&candles).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct StdDev {
    period: usize,
    name: String,
}

impl StdDev {
    /// Create a new StdDev indicator with the specified period.
    ///
    /// # Errors
    ///
    /// Returns `InvalidParameter` if period is 0.
    pub fn new(period: usize) -> Result<Self, IndicatorError> {
        if period == 0 {
            return Err(IndicatorError::InvalidParameter {
                message: "StdDev period must be > 0".to_string(),
            });
        }
        Ok(Self {
            period,
            name: format!("StdDev({})", period),
        })
    }

    /// Get the period.
    pub fn period(&self) -> usize {
        self.period
    }
}

impl Indicator for StdDev {
    fn name(&self) -> &str {
        &self.name
    }

    fn warmup_period(&self) -> usize {
        self.period
    }

    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
        if candles.len() < self.period {
            return Err(IndicatorError::InsufficientData {
                required: self.period,
                actual: candles.len(),
            });
        }

        let mut values = Vec::with_capacity(candles.len() - self.period + 1);
        let period_dec = Decimal::from(self.period as u64);

        for window in candles.windows(self.period) {
            // Calculate mean
            let sum: Decimal = window.iter().map(|c| c.close()).sum();
            let mean = sum / period_dec;

            // Calculate sum of squared differences
            let variance_sum: Decimal = window
                .iter()
                .map(|c| {
                    let diff = c.close() - mean;
                    diff * diff
                })
                .sum();

            // Population variance
            let variance = variance_sum / period_dec;

            // Standard deviation (sqrt of variance)
            let stddev = decimal_sqrt(variance);

            // Safe: windows(n) always yields slices of length n when n > 0
            let ts = window[self.period - 1].timestamp();
            values.push((ts, stddev));
        }

        Ok(Series::new(values))
    }
}

/// Compute square root of a Decimal using Newton-Raphson method.
pub(crate) fn decimal_sqrt(n: Decimal) -> Decimal {
    if n.is_zero() {
        return Decimal::ZERO;
    }

    if n.is_sign_negative() {
        return Decimal::ZERO; // Invalid, but return 0 for safety
    }

    // Newton-Raphson: x_new = (x + n/x) / 2
    let two = Decimal::TWO;
    let epsilon = Decimal::new(1, 10); // 0.0000000001
    let mut x = n; // Initial guess

    // Iterate until convergence
    for _ in 0..20 {
        let x_new = (x + n / x) / two;
        if (x_new - x).abs() < epsilon {
            return x_new.round_dp(10);
        }
        x = x_new;
    }

    x.round_dp(10)
}

#[cfg(test)]
#[path = "stddev_tests.rs"]
mod tests;