use crate::error::{Error, Result};
use crate::indicators::ema::Ema;
use crate::indicators::ppo::Ppo;
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct PpoHistogram {
ppo: Ppo,
signal_ema: Ema,
signal_period: usize,
current: Option<f64>,
}
impl PpoHistogram {
pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
if signal == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
ppo: Ppo::new(fast, slow)?,
signal_ema: Ema::new(signal)?,
signal_period: signal,
current: None,
})
}
pub fn classic() -> Self {
Self::new(12, 26, 9).expect("classic PPO periods are valid")
}
pub const fn periods(&self) -> (usize, usize, usize) {
let (fast, slow) = self.ppo.periods();
(fast, slow, self.signal_period)
}
pub const fn value(&self) -> Option<f64> {
self.current
}
}
impl Indicator for PpoHistogram {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.current;
}
let ppo = self.ppo.update(input)?;
let signal = self.signal_ema.update(ppo)?;
let histogram = ppo - signal;
self.current = Some(histogram);
Some(histogram)
}
fn reset(&mut self) {
self.ppo.reset();
self.signal_ema.reset();
self.current = None;
}
fn warmup_period(&self) -> usize {
self.ppo.warmup_period() + self.signal_period - 1
}
fn is_ready(&self) -> bool {
self.current.is_some()
}
fn name(&self) -> &'static str {
"PpoHistogram"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_invalid_periods() {
assert!(matches!(
PpoHistogram::new(0, 26, 9),
Err(Error::PeriodZero)
));
assert!(matches!(
PpoHistogram::new(12, 0, 9),
Err(Error::PeriodZero)
));
assert!(matches!(
PpoHistogram::new(12, 26, 0),
Err(Error::PeriodZero)
));
assert!(matches!(
PpoHistogram::new(26, 12, 9),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let osc = PpoHistogram::classic();
assert_eq!(osc.periods(), (12, 26, 9));
assert_eq!(osc.name(), "PpoHistogram");
assert_eq!(osc.warmup_period(), 26 + 9 - 1);
assert_eq!(osc.value(), None);
assert!(!osc.is_ready());
}
#[test]
fn equals_ppo_minus_signal_ema() {
let prices: Vec<f64> = (1..=120)
.map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 6.0)
.collect();
let got = PpoHistogram::new(12, 26, 9).unwrap().batch(&prices);
let mut ppo = Ppo::new(12, 26).unwrap();
let mut sig = Ema::new(9).unwrap();
let mut expected = Vec::with_capacity(prices.len());
for p in &prices {
let out = ppo
.update(*p)
.and_then(|line| sig.update(line).map(|signal| line - signal));
expected.push(out);
}
assert_eq!(got, expected);
}
#[test]
fn warmup_emits_first_value_at_warmup_period() {
let mut osc = PpoHistogram::new(3, 6, 3).unwrap();
let warmup = osc.warmup_period();
assert_eq!(warmup, 6 + 3 - 1);
for i in 1..warmup {
assert!(osc.update(100.0 + i as f64).is_none());
}
assert!(osc.update(100.0 + warmup as f64).is_some());
assert!(osc.is_ready());
}
#[test]
fn constant_series_converges_to_zero() {
let mut osc = PpoHistogram::classic();
let out = osc.batch(&[100.0_f64; 200]);
let last = out.iter().rev().flatten().next().expect("emits a value");
assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
}
#[test]
fn ignores_non_finite_input() {
let mut osc = PpoHistogram::new(3, 6, 3).unwrap();
let out = osc.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
let before = *out.last().unwrap();
assert!(before.is_some());
assert_eq!(osc.update(f64::NAN), before);
assert_eq!(osc.update(f64::INFINITY), before);
assert_eq!(osc.value(), before);
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=100)
.map(|i| 100.0 + (f64::from(i) * 0.4).cos() * 10.0)
.collect();
let mut a = PpoHistogram::classic();
let mut b = PpoHistogram::classic();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut osc = PpoHistogram::classic();
osc.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
assert!(osc.is_ready());
osc.reset();
assert!(!osc.is_ready());
assert_eq!(osc.update(1.0), None);
}
}