quant-indicators 0.7.0

Pure indicator math library for trading — MA, RSI, Bollinger, MACD, ATR, HRP
Documentation
//! Exponential Moving Average (EMA) indicator.

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

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

/// Exponential Moving Average indicator.
///
/// Computes a weighted moving average that gives more weight to recent prices.
/// The weighting decreases exponentially for older prices.
///
/// # Formula
///
/// multiplier = 2 / (period + 1)
/// EMA_today = (Close - EMA_yesterday) * multiplier + EMA_yesterday
///
/// The first EMA value is the SMA of the first `period` candles.
///
/// # Example
///
/// ```
/// use quant_indicators::{Indicator, Ema};
/// 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 ema = Ema::new(20).unwrap();
/// let series = ema.compute(&candles).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct Ema {
    period: usize,
    multiplier: Decimal,
    name: String,
}

impl Ema {
    /// Create a new EMA 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: "EMA period must be > 0".to_string(),
            });
        }

        // multiplier = 2 / (period + 1)
        let multiplier = Decimal::TWO / Decimal::from(period as u64 + 1);

        Ok(Self {
            period,
            multiplier,
            name: format!("EMA({})", period),
        })
    }

    /// Get the smoothing multiplier.
    pub fn multiplier(&self) -> Decimal {
        self.multiplier
    }
}

impl Indicator for Ema {
    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);

        // First EMA is SMA of first `period` candles
        let initial_sum: Decimal = candles[..self.period].iter().map(|c| c.close()).sum();
        let mut ema = initial_sum / period_dec;
        let ts = candles[self.period - 1].timestamp();
        values.push((ts, ema));

        // Subsequent EMAs use the recursive formula
        for candle in candles.iter().skip(self.period) {
            let close = candle.close();
            // EMA = (Close - EMA_prev) * multiplier + EMA_prev
            ema = (close - ema) * self.multiplier + ema;
            values.push((candle.timestamp(), ema));
        }

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

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