Skip to main content

finance_query/indicators/
macd.rs

1//! Moving Average Convergence Divergence (MACD) indicator.
2
3use super::{IndicatorError, Result, ema::ema_raw};
4use serde::{Deserialize, Serialize};
5
6/// MACD calculation result containing the MACD line, signal line, and histogram.
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct MacdResult {
9    /// MACD line (fast EMA - slow EMA)
10    pub macd_line: Vec<Option<f64>>,
11
12    /// Signal line (EMA of MACD line)
13    pub signal_line: Vec<Option<f64>>,
14
15    /// Histogram (MACD line - signal line)
16    pub histogram: Vec<Option<f64>>,
17}
18
19/// Calculate Moving Average Convergence Divergence (MACD).
20///
21/// MACD shows the relationship between two moving averages and helps identify trend changes.
22/// Standard parameters are (12, 26, 9).
23///
24/// # Arguments
25///
26/// * `data` - Price data (typically close prices)
27/// * `fast_period` - Fast EMA period (typically 12)
28/// * `slow_period` - Slow EMA period (typically 26)
29/// * `signal_period` - Signal line EMA period (typically 9)
30///
31/// # Formula
32///
33/// - MACD Line = 12-period EMA - 26-period EMA
34/// - Signal Line = 9-period EMA of MACD Line
35/// - Histogram = MACD Line - Signal Line
36///
37/// # Example
38///
39/// ```
40/// use finance_query::indicators::macd;
41///
42/// let prices: Vec<f64> = (1..=50).map(|x| x as f64).collect();
43/// let result = macd(&prices, 12, 26, 9).unwrap();
44///
45/// assert_eq!(result.macd_line.len(), prices.len());
46/// assert_eq!(result.signal_line.len(), prices.len());
47/// assert_eq!(result.histogram.len(), prices.len());
48/// ```
49pub fn macd(
50    data: &[f64],
51    fast_period: usize,
52    slow_period: usize,
53    signal_period: usize,
54) -> Result<MacdResult> {
55    if fast_period == 0 || slow_period == 0 || signal_period == 0 {
56        return Err(IndicatorError::InvalidPeriod(
57            "All periods must be greater than 0".to_string(),
58        ));
59    }
60
61    if fast_period >= slow_period {
62        return Err(IndicatorError::InvalidPeriod(
63            "Fast period must be less than slow period".to_string(),
64        ));
65    }
66
67    let min_data_points = slow_period + signal_period;
68    if data.len() < min_data_points {
69        return Err(IndicatorError::InsufficientData {
70            need: min_data_points,
71            got: data.len(),
72        });
73    }
74
75    // Compute EMAs using raw variant (no Option/None padding)
76    let fast_raw = ema_raw(data, fast_period); // len = N - (fast_period-1)
77    let slow_raw = ema_raw(data, slow_period); // len = N - (slow_period-1)
78
79    // MACD line: fast - slow, valid from original index slow_period - 1
80    // fast_raw[k + (slow_period - fast_period)] aligns with slow_raw[k]
81    let shift = slow_period - fast_period;
82    let macd_values: Vec<f64> = slow_raw
83        .iter()
84        .enumerate()
85        .map(|(k, &s)| fast_raw[k + shift] - s)
86        .collect();
87
88    // Signal line: EMA of MACD values
89    let signal_raw = ema_raw(&macd_values, signal_period);
90
91    // Build full-length output vectors
92    let macd_start = slow_period - 1;
93    let signal_start = macd_start + signal_period - 1;
94    let n = data.len();
95
96    let mut macd_line = vec![None; n];
97    let mut signal_line = vec![None; n];
98    let mut histogram = vec![None; n];
99
100    for (k, &mv) in macd_values.iter().enumerate() {
101        let i = k + macd_start;
102        macd_line[i] = Some(mv);
103    }
104    for (k, &sv) in signal_raw.iter().enumerate() {
105        let i = k + signal_start;
106        signal_line[i] = Some(sv);
107        if let Some(mv) = macd_line[i] {
108            histogram[i] = Some(mv - sv);
109        }
110    }
111
112    Ok(MacdResult {
113        macd_line,
114        signal_line,
115        histogram,
116    })
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_macd_basic() {
125        let data: Vec<f64> = (1..=50).map(|x| x as f64).collect();
126        let result = macd(&data, 12, 26, 9).unwrap();
127
128        assert_eq!(result.macd_line.len(), 50);
129        assert_eq!(result.signal_line.len(), 50);
130        assert_eq!(result.histogram.len(), 50);
131
132        // Early values should be None
133        assert!(result.macd_line[0].is_none());
134        assert!(result.signal_line[0].is_none());
135        assert!(result.histogram[0].is_none());
136
137        // Later values should have data
138        assert!(result.macd_line[40].is_some());
139    }
140
141    #[test]
142    fn test_macd_invalid_periods() {
143        let data: Vec<f64> = (1..=50).map(|x| x as f64).collect();
144
145        // Fast period >= slow period
146        let result = macd(&data, 26, 12, 9);
147        assert!(result.is_err());
148
149        // Zero period
150        let result = macd(&data, 0, 26, 9);
151        assert!(result.is_err());
152    }
153
154    #[test]
155    fn test_macd_insufficient_data() {
156        let data = vec![1.0, 2.0, 3.0];
157        let result = macd(&data, 12, 26, 9);
158
159        assert!(result.is_err());
160    }
161}