finance_solution/stocks/ta/
macd.rs1use crate::stocks::ta::common::{opt_cell, validate_series};
52use crate::stocks::ta::state::MacdState;
53use crate::util::error::{FinanceError, FinanceResult};
54use crate::util::primitives::PeriodLength;
55use crate::{columns_with_strings, print_table_locale_opt};
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
59pub struct MacdParams {
60 pub fast: usize,
61 pub slow: usize,
62 pub signal: usize,
63}
64
65impl MacdParams {
66 pub const fn standard() -> Self {
68 Self {
69 fast: 12,
70 slow: 26,
71 signal: 9,
72 }
73 }
74
75 pub const fn new(fast: usize, slow: usize, signal: usize) -> Self {
76 Self { fast, slow, signal }
77 }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
82pub struct ValidatedMacd {
83 params: MacdParams,
84}
85
86impl ValidatedMacd {
87 pub fn new(params: MacdParams) -> FinanceResult<Self> {
88 PeriodLength::new(params.fast)?;
89 PeriodLength::new(params.slow)?;
90 PeriodLength::new(params.signal)?;
91 if params.fast >= params.slow {
92 return Err(FinanceError::Unsolvable {
93 message: "MACD requires fast period < slow period",
94 });
95 }
96 Ok(Self { params })
97 }
98
99 pub fn params(self) -> MacdParams {
100 self.params
101 }
102
103 pub fn compute(self, closes: &[f64]) -> FinanceResult<MacdSeries> {
104 macd_validated(closes, self)
105 }
106}
107
108#[derive(Clone, Debug, PartialEq)]
110pub struct MacdSeries {
111 pub macd: Vec<Option<f64>>,
112 pub signal: Vec<Option<f64>>,
113 pub histogram: Vec<Option<f64>>,
114 pub params: MacdParams,
115}
116
117impl MacdSeries {
118 pub fn last(&self) -> Option<(f64, f64, f64)> {
119 let m = self.macd.iter().rev().find_map(|x| *x)?;
120 let s = self.signal.iter().rev().find_map(|x| *x)?;
121 let h = self.histogram.iter().rev().find_map(|x| *x)?;
122 Some((m, s, h))
123 }
124}
125
126#[derive(Clone, Debug)]
128pub struct MacdSolution {
129 series: MacdSeries,
130 closes: Vec<f64>,
131 formula: String,
132 symbolic_formula: String,
133}
134
135impl MacdSolution {
136 pub fn series(&self) -> &MacdSeries {
137 &self.series
138 }
139 pub fn formula(&self) -> &str {
140 &self.formula
141 }
142 pub fn symbolic_formula(&self) -> &str {
143 &self.symbolic_formula
144 }
145 pub fn params(&self) -> MacdParams {
146 self.series.params
147 }
148
149 pub fn print_table(&self) {
160 self.print_table_locale_opt(None, None);
161 }
162
163 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
164 self.print_table_locale_opt(Some(locale), Some(precision));
165 }
166
167 fn print_table_locale_opt(
168 &self,
169 locale: Option<&num_format::Locale>,
170 precision: Option<usize>,
171 ) {
172 let columns = columns_with_strings(&[
173 ("period", "i", true),
174 ("close", "f", true),
175 ("macd", "f", true),
176 ("signal", "f", true),
177 ("hist", "f", true),
178 ]);
179 let data = self
180 .closes
181 .iter()
182 .enumerate()
183 .map(|(i, c)| {
184 vec![
185 i.to_string(),
186 c.to_string(),
187 opt_cell(self.series.macd[i]),
188 opt_cell(self.series.signal[i]),
189 opt_cell(self.series.histogram[i]),
190 ]
191 })
192 .collect();
193 print_table_locale_opt(&columns, data, locale, precision);
194 }
195}
196
197pub fn macd(closes: &[f64], params: MacdParams) -> FinanceResult<MacdSeries> {
199 ValidatedMacd::new(params)?.compute(closes)
200}
201
202pub fn macd_solution(closes: &[f64], params: MacdParams) -> FinanceResult<MacdSolution> {
213 let series = macd(closes, params)?;
214 let formula = format!(
215 "macd = EMA({}) - EMA({}); signal = EMA({})(macd); hist = macd - signal",
216 params.fast, params.slow, params.signal
217 );
218 let symbolic =
219 "macd = ema_fast(close) - ema_slow(close); signal = ema_signal(macd); hist = macd - signal"
220 .to_string();
221 Ok(MacdSolution {
222 series,
223 closes: closes.to_vec(),
224 formula,
225 symbolic_formula: symbolic,
226 })
227}
228
229fn macd_validated(closes: &[f64], v: ValidatedMacd) -> FinanceResult<MacdSeries> {
230 validate_series("close", closes)?;
231 let p = v.params;
232 let mut st = MacdState::new(p)?;
233 let bars = st.push_bars_detail(closes)?;
234 let n = bars.len();
235 let mut macd_line = vec![None; n];
236 let mut signal = vec![None; n];
237 let mut histogram = vec![None; n];
238 for (i, b) in bars.into_iter().enumerate() {
239 macd_line[i] = b.macd;
240 signal[i] = b.signal;
241 histogram[i] = b.histogram;
242 }
243 Ok(MacdSeries {
244 macd: macd_line,
245 signal,
246 histogram,
247 params: p,
248 })
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn standard_runs() {
257 let c: Vec<_> = (1..=50).map(|x| 100.0 + x as f64 * 0.2).collect();
258 let s = macd(&c, MacdParams::standard()).unwrap();
259 assert_eq!(s.macd.len(), 50);
260 assert!(s.last().is_some());
261 }
262
263 #[test]
264 fn fast_not_lt_slow_err() {
265 assert!(ValidatedMacd::new(MacdParams::new(26, 12, 9)).is_err());
266 }
267
268 #[test]
269 fn histogram_is_macd_minus_signal() {
270 let c: Vec<_> = (1..=60).map(|x| 100.0 + x as f64 * 0.15).collect();
271 let s = macd(&c, MacdParams::standard()).unwrap();
272 for i in 0..s.macd.len() {
273 if let (Some(m), Some(sig), Some(h)) = (s.macd[i], s.signal[i], s.histogram[i]) {
274 assert!((h - (m - sig)).abs() < 1e-12);
275 }
276 }
277 }
278
279 #[test]
280 fn empty_err() {
281 assert!(macd(&[], MacdParams::standard()).is_err());
282 }
283}