use std::f64::consts::PI;
use crate::error::{Error, Result};
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct Decycler {
period: usize,
alpha: f64,
prev_in_1: Option<f64>,
prev_in_2: Option<f64>,
prev_hp_1: f64,
prev_hp_2: f64,
last_value: Option<f64>,
}
impl Decycler {
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
let arg = 0.707 * 2.0 * PI / period as f64;
let c = arg.cos();
let alpha = (c + arg.sin() - 1.0) / c;
Ok(Self {
period,
alpha,
prev_in_1: None,
prev_in_2: None,
prev_hp_1: 0.0,
prev_hp_2: 0.0,
last_value: None,
})
}
pub const fn period(&self) -> usize {
self.period
}
pub const fn alpha(&self) -> f64 {
self.alpha
}
pub const fn value(&self) -> Option<f64> {
self.last_value
}
fn step_hp(&mut self, input: f64) -> f64 {
let (Some(x1), Some(x2)) = (self.prev_in_1, self.prev_in_2) else {
self.prev_hp_2 = self.prev_hp_1;
self.prev_hp_1 = 0.0;
return 0.0;
};
let one_minus_half_alpha = 1.0 - self.alpha / 2.0;
let one_minus_alpha = 1.0 - self.alpha;
let drv = one_minus_half_alpha * one_minus_half_alpha;
let term1 = drv * (input - 2.0 * x1 + x2);
let term2 = 2.0 * one_minus_alpha * self.prev_hp_1;
let term3 = one_minus_alpha * one_minus_alpha * self.prev_hp_2;
let hp = term1 + term2 - term3;
self.prev_hp_2 = self.prev_hp_1;
self.prev_hp_1 = hp;
hp
}
}
impl Indicator for Decycler {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.last_value;
}
let hp = self.step_hp(input);
let v = input - hp;
self.prev_in_2 = self.prev_in_1;
self.prev_in_1 = Some(input);
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.prev_in_1 = None;
self.prev_in_2 = None;
self.prev_hp_1 = 0.0;
self.prev_hp_2 = 0.0;
self.last_value = None;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"Decycler"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn new_rejects_zero_period() {
assert!(matches!(Decycler::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let mut dc = Decycler::new(20).unwrap();
assert_eq!(dc.period(), 20);
assert_eq!(dc.warmup_period(), 1);
assert_eq!(dc.name(), "Decycler");
assert!(dc.alpha() > 0.0 && dc.alpha() < 1.0);
assert!(!dc.is_ready());
dc.update(100.0);
assert!(dc.is_ready());
assert!(dc.value().is_some());
}
#[test]
fn constant_series_passes_through() {
let mut dc = Decycler::new(20).unwrap();
let out = dc.batch(&[42.0_f64; 80]);
for x in out.iter().flatten() {
assert_relative_eq!(*x, 42.0, epsilon = 1e-9);
}
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (0..100)
.map(|i| 100.0 + (f64::from(i) * 0.15).sin() * 5.0)
.collect();
let mut a = Decycler::new(20).unwrap();
let mut b = Decycler::new(20).unwrap();
let batch = a.batch(&prices);
let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn ignores_non_finite_input() {
let mut dc = Decycler::new(20).unwrap();
dc.batch(&(1..=30).map(f64::from).collect::<Vec<_>>());
let before = dc.value();
assert!(before.is_some());
assert_eq!(dc.update(f64::NAN), before);
assert_eq!(dc.update(f64::INFINITY), before);
}
#[test]
fn reset_clears_state() {
let mut dc = Decycler::new(20).unwrap();
dc.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
assert!(dc.is_ready());
dc.reset();
assert!(!dc.is_ready());
}
}