Skip to main content

finance_query/indicators/
macd.rs

1//! Moving Average Convergence Divergence (MACD) indicator.
2
3use super::{IndicatorError, Result, ema::ema};
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    // Calculate fast and slow EMAs
76    let fast_ema = ema(data, fast_period);
77    let slow_ema = ema(data, slow_period);
78
79    // Calculate MACD line (fast - slow)
80    let mut macd_line = Vec::with_capacity(data.len());
81    for i in 0..data.len() {
82        match (fast_ema[i], slow_ema[i]) {
83            (Some(fast), Some(slow)) => macd_line.push(Some(fast - slow)),
84            _ => macd_line.push(None),
85        }
86    }
87
88    // Extract non-None MACD values for signal line calculation
89    let macd_values: Vec<f64> = macd_line.iter().filter_map(|&v| v).collect();
90
91    // Calculate signal line (EMA of MACD line)
92    let signal_ema = ema(&macd_values, signal_period);
93
94    // Map signal EMA back to full length vector
95    let mut signal_line = vec![None; data.len()];
96    let mut signal_idx = 0;
97    for i in 0..data.len() {
98        if macd_line[i].is_some() {
99            signal_line[i] = signal_ema.get(signal_idx).copied().flatten();
100            signal_idx += 1;
101        }
102    }
103
104    // Calculate histogram (MACD - Signal)
105    let mut histogram = Vec::with_capacity(data.len());
106    for i in 0..data.len() {
107        match (macd_line[i], signal_line[i]) {
108            (Some(macd), Some(signal)) => histogram.push(Some(macd - signal)),
109            _ => histogram.push(None),
110        }
111    }
112
113    Ok(MacdResult {
114        macd_line,
115        signal_line,
116        histogram,
117    })
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_macd_basic() {
126        let data: Vec<f64> = (1..=50).map(|x| x as f64).collect();
127        let result = macd(&data, 12, 26, 9).unwrap();
128
129        assert_eq!(result.macd_line.len(), 50);
130        assert_eq!(result.signal_line.len(), 50);
131        assert_eq!(result.histogram.len(), 50);
132
133        // Early values should be None
134        assert!(result.macd_line[0].is_none());
135        assert!(result.signal_line[0].is_none());
136        assert!(result.histogram[0].is_none());
137
138        // Later values should have data
139        assert!(result.macd_line[40].is_some());
140    }
141
142    #[test]
143    fn test_macd_invalid_periods() {
144        let data: Vec<f64> = (1..=50).map(|x| x as f64).collect();
145
146        // Fast period >= slow period
147        let result = macd(&data, 26, 12, 9);
148        assert!(result.is_err());
149
150        // Zero period
151        let result = macd(&data, 0, 26, 9);
152        assert!(result.is_err());
153    }
154
155    #[test]
156    fn test_macd_insufficient_data() {
157        let data = vec![1.0, 2.0, 3.0];
158        let result = macd(&data, 12, 26, 9);
159
160        assert!(result.is_err());
161    }
162}