use crate::error::{Error, Result};
use crate::indicators::decycler::Decycler;
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct DecyclerOscillator {
fast: Decycler,
slow: Decycler,
last_value: Option<f64>,
}
impl DecyclerOscillator {
pub fn new(fast: usize, slow: usize) -> Result<Self> {
if fast == 0 || slow == 0 {
return Err(Error::PeriodZero);
}
if fast >= slow {
return Err(Error::InvalidPeriod {
message: "fast period must be strictly less than slow period",
});
}
Ok(Self {
fast: Decycler::new(fast)?,
slow: Decycler::new(slow)?,
last_value: None,
})
}
pub fn periods(&self) -> (usize, usize) {
(self.fast.period(), self.slow.period())
}
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for DecyclerOscillator {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.last_value;
}
let f = self.fast.update(input)?;
let s = self.slow.update(input)?;
let v = f - s;
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.fast.reset();
self.slow.reset();
self.last_value = None;
}
fn warmup_period(&self) -> usize {
self.fast.warmup_period().max(self.slow.warmup_period())
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"DecyclerOscillator"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn new_rejects_invalid_periods() {
assert!(matches!(
DecyclerOscillator::new(0, 20),
Err(Error::PeriodZero)
));
assert!(matches!(
DecyclerOscillator::new(10, 0),
Err(Error::PeriodZero)
));
assert!(matches!(
DecyclerOscillator::new(20, 10),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
DecyclerOscillator::new(10, 10),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let mut dco = DecyclerOscillator::new(10, 30).unwrap();
assert_eq!(dco.periods(), (10, 30));
assert_eq!(dco.name(), "DecyclerOscillator");
assert!(dco.warmup_period() >= 1);
assert!(!dco.is_ready());
dco.update(100.0);
assert!(dco.is_ready());
assert!(dco.value().is_some());
}
#[test]
fn constant_series_yields_zero() {
let mut dco = DecyclerOscillator::new(10, 30).unwrap();
let out = dco.batch(&[42.0_f64; 80]);
for x in out.iter().flatten() {
assert_relative_eq!(*x, 0.0, epsilon = 1e-9);
}
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (0..100)
.map(|i| 100.0 + (f64::from(i) * 0.2).cos() * 6.0)
.collect();
let mut a = DecyclerOscillator::new(10, 30).unwrap();
let mut b = DecyclerOscillator::new(10, 30).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 dco = DecyclerOscillator::new(10, 30).unwrap();
dco.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
let before = dco.value();
assert!(before.is_some());
assert_eq!(dco.update(f64::NAN), before);
}
#[test]
fn reset_clears_state() {
let mut dco = DecyclerOscillator::new(10, 30).unwrap();
dco.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
assert!(dco.is_ready());
dco.reset();
assert!(!dco.is_ready());
}
}