quant-indicators 0.7.0

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

use quant_primitives::Candle;

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

/// MACD indicator.
///
/// Measures the relationship between two EMAs. Produces three outputs:
/// - MACD line: fast EMA - slow EMA
/// - Signal line: EMA of MACD line
/// - Histogram: MACD - Signal
///
/// This implementation returns the MACD line. Use `MacdSignal` for signal line
/// or `MacdHistogram` for histogram.
///
/// # Standard Parameters
///
/// - Fast: 12
/// - Slow: 26
/// - Signal: 9
///
/// # Example
///
/// ```
/// use quant_indicators::{Indicator, Macd};
/// use quant_primitives::Candle;
/// use chrono::Utc;
/// use rust_decimal_macros::dec;
///
/// let ts = Utc::now();
/// let candles: Vec<Candle> = (0..30).map(|i| {
///     Candle::new(dec!(100), dec!(110), dec!(90), dec!(100) + rust_decimal::Decimal::from(i), dec!(1000), ts).unwrap()
/// }).collect();
/// let macd = Macd::new(12, 26).unwrap();
/// let series = macd.compute(&candles).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct Macd {
    fast_period: usize,
    slow_period: usize,
    name: String,
}

impl Macd {
    /// Create a new MACD indicator.
    ///
    /// # Arguments
    ///
    /// * `fast_period` - Period for fast EMA (typically 12)
    /// * `slow_period` - Period for slow EMA (typically 26)
    ///
    /// # Errors
    ///
    /// Returns `InvalidParameter` if fast >= slow or periods are 0.
    pub fn new(fast_period: usize, slow_period: usize) -> Result<Self, IndicatorError> {
        if fast_period == 0 || slow_period == 0 {
            return Err(IndicatorError::InvalidParameter {
                message: "MACD periods must be > 0".to_string(),
            });
        }
        if fast_period >= slow_period {
            return Err(IndicatorError::InvalidParameter {
                message: format!(
                    "MACD fast period ({}) must be < slow period ({})",
                    fast_period, slow_period
                ),
            });
        }
        Ok(Self {
            fast_period,
            slow_period,
            name: format!("MACD({},{})", fast_period, slow_period),
        })
    }

    /// Create MACD with standard parameters (12, 26).
    pub fn standard() -> Result<Self, IndicatorError> {
        Self::new(12, 26)
    }
}

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

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

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

        let fast_ema = Ema::new(self.fast_period)?;
        let slow_ema = Ema::new(self.slow_period)?;

        let fast_series = fast_ema.compute(candles)?;
        let slow_series = slow_ema.compute(candles)?;

        // Align: slow EMA starts later
        let offset = self.slow_period - self.fast_period;
        let fast_values = fast_series.values();
        let slow_values = slow_series.values();

        let mut values = Vec::with_capacity(slow_values.len());
        for (i, (ts, slow_val)) in slow_values.iter().enumerate() {
            let fast_val = fast_values[i + offset].1;
            let macd = fast_val - *slow_val;
            values.push((*ts, macd));
        }

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

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