1use std::collections::HashMap;
19
20use crate::functions::{self};
21use crate::error::IndicatorError;
22use crate::indicator::{Indicator, IndicatorOutput, PriceColumn};
23use crate::types::Candle;
24
25#[derive(Debug, Clone)]
28pub struct MacdParams {
29 pub fast_period: usize,
31 pub slow_period: usize,
33 pub signal_period: usize,
35 pub column: PriceColumn,
37}
38
39impl Default for MacdParams {
40 fn default() -> Self {
41 Self {
42 fast_period: 12,
43 slow_period: 26,
44 signal_period: 9,
45 column: PriceColumn::Close,
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
53pub struct Macd {
54 pub params: MacdParams,
55}
56
57impl Macd {
58 pub fn new(params: MacdParams) -> Self {
59 Self { params }
60 }
61
62 pub fn default() -> Self {
63 Self::new(MacdParams::default())
64 }
65}
66
67impl Indicator for Macd {
68 fn name(&self) -> &str { "MACD" }
69
70 fn required_len(&self) -> usize {
71 self.params.slow_period + self.params.signal_period
73 }
74
75 fn required_columns(&self) -> &[&'static str] { &["close"] }
76
77 fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
81 self.check_len(candles)?;
82
83 let prices = self.params.column.extract(candles);
84 let (macd_line, signal_line, histogram) = functions::macd(
85 &prices,
86 self.params.fast_period,
87 self.params.slow_period,
88 self.params.signal_period,
89 )?;
90
91 Ok(IndicatorOutput::from_pairs([
92 ("MACD_line".to_string(), macd_line),
93 ("MACD_signal".to_string(), signal_line),
94 ("MACD_histogram".to_string(), histogram),
95 ]))
96 }
97}
98
99pub fn factory(params: &HashMap<String, String>) -> Result<Box<dyn Indicator>, IndicatorError> {
102 Ok(Box::new(Macd::new(MacdParams {
103 fast_period: crate::registry::param_usize(params, "fast_period", 12)?,
104 slow_period: crate::registry::param_usize(params, "slow_period", 26)?,
105 signal_period: crate::registry::param_usize(params, "signal_period", 9)?,
106 column: PriceColumn::Close,
107 })))
108}
109
110#[cfg(test)]
113mod tests {
114 use super::*;
115
116 fn candles(closes: &[f64]) -> Vec<Candle> {
117 closes.iter().enumerate().map(|(i, &c)| Candle {
118 time: i as i64, open: c, high: c, low: c, close: c, volume: 1.0,
119 }).collect()
120 }
121
122 #[test]
123 fn macd_insufficient_data() {
124 let macd = Macd::default();
125 assert!(macd.calculate(&candles(&[1.0; 10])).is_err());
126 }
127
128 #[test]
129 fn macd_output_has_three_columns() {
130 let macd = Macd::default();
131 let closes: Vec<f64> = (1..=50).map(|x| x as f64).collect();
132 let out = macd.calculate(&candles(&closes)).unwrap();
133 assert!(out.get("MACD_line").is_some(), "missing MACD_line");
134 assert!(out.get("MACD_signal").is_some(), "missing MACD_signal");
135 assert!(out.get("MACD_histogram").is_some(), "missing MACD_histogram");
136 }
137
138 #[test]
139 fn macd_histogram_is_line_minus_signal() {
140 let macd = Macd::default();
141 let closes: Vec<f64> = (1..=50).map(|x| x as f64).collect();
142 let out = macd.calculate(&candles(&closes)).unwrap();
143 let line = out.get("MACD_line").unwrap();
144 let signal = out.get("MACD_signal").unwrap();
145 let hist = out.get("MACD_histogram").unwrap();
146 for i in 0..line.len() {
147 if !line[i].is_nan() && !signal[i].is_nan() {
148 assert!((hist[i] - (line[i] - signal[i])).abs() < 1e-9);
149 }
150 }
151 }
152
153 #[test]
154 fn factory_creates_macd() {
155 let params = HashMap::new();
156 let ind = factory(¶ms).unwrap();
157 assert_eq!(ind.name(), "MACD");
158 }
159}