wickra_core/indicators/
macd_histogram.rs1use crate::error::Result;
4use crate::indicators::macd::MacdIndicator;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
40pub struct MacdHistogram {
41 macd: MacdIndicator,
42}
43
44impl MacdHistogram {
45 pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
52 Ok(Self {
53 macd: MacdIndicator::new(fast, slow, signal)?,
54 })
55 }
56
57 pub fn classic() -> Self {
59 Self::new(12, 26, 9).expect("classic MACD periods are valid")
60 }
61
62 pub const fn periods(&self) -> (usize, usize, usize) {
64 self.macd.periods()
65 }
66}
67
68impl Indicator for MacdHistogram {
69 type Input = f64;
70 type Output = f64;
71
72 fn update(&mut self, input: f64) -> Option<f64> {
73 self.macd.update(input).map(|out| out.histogram)
74 }
75
76 fn reset(&mut self) {
77 self.macd.reset();
78 }
79
80 fn warmup_period(&self) -> usize {
81 self.macd.warmup_period()
82 }
83
84 fn is_ready(&self) -> bool {
85 self.macd.is_ready()
86 }
87
88 fn name(&self) -> &'static str {
89 "MacdHistogram"
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::error::Error;
97 use crate::traits::BatchExt;
98 use approx::assert_relative_eq;
99
100 #[test]
101 fn rejects_invalid_periods() {
102 assert!(matches!(
103 MacdHistogram::new(0, 26, 9),
104 Err(Error::PeriodZero)
105 ));
106 assert!(matches!(
107 MacdHistogram::new(12, 26, 0),
108 Err(Error::PeriodZero)
109 ));
110 assert!(matches!(
111 MacdHistogram::new(26, 12, 9),
112 Err(Error::InvalidPeriod { .. })
113 ));
114 }
115
116 #[test]
117 fn accessors_and_metadata() {
118 let osc = MacdHistogram::classic();
119 assert_eq!(osc.periods(), (12, 26, 9));
120 assert_eq!(osc.name(), "MacdHistogram");
121 assert_eq!(osc.warmup_period(), 26 + 9 - 1);
122 assert!(!osc.is_ready());
123 }
124
125 #[test]
126 fn equals_macd_histogram_field() {
127 let prices: Vec<f64> = (1..=120)
129 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 8.0)
130 .collect();
131 let hist = MacdHistogram::classic().batch(&prices);
132 let full = MacdIndicator::classic().batch(&prices);
133 assert_eq!(hist.len(), full.len());
134 for (h, m) in hist.iter().zip(full.iter()) {
135 assert_eq!(h.is_some(), m.is_some());
136 if let (Some(h), Some(m)) = (h, m) {
137 assert_relative_eq!(*h, m.histogram, epsilon = 1e-12);
138 }
139 }
140 }
141
142 #[test]
143 fn warmup_emits_first_value_at_warmup_period() {
144 let mut osc = MacdHistogram::new(3, 6, 3).unwrap();
145 let warmup = osc.warmup_period();
146 assert_eq!(warmup, 6 + 3 - 1);
147 for i in 1..warmup {
148 assert!(osc.update(100.0 + i as f64).is_none());
149 }
150 assert!(osc.update(100.0 + warmup as f64).is_some());
151 assert!(osc.is_ready());
152 }
153
154 #[test]
155 fn constant_series_converges_to_zero() {
156 let mut osc = MacdHistogram::classic();
157 let out = osc.batch(&[100.0_f64; 200]);
158 let last = out.iter().rev().flatten().next().expect("emits a value");
159 assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
160 }
161
162 #[test]
163 fn batch_equals_streaming() {
164 let prices: Vec<f64> = (1..=100)
165 .map(|i| (f64::from(i) * 0.4).cos() * 10.0)
166 .collect();
167 let mut a = MacdHistogram::classic();
168 let mut b = MacdHistogram::classic();
169 assert_eq!(
170 a.batch(&prices),
171 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
172 );
173 }
174
175 #[test]
176 fn reset_clears_state() {
177 let mut osc = MacdHistogram::classic();
178 osc.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
179 assert!(osc.is_ready());
180 osc.reset();
181 assert!(!osc.is_ready());
182 assert_eq!(osc.update(1.0), None);
183 }
184}