wickra_core/indicators/
macd_fix.rs1use crate::error::Result;
4use crate::indicators::macd::{MacdIndicator, MacdOutput};
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
36pub struct MacdFix {
37 inner: MacdIndicator,
38}
39
40impl MacdFix {
41 pub fn new(signal: usize) -> Result<Self> {
46 Ok(Self {
47 inner: MacdIndicator::fixed_12_26(signal)?,
48 })
49 }
50
51 pub fn signal_period(&self) -> usize {
53 self.inner.periods().2
54 }
55
56 pub fn batch_macd_into(&mut self, inputs: &[f64], out: &mut [f64]) {
63 self.inner.batch_macd_into(inputs, out);
64 }
65
66 pub fn batch_macd(&mut self, inputs: &[f64]) -> Vec<f64> {
68 self.inner.batch_macd(inputs)
69 }
70
71 pub fn batch_macd_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
77 self.inner.batch_macd_fast_into(inputs, out);
78 }
79
80 pub fn batch_macd_fast(&mut self, inputs: &[f64]) -> Vec<f64> {
82 self.inner.batch_macd_fast(inputs)
83 }
84}
85
86impl Indicator for MacdFix {
87 type Input = f64;
88 type Output = MacdOutput;
89
90 #[inline]
91 fn update(&mut self, value: f64) -> Option<MacdOutput> {
92 self.inner.update(value)
93 }
94
95 fn reset(&mut self) {
96 self.inner.reset();
97 }
98
99 #[inline]
100 fn warmup_period(&self) -> usize {
101 self.inner.warmup_period()
102 }
103
104 #[inline]
105 fn is_ready(&self) -> bool {
106 self.inner.is_ready()
107 }
108
109 #[inline]
110 fn name(&self) -> &'static str {
111 "MACDFIX"
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::traits::BatchExt;
119
120 #[test]
121 fn rejects_zero_signal() {
122 assert!(MacdFix::new(0).is_err());
123 }
124
125 #[test]
126 fn accessors_report_config() {
127 let m = MacdFix::new(9).unwrap();
128 assert_eq!(m.signal_period(), 9);
129 assert_eq!(m.name(), "MACDFIX");
130 assert!(!m.is_ready());
131 assert_eq!(
132 m.warmup_period(),
133 MacdIndicator::new(12, 26, 9).unwrap().warmup_period()
134 );
135 }
136
137 #[test]
138 fn uses_the_fixed_smoothing_constants() {
139 let prices: Vec<f64> = (0..80)
140 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
141 .collect();
142 let ema = |n: usize, a: f64| -> Vec<Option<f64>> {
144 let mut out = vec![None; prices.len()];
145 let mut v = prices[..n].iter().sum::<f64>() / n as f64;
146 out[n - 1] = Some(v);
147 for i in n..prices.len() {
148 v = a * prices[i] + (1.0 - a) * v;
149 out[i] = Some(v);
150 }
151 out
152 };
153 let (fast, slow) = (ema(12, 0.15), ema(26, 0.075));
154 let fix: Vec<Option<MacdOutput>> = MacdFix::new(9).unwrap().batch(&prices);
155 for (i, out) in fix.iter().enumerate() {
158 if let Some(o) = out {
159 let expected = fast[i].unwrap() - slow[i].unwrap();
160 assert!((o.macd - expected).abs() < 1e-9, "at {i}");
161 }
162 }
163 assert!(fix.iter().any(Option::is_some));
164 let classic: Vec<Option<MacdOutput>> =
166 MacdIndicator::new(12, 26, 9).unwrap().batch(&prices);
167 assert_ne!(fix, classic);
168 }
169
170 #[test]
171 fn flat_batch_matches_streaming() {
172 let prices: Vec<f64> = (0..80)
173 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
174 .collect();
175 let streamed: Vec<f64> = MacdFix::new(9)
176 .unwrap()
177 .batch(&prices)
178 .into_iter()
179 .flat_map(|o| o.map_or([f64::NAN; 3], |o| [o.macd, o.signal, o.histogram]))
180 .collect();
181 let flat = MacdFix::new(9).unwrap().batch_macd(&prices);
182 assert_eq!(flat.len(), streamed.len());
183 for (a, b) in flat.iter().zip(&streamed) {
184 assert!(a.to_bits() == b.to_bits() || (a.is_nan() && b.is_nan()));
185 }
186 }
187
188 #[test]
189 fn reset_clears_state() {
190 let prices: Vec<f64> = (0..80).map(|i| 100.0 + f64::from(i)).collect();
191 let mut m = MacdFix::new(9).unwrap();
192 let _ = m.batch(&prices);
193 assert!(m.is_ready());
194 m.reset();
195 assert!(!m.is_ready());
196 }
197
198 fn prices(len: i32) -> Vec<f64> {
199 (0..len)
200 .map(|i| 100.0 + (f64::from(i) * 0.21).sin() * 7.0 + f64::from(i % 11) * 0.3)
201 .collect()
202 }
203
204 fn streamed_flat(signal: usize, inputs: &[f64]) -> Vec<f64> {
205 let mut m = MacdFix::new(signal).unwrap();
206 inputs
207 .iter()
208 .flat_map(|&x| {
209 m.update(x)
210 .map_or([f64::NAN; 3], |o| [o.macd, o.signal, o.histogram])
211 })
212 .collect()
213 }
214
215 fn bits(v: &[f64]) -> Vec<u64> {
216 v.iter().map(|x| x.to_bits()).collect()
217 }
218
219 #[test]
220 fn rejects_signal_above_max() {
221 let too_big = crate::error::MAX_PERIOD + 1;
222 assert!(matches!(
223 MacdFix::new(too_big),
224 Err(crate::Error::InvalidPeriod { .. })
225 ));
226 assert!(matches!(MacdFix::new(0), Err(crate::Error::PeriodZero)));
227 }
228
229 #[test]
230 fn warmup_is_exact() {
231 let xs = prices(60);
232 let mut m = MacdFix::new(9).unwrap();
233 assert_eq!(m.warmup_period(), 34);
234 let out = m.batch(&xs);
235 assert!(out[..33].iter().all(Option::is_none));
236 assert!(out[33..].iter().all(Option::is_some));
237 }
238
239 #[test]
240 fn hand_computed_signal_two() {
241 let mut xs = vec![100.0; 26];
248 xs.extend_from_slice(&[110.0, 110.0]);
249 let out = MacdFix::new(2).unwrap().batch(&xs);
250 assert!(out[..26].iter().all(Option::is_none));
251 let o26 = out[26].unwrap();
252 approx::assert_relative_eq!(o26.macd, 0.75, epsilon = 1e-12);
253 approx::assert_relative_eq!(o26.signal, 0.375, epsilon = 1e-12);
254 approx::assert_relative_eq!(o26.histogram, 0.375, epsilon = 1e-12);
255 let o27 = out[27].unwrap();
256 approx::assert_relative_eq!(o27.macd, 1.331_25, epsilon = 1e-12);
257 approx::assert_relative_eq!(o27.signal, 1.0125, epsilon = 1e-12);
258 approx::assert_relative_eq!(o27.histogram, 0.318_75, epsilon = 1e-12);
259 }
260
261 #[test]
262 fn batch_equals_streaming() {
263 let xs = prices(120);
264 let mut streaming = MacdFix::new(9).unwrap();
265 let expected: Vec<Option<MacdOutput>> = xs.iter().map(|&x| streaming.update(x)).collect();
266 assert_eq!(MacdFix::new(9).unwrap().batch(&xs), expected);
267 }
268
269 #[test]
270 fn batch_macd_into_is_bit_identical_to_streaming() {
271 let xs = prices(200);
272 let mut out = vec![-7.0; xs.len() * 3];
273 let mut m = MacdFix::new(9).unwrap();
274 m.batch_macd_into(&xs, &mut out);
275 assert_eq!(bits(&out), bits(&streamed_flat(9, &xs)));
276 let mut replay = MacdFix::new(9).unwrap();
278 let _ = replay.batch(&xs);
279 assert_eq!(m.update(103.0), replay.update(103.0));
280 assert_eq!(bits(&MacdFix::new(9).unwrap().batch_macd(&xs)), bits(&out));
282 }
283
284 #[test]
285 fn batch_macd_into_short_input_falls_back_to_replay() {
286 let xs = prices(20);
287 let mut out = vec![0.0; xs.len() * 3];
288 MacdFix::new(9).unwrap().batch_macd_into(&xs, &mut out);
289 assert_eq!(bits(&out), bits(&streamed_flat(9, &xs)));
290 }
291
292 #[test]
293 fn batch_macd_fast_is_within_tolerance_with_identical_nans() {
294 let xs = prices(500);
295 let exact = streamed_flat(9, &xs);
296 let mut m = MacdFix::new(9).unwrap();
297 let fast = m.batch_macd_fast(&xs);
298 assert_eq!(fast.len(), exact.len());
299 assert!(fast
300 .iter()
301 .zip(&exact)
302 .all(|(f, e)| f.is_nan() == e.is_nan()));
303 let close = fast
304 .iter()
305 .zip(&exact)
306 .filter(|(f, _)| !f.is_nan())
307 .all(|(f, e)| (f - e).abs() <= 1e-12 * e.abs().max(1.0));
308 assert!(
309 close,
310 "fast batch must stay within 1e-12 relative of the exact batch"
311 );
312 let mut replay = MacdFix::new(9).unwrap();
314 let _ = replay.batch(&xs);
315 let (a, b) = (m.update(104.0).unwrap(), replay.update(104.0).unwrap());
316 approx::assert_relative_eq!(a.macd, b.macd, max_relative = 1e-12);
317 approx::assert_relative_eq!(a.signal, b.signal, max_relative = 1e-12);
318 }
319
320 #[test]
321 fn batch_macd_fast_into_fills_a_caller_buffer() {
322 let xs = prices(300);
323 let mut out = vec![5.0; xs.len() * 3];
324 MacdFix::new(5).unwrap().batch_macd_fast_into(&xs, &mut out);
325 let exact = MacdFix::new(5).unwrap().batch_macd(&xs);
326 assert!(out
327 .iter()
328 .zip(&exact)
329 .all(|(f, e)| f.is_nan() == e.is_nan()));
330 let close = out
331 .iter()
332 .zip(&exact)
333 .filter(|(f, _)| !f.is_nan())
334 .all(|(f, e)| (f - e).abs() <= 1e-12 * e.abs().max(1.0));
335 assert!(
336 close,
337 "fast batch must stay within 1e-12 relative of the exact batch"
338 );
339 }
340
341 #[test]
342 #[should_panic(expected = "three values per input")]
343 fn batch_macd_into_rejects_length_mismatch() {
344 let xs = prices(40);
345 let mut out = vec![0.0; xs.len() * 3 - 1];
346 MacdFix::new(9).unwrap().batch_macd_into(&xs, &mut out);
347 }
348
349 #[test]
350 #[should_panic(expected = "three values per input")]
351 fn batch_macd_fast_into_rejects_length_mismatch() {
352 let xs = prices(40);
353 let mut out = vec![0.0; xs.len()];
354 MacdFix::new(9).unwrap().batch_macd_fast_into(&xs, &mut out);
355 }
356
357 #[test]
358 fn reset_reproduces_a_fresh_run() {
359 let xs = prices(90);
360 let mut m = MacdFix::new(9).unwrap();
361 let first = m.batch(&xs);
362 m.reset();
363 let second = m.batch(&xs);
364 assert_eq!(first, second);
365 assert_eq!(second, MacdFix::new(9).unwrap().batch(&xs));
366 }
367}