use crate::error::Result;
use crate::indicators::macd::{MacdIndicator, MacdOutput};
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct MacdFix {
inner: MacdIndicator,
}
impl MacdFix {
pub fn new(signal: usize) -> Result<Self> {
Ok(Self {
inner: MacdIndicator::fixed_12_26(signal)?,
})
}
pub fn signal_period(&self) -> usize {
self.inner.periods().2
}
pub fn batch_macd_into(&mut self, inputs: &[f64], out: &mut [f64]) {
self.inner.batch_macd_into(inputs, out);
}
pub fn batch_macd(&mut self, inputs: &[f64]) -> Vec<f64> {
self.inner.batch_macd(inputs)
}
pub fn batch_macd_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
self.inner.batch_macd_fast_into(inputs, out);
}
pub fn batch_macd_fast(&mut self, inputs: &[f64]) -> Vec<f64> {
self.inner.batch_macd_fast(inputs)
}
}
impl Indicator for MacdFix {
type Input = f64;
type Output = MacdOutput;
#[inline]
fn update(&mut self, value: f64) -> Option<MacdOutput> {
self.inner.update(value)
}
fn reset(&mut self) {
self.inner.reset();
}
#[inline]
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
#[inline]
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[inline]
fn name(&self) -> &'static str {
"MACDFIX"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
#[test]
fn rejects_zero_signal() {
assert!(MacdFix::new(0).is_err());
}
#[test]
fn accessors_report_config() {
let m = MacdFix::new(9).unwrap();
assert_eq!(m.signal_period(), 9);
assert_eq!(m.name(), "MACDFIX");
assert!(!m.is_ready());
assert_eq!(
m.warmup_period(),
MacdIndicator::new(12, 26, 9).unwrap().warmup_period()
);
}
#[test]
fn uses_the_fixed_smoothing_constants() {
let prices: Vec<f64> = (0..80)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
.collect();
let ema = |n: usize, a: f64| -> Vec<Option<f64>> {
let mut out = vec![None; prices.len()];
let mut v = prices[..n].iter().sum::<f64>() / n as f64;
out[n - 1] = Some(v);
for i in n..prices.len() {
v = a * prices[i] + (1.0 - a) * v;
out[i] = Some(v);
}
out
};
let (fast, slow) = (ema(12, 0.15), ema(26, 0.075));
let fix: Vec<Option<MacdOutput>> = MacdFix::new(9).unwrap().batch(&prices);
for (i, out) in fix.iter().enumerate() {
if let Some(o) = out {
let expected = fast[i].unwrap() - slow[i].unwrap();
assert!((o.macd - expected).abs() < 1e-9, "at {i}");
}
}
assert!(fix.iter().any(Option::is_some));
let classic: Vec<Option<MacdOutput>> =
MacdIndicator::new(12, 26, 9).unwrap().batch(&prices);
assert_ne!(fix, classic);
}
#[test]
fn flat_batch_matches_streaming() {
let prices: Vec<f64> = (0..80)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
.collect();
let streamed: Vec<f64> = MacdFix::new(9)
.unwrap()
.batch(&prices)
.into_iter()
.flat_map(|o| o.map_or([f64::NAN; 3], |o| [o.macd, o.signal, o.histogram]))
.collect();
let flat = MacdFix::new(9).unwrap().batch_macd(&prices);
assert_eq!(flat.len(), streamed.len());
for (a, b) in flat.iter().zip(&streamed) {
assert!(a.to_bits() == b.to_bits() || (a.is_nan() && b.is_nan()));
}
}
#[test]
fn reset_clears_state() {
let prices: Vec<f64> = (0..80).map(|i| 100.0 + f64::from(i)).collect();
let mut m = MacdFix::new(9).unwrap();
let _ = m.batch(&prices);
assert!(m.is_ready());
m.reset();
assert!(!m.is_ready());
}
fn prices(len: i32) -> Vec<f64> {
(0..len)
.map(|i| 100.0 + (f64::from(i) * 0.21).sin() * 7.0 + f64::from(i % 11) * 0.3)
.collect()
}
fn streamed_flat(signal: usize, inputs: &[f64]) -> Vec<f64> {
let mut m = MacdFix::new(signal).unwrap();
inputs
.iter()
.flat_map(|&x| {
m.update(x)
.map_or([f64::NAN; 3], |o| [o.macd, o.signal, o.histogram])
})
.collect()
}
fn bits(v: &[f64]) -> Vec<u64> {
v.iter().map(|x| x.to_bits()).collect()
}
#[test]
fn rejects_signal_above_max() {
let too_big = crate::error::MAX_PERIOD + 1;
assert!(matches!(
MacdFix::new(too_big),
Err(crate::Error::InvalidPeriod { .. })
));
assert!(matches!(MacdFix::new(0), Err(crate::Error::PeriodZero)));
}
#[test]
fn warmup_is_exact() {
let xs = prices(60);
let mut m = MacdFix::new(9).unwrap();
assert_eq!(m.warmup_period(), 34);
let out = m.batch(&xs);
assert!(out[..33].iter().all(Option::is_none));
assert!(out[33..].iter().all(Option::is_some));
}
#[test]
fn hand_computed_signal_two() {
let mut xs = vec![100.0; 26];
xs.extend_from_slice(&[110.0, 110.0]);
let out = MacdFix::new(2).unwrap().batch(&xs);
assert!(out[..26].iter().all(Option::is_none));
let o26 = out[26].unwrap();
approx::assert_relative_eq!(o26.macd, 0.75, epsilon = 1e-12);
approx::assert_relative_eq!(o26.signal, 0.375, epsilon = 1e-12);
approx::assert_relative_eq!(o26.histogram, 0.375, epsilon = 1e-12);
let o27 = out[27].unwrap();
approx::assert_relative_eq!(o27.macd, 1.331_25, epsilon = 1e-12);
approx::assert_relative_eq!(o27.signal, 1.0125, epsilon = 1e-12);
approx::assert_relative_eq!(o27.histogram, 0.318_75, epsilon = 1e-12);
}
#[test]
fn batch_equals_streaming() {
let xs = prices(120);
let mut streaming = MacdFix::new(9).unwrap();
let expected: Vec<Option<MacdOutput>> = xs.iter().map(|&x| streaming.update(x)).collect();
assert_eq!(MacdFix::new(9).unwrap().batch(&xs), expected);
}
#[test]
fn batch_macd_into_is_bit_identical_to_streaming() {
let xs = prices(200);
let mut out = vec![-7.0; xs.len() * 3];
let mut m = MacdFix::new(9).unwrap();
m.batch_macd_into(&xs, &mut out);
assert_eq!(bits(&out), bits(&streamed_flat(9, &xs)));
let mut replay = MacdFix::new(9).unwrap();
let _ = replay.batch(&xs);
assert_eq!(m.update(103.0), replay.update(103.0));
assert_eq!(bits(&MacdFix::new(9).unwrap().batch_macd(&xs)), bits(&out));
}
#[test]
fn batch_macd_into_short_input_falls_back_to_replay() {
let xs = prices(20);
let mut out = vec![0.0; xs.len() * 3];
MacdFix::new(9).unwrap().batch_macd_into(&xs, &mut out);
assert_eq!(bits(&out), bits(&streamed_flat(9, &xs)));
}
#[test]
fn batch_macd_fast_is_within_tolerance_with_identical_nans() {
let xs = prices(500);
let exact = streamed_flat(9, &xs);
let mut m = MacdFix::new(9).unwrap();
let fast = m.batch_macd_fast(&xs);
assert_eq!(fast.len(), exact.len());
assert!(fast
.iter()
.zip(&exact)
.all(|(f, e)| f.is_nan() == e.is_nan()));
let close = fast
.iter()
.zip(&exact)
.filter(|(f, _)| !f.is_nan())
.all(|(f, e)| (f - e).abs() <= 1e-12 * e.abs().max(1.0));
assert!(
close,
"fast batch must stay within 1e-12 relative of the exact batch"
);
let mut replay = MacdFix::new(9).unwrap();
let _ = replay.batch(&xs);
let (a, b) = (m.update(104.0).unwrap(), replay.update(104.0).unwrap());
approx::assert_relative_eq!(a.macd, b.macd, max_relative = 1e-12);
approx::assert_relative_eq!(a.signal, b.signal, max_relative = 1e-12);
}
#[test]
fn batch_macd_fast_into_fills_a_caller_buffer() {
let xs = prices(300);
let mut out = vec![5.0; xs.len() * 3];
MacdFix::new(5).unwrap().batch_macd_fast_into(&xs, &mut out);
let exact = MacdFix::new(5).unwrap().batch_macd(&xs);
assert!(out
.iter()
.zip(&exact)
.all(|(f, e)| f.is_nan() == e.is_nan()));
let close = out
.iter()
.zip(&exact)
.filter(|(f, _)| !f.is_nan())
.all(|(f, e)| (f - e).abs() <= 1e-12 * e.abs().max(1.0));
assert!(
close,
"fast batch must stay within 1e-12 relative of the exact batch"
);
}
#[test]
#[should_panic(expected = "three values per input")]
fn batch_macd_into_rejects_length_mismatch() {
let xs = prices(40);
let mut out = vec![0.0; xs.len() * 3 - 1];
MacdFix::new(9).unwrap().batch_macd_into(&xs, &mut out);
}
#[test]
#[should_panic(expected = "three values per input")]
fn batch_macd_fast_into_rejects_length_mismatch() {
let xs = prices(40);
let mut out = vec![0.0; xs.len()];
MacdFix::new(9).unwrap().batch_macd_fast_into(&xs, &mut out);
}
#[test]
fn reset_reproduces_a_fresh_run() {
let xs = prices(90);
let mut m = MacdFix::new(9).unwrap();
let first = m.batch(&xs);
m.reset();
let second = m.batch(&xs);
assert_eq!(first, second);
assert_eq!(second, MacdFix::new(9).unwrap().batch(&xs));
}
}