Skip to main content

quant_indicators/
macd.rs

1//! Moving Average Convergence Divergence (MACD) indicator.
2
3use quant_primitives::Candle;
4
5use crate::ema::Ema;
6use crate::error::IndicatorError;
7use crate::indicator::Indicator;
8use crate::series::Series;
9
10/// MACD indicator.
11///
12/// Measures the relationship between two EMAs. Produces three outputs:
13/// - MACD line: fast EMA - slow EMA
14/// - Signal line: EMA of MACD line
15/// - Histogram: MACD - Signal
16///
17/// This implementation returns the MACD line. Use `MacdSignal` for signal line
18/// or `MacdHistogram` for histogram.
19///
20/// # Standard Parameters
21///
22/// - Fast: 12
23/// - Slow: 26
24/// - Signal: 9
25///
26/// # Example
27///
28/// ```
29/// use quant_indicators::{Indicator, Macd};
30/// use quant_primitives::Candle;
31/// use chrono::Utc;
32/// use rust_decimal_macros::dec;
33///
34/// let ts = Utc::now();
35/// let candles: Vec<Candle> = (0..30).map(|i| {
36///     Candle::new(dec!(100), dec!(110), dec!(90), dec!(100) + rust_decimal::Decimal::from(i), dec!(1000), ts).unwrap()
37/// }).collect();
38/// let macd = Macd::new(12, 26).unwrap();
39/// let series = macd.compute(&candles).unwrap();
40/// ```
41#[derive(Debug, Clone)]
42pub struct Macd {
43    fast_period: usize,
44    slow_period: usize,
45    name: String,
46}
47
48impl Macd {
49    /// Create a new MACD indicator.
50    ///
51    /// # Arguments
52    ///
53    /// * `fast_period` - Period for fast EMA (typically 12)
54    /// * `slow_period` - Period for slow EMA (typically 26)
55    ///
56    /// # Errors
57    ///
58    /// Returns `InvalidParameter` if fast >= slow or periods are 0.
59    pub fn new(fast_period: usize, slow_period: usize) -> Result<Self, IndicatorError> {
60        if fast_period == 0 || slow_period == 0 {
61            return Err(IndicatorError::InvalidParameter {
62                message: "MACD periods must be > 0".to_string(),
63            });
64        }
65        if fast_period >= slow_period {
66            return Err(IndicatorError::InvalidParameter {
67                message: format!(
68                    "MACD fast period ({}) must be < slow period ({})",
69                    fast_period, slow_period
70                ),
71            });
72        }
73        Ok(Self {
74            fast_period,
75            slow_period,
76            name: format!("MACD({},{})", fast_period, slow_period),
77        })
78    }
79
80    /// Create MACD with standard parameters (12, 26).
81    pub fn standard() -> Result<Self, IndicatorError> {
82        Self::new(12, 26)
83    }
84}
85
86impl Indicator for Macd {
87    fn name(&self) -> &str {
88        &self.name
89    }
90
91    fn warmup_period(&self) -> usize {
92        self.slow_period
93    }
94
95    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
96        if candles.len() < self.slow_period {
97            return Err(IndicatorError::InsufficientData {
98                required: self.slow_period,
99                actual: candles.len(),
100            });
101        }
102
103        let fast_ema = Ema::new(self.fast_period)?;
104        let slow_ema = Ema::new(self.slow_period)?;
105
106        let fast_series = fast_ema.compute(candles)?;
107        let slow_series = slow_ema.compute(candles)?;
108
109        // Align: slow EMA starts later
110        let offset = self.slow_period - self.fast_period;
111        let fast_values = fast_series.values();
112        let slow_values = slow_series.values();
113
114        let mut values = Vec::with_capacity(slow_values.len());
115        for (i, (ts, slow_val)) in slow_values.iter().enumerate() {
116            let fast_val = fast_values[i + offset].1;
117            let macd = fast_val - *slow_val;
118            values.push((*ts, macd));
119        }
120
121        Ok(Series::new(values))
122    }
123}
124
125#[cfg(test)]
126#[path = "macd_tests.rs"]
127mod tests;