1use std::collections::HashMap;
19
20use crate::error::IndicatorError;
21use crate::functions::{self};
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
63impl Default for Macd {
64 fn default() -> Self {
65 Self::new(MacdParams::default())
66 }
67}
68
69impl Indicator for Macd {
70 fn name(&self) -> &'static str {
71 "MACD"
72 }
73
74 fn required_len(&self) -> usize {
75 self.params.slow_period
79 }
80
81 fn required_columns(&self) -> &[&'static str] {
82 &["close"]
83 }
84
85 fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
90 self.check_len(candles)?;
91
92 let prices = self.params.column.extract(candles);
93 let (macd_line, signal_line, histogram) = functions::macd(
94 &prices,
95 self.params.fast_period,
96 self.params.slow_period,
97 self.params.signal_period,
98 )?;
99
100 Ok(IndicatorOutput::from_pairs([
101 ("MACD_line".to_string(), macd_line),
102 ("MACD_signal".to_string(), signal_line),
103 ("MACD_histogram".to_string(), histogram),
104 ]))
105 }
106}
107
108pub fn factory<S: ::std::hash::BuildHasher>(
111 params: &HashMap<String, String, S>,
112) -> Result<Box<dyn Indicator>, IndicatorError> {
113 Ok(Box::new(Macd::new(MacdParams {
114 fast_period: crate::registry::param_usize(params, "fast_period", 12)?,
115 slow_period: crate::registry::param_usize(params, "slow_period", 26)?,
116 signal_period: crate::registry::param_usize(params, "signal_period", 9)?,
117 column: PriceColumn::Close,
118 })))
119}
120
121#[cfg(test)]
124mod tests {
125 use super::*;
126
127 fn candles(closes: &[f64]) -> Vec<Candle> {
128 closes
129 .iter()
130 .enumerate()
131 .map(|(i, &c)| Candle {
132 time: i64::try_from(i).expect("time index fits i64"),
133 open: c,
134 high: c,
135 low: c,
136 close: c,
137 volume: 1.0,
138 })
139 .collect()
140 }
141
142 #[test]
143 fn macd_insufficient_data() {
144 let macd = Macd::default();
145 assert!(macd.calculate(&candles(&[1.0; 10])).is_err());
146 }
147
148 #[test]
149 fn macd_output_has_three_columns() {
150 let macd = Macd::default();
151 let closes: Vec<f64> = (1..=50).map(|x| x as f64).collect();
152 let out = macd.calculate(&candles(&closes)).unwrap();
153 assert!(out.get("MACD_line").is_some(), "missing MACD_line");
154 assert!(out.get("MACD_signal").is_some(), "missing MACD_signal");
155 assert!(
156 out.get("MACD_histogram").is_some(),
157 "missing MACD_histogram"
158 );
159 }
160
161 #[test]
162 fn macd_histogram_is_line_minus_signal() {
163 let macd = Macd::default();
164 let closes: Vec<f64> = (1..=50).map(|x| x as f64).collect();
165 let out = macd.calculate(&candles(&closes)).unwrap();
166 let line = out.get("MACD_line").unwrap();
167 let signal = out.get("MACD_signal").unwrap();
168 let hist = out.get("MACD_histogram").unwrap();
169 for i in 0..line.len() {
170 if !line[i].is_nan() && !signal[i].is_nan() {
171 assert!((hist[i] - (line[i] - signal[i])).abs() < 1e-9);
172 }
173 }
174 }
175
176 #[test]
177 fn factory_creates_macd() {
178 let params = HashMap::new();
179 let ind = factory(¶ms).unwrap();
180 assert_eq!(ind.name(), "MACD");
181 }
182}