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