finance_solution/stocks/ta/
macd.rs1use crate::stocks::ta::common::{opt_cell, validate_series};
50use crate::stocks::ta::moving_average::ema;
51use crate::util::error::{FinanceError, FinanceResult};
52use crate::util::primitives::PeriodLength;
53use crate::{columns_with_strings, print_table_locale_opt};
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub struct MacdParams {
58 pub fast: usize,
59 pub slow: usize,
60 pub signal: usize,
61}
62
63impl MacdParams {
64 pub const fn standard() -> Self {
66 Self {
67 fast: 12,
68 slow: 26,
69 signal: 9,
70 }
71 }
72
73 pub const fn new(fast: usize, slow: usize, signal: usize) -> Self {
74 Self { fast, slow, signal }
75 }
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
80pub struct ValidatedMacd {
81 params: MacdParams,
82}
83
84impl ValidatedMacd {
85 pub fn new(params: MacdParams) -> FinanceResult<Self> {
86 PeriodLength::new(params.fast)?;
87 PeriodLength::new(params.slow)?;
88 PeriodLength::new(params.signal)?;
89 if params.fast >= params.slow {
90 return Err(FinanceError::Unsolvable {
91 message: "MACD requires fast period < slow period",
92 });
93 }
94 Ok(Self { params })
95 }
96
97 pub fn params(self) -> MacdParams {
98 self.params
99 }
100
101 pub fn compute(self, closes: &[f64]) -> FinanceResult<MacdSeries> {
102 macd_validated(closes, self)
103 }
104}
105
106#[derive(Clone, Debug, PartialEq)]
108pub struct MacdSeries {
109 pub macd: Vec<Option<f64>>,
110 pub signal: Vec<Option<f64>>,
111 pub histogram: Vec<Option<f64>>,
112 pub params: MacdParams,
113}
114
115impl MacdSeries {
116 pub fn last(&self) -> Option<(f64, f64, f64)> {
117 let m = self.macd.iter().rev().find_map(|x| *x)?;
118 let s = self.signal.iter().rev().find_map(|x| *x)?;
119 let h = self.histogram.iter().rev().find_map(|x| *x)?;
120 Some((m, s, h))
121 }
122}
123
124#[derive(Clone, Debug)]
126pub struct MacdSolution {
127 series: MacdSeries,
128 closes: Vec<f64>,
129 formula: String,
130 symbolic_formula: String,
131}
132
133impl MacdSolution {
134 pub fn series(&self) -> &MacdSeries {
135 &self.series
136 }
137 pub fn formula(&self) -> &str {
138 &self.formula
139 }
140 pub fn symbolic_formula(&self) -> &str {
141 &self.symbolic_formula
142 }
143 pub fn params(&self) -> MacdParams {
144 self.series.params
145 }
146
147 pub fn print_table(&self) {
158 self.print_table_locale_opt(None, None);
159 }
160
161 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
162 self.print_table_locale_opt(Some(locale), Some(precision));
163 }
164
165 fn print_table_locale_opt(
166 &self,
167 locale: Option<&num_format::Locale>,
168 precision: Option<usize>,
169 ) {
170 let columns = columns_with_strings(&[
171 ("period", "i", true),
172 ("close", "f", true),
173 ("macd", "f", true),
174 ("signal", "f", true),
175 ("hist", "f", true),
176 ]);
177 let data = self
178 .closes
179 .iter()
180 .enumerate()
181 .map(|(i, c)| {
182 vec![
183 i.to_string(),
184 c.to_string(),
185 opt_cell(self.series.macd[i]),
186 opt_cell(self.series.signal[i]),
187 opt_cell(self.series.histogram[i]),
188 ]
189 })
190 .collect();
191 print_table_locale_opt(&columns, data, locale, precision);
192 }
193}
194
195pub fn macd(closes: &[f64], params: MacdParams) -> FinanceResult<MacdSeries> {
197 ValidatedMacd::new(params)?.compute(closes)
198}
199
200pub fn macd_solution(closes: &[f64], params: MacdParams) -> FinanceResult<MacdSolution> {
211 let series = macd(closes, params)?;
212 let formula = format!(
213 "macd = EMA({}) - EMA({}); signal = EMA({})(macd); hist = macd - signal",
214 params.fast, params.slow, params.signal
215 );
216 let symbolic =
217 "macd = ema_fast(close) - ema_slow(close); signal = ema_signal(macd); hist = macd - signal"
218 .to_string();
219 Ok(MacdSolution {
220 series,
221 closes: closes.to_vec(),
222 formula,
223 symbolic_formula: symbolic,
224 })
225}
226
227fn macd_validated(closes: &[f64], v: ValidatedMacd) -> FinanceResult<MacdSeries> {
228 validate_series("close", closes)?;
229 let p = v.params;
230 let fast = ema(closes, p.fast)?;
231 let slow = ema(closes, p.slow)?;
232 let n = closes.len();
233 let mut macd_line = vec![None; n];
234 for i in 0..n {
235 match (fast[i], slow[i]) {
236 (Some(f), Some(s)) => macd_line[i] = Some(f - s),
237 _ => {}
238 }
239 }
240 let signal = ema_on_option_series(&macd_line, p.signal);
241 let mut histogram = vec![None; n];
242 for i in 0..n {
243 match (macd_line[i], signal[i]) {
244 (Some(m), Some(s)) => histogram[i] = Some(m - s),
245 _ => {}
246 }
247 }
248 Ok(MacdSeries {
249 macd: macd_line,
250 signal,
251 histogram,
252 params: p,
253 })
254}
255
256fn ema_on_option_series(data: &[Option<f64>], period: usize) -> Vec<Option<f64>> {
263 let n = data.len();
264 let mut out = vec![None; n];
265 if period == 0 {
266 return out;
267 }
268 let pts: Vec<(usize, f64)> = data
270 .iter()
271 .enumerate()
272 .filter_map(|(i, v)| v.map(|x| (i, x)))
273 .collect();
274 if pts.len() < period {
275 return out;
276 }
277 let mut sum = 0.0;
278 for j in 0..period {
279 sum += pts[j].1;
280 }
281 let mut prev = sum / period as f64;
282 let seed_idx = pts[period - 1].0;
283 out[seed_idx] = Some(prev);
284 let alpha = 2.0 / (period as f64 + 1.0);
285 for j in period..pts.len() {
286 prev = alpha * pts[j].1 + (1.0 - alpha) * prev;
287 out[pts[j].0] = Some(prev);
288 }
289 out
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn standard_runs() {
298 let c: Vec<_> = (1..=50).map(|x| 100.0 + x as f64 * 0.2).collect();
299 let s = macd(&c, MacdParams::standard()).unwrap();
300 assert_eq!(s.macd.len(), 50);
301 assert!(s.last().is_some());
302 }
303
304 #[test]
305 fn fast_not_lt_slow_err() {
306 assert!(ValidatedMacd::new(MacdParams::new(26, 12, 9)).is_err());
307 }
308
309 #[test]
310 fn histogram_is_macd_minus_signal() {
311 let c: Vec<_> = (1..=60).map(|x| 100.0 + x as f64 * 0.15).collect();
312 let s = macd(&c, MacdParams::standard()).unwrap();
313 for i in 0..s.macd.len() {
314 if let (Some(m), Some(sig), Some(h)) = (s.macd[i], s.signal[i], s.histogram[i]) {
315 assert!((h - (m - sig)).abs() < 1e-12);
316 }
317 }
318 }
319
320 #[test]
321 fn empty_err() {
322 assert!(macd(&[], MacdParams::standard()).is_err());
323 }
324}