use crate::error::{Error, Result};
use crate::traits::Indicator;
use super::Ema;
#[derive(Debug, Clone)]
pub struct Pmo {
smoothing1: usize,
smoothing2: usize,
prev_price: Option<f64>,
ema1: Ema,
ema2: Ema,
current: Option<f64>,
}
impl Pmo {
pub fn new(smoothing1: usize, smoothing2: usize) -> Result<Self> {
if smoothing1 == 0 || smoothing2 == 0 {
return Err(Error::PeriodZero);
}
if smoothing1 < 2 || smoothing2 < 2 {
return Err(Error::InvalidPeriod {
message: "PMO smoothing periods must be >= 2",
});
}
Ok(Self {
smoothing1,
smoothing2,
prev_price: None,
ema1: Ema::with_alpha(2.0 / smoothing1 as f64)?,
ema2: Ema::with_alpha(2.0 / smoothing2 as f64)?,
current: None,
})
}
pub const fn periods(&self) -> (usize, usize) {
(self.smoothing1, self.smoothing2)
}
pub const fn value(&self) -> Option<f64> {
self.current
}
}
impl Indicator for Pmo {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.current;
}
let Some(prev) = self.prev_price else {
self.prev_price = Some(input);
return None;
};
self.prev_price = Some(input);
let roc = if prev == 0.0 {
0.0
} else {
(input / prev - 1.0) * 100.0
};
let smoothed = self.ema1.update(roc)?;
let pmo = self.ema2.update(10.0 * smoothed)?;
self.current = Some(pmo);
Some(pmo)
}
fn reset(&mut self) {
self.prev_price = None;
self.ema1.reset();
self.ema2.reset();
self.current = None;
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.current.is_some()
}
fn name(&self) -> &'static str {
"PMO"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn new_rejects_zero_period() {
assert!(matches!(Pmo::new(0, 20), Err(Error::PeriodZero)));
assert!(matches!(Pmo::new(35, 0), Err(Error::PeriodZero)));
}
#[test]
fn new_rejects_period_one() {
assert!(matches!(Pmo::new(1, 20), Err(Error::InvalidPeriod { .. })));
assert!(matches!(Pmo::new(35, 1), Err(Error::InvalidPeriod { .. })));
}
#[test]
fn first_emission_at_second_update() {
let mut pmo = Pmo::new(35, 20).unwrap();
assert_eq!(pmo.warmup_period(), 2);
assert_eq!(pmo.update(100.0), None);
assert!(pmo.update(101.0).is_some());
}
#[test]
fn constant_series_yields_zero() {
let mut pmo = Pmo::new(35, 20).unwrap();
let out = pmo.batch(&[100.0; 60]);
for v in out.iter().skip(2).flatten() {
assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
}
}
#[test]
fn steady_uptrend_is_positive() {
let mut pmo = Pmo::new(35, 20).unwrap();
let prices: Vec<f64> = (1..=120).map(|i| 100.0 * 1.01_f64.powi(i)).collect();
let out = pmo.batch(&prices);
let last = out.iter().rev().flatten().next().unwrap();
assert!(
*last > 0.0,
"steady uptrend PMO should be positive, got {last}"
);
}
#[test]
fn ignores_non_finite_input() {
let mut pmo = Pmo::new(35, 20).unwrap();
let out = pmo.batch(&(1..=60).map(f64::from).collect::<Vec<_>>());
let last = *out.last().unwrap();
assert!(last.is_some());
assert_eq!(pmo.update(f64::NAN), last);
assert_eq!(pmo.update(f64::INFINITY), last);
}
#[test]
fn reset_clears_state() {
let mut pmo = Pmo::new(35, 20).unwrap();
pmo.batch(&(1..=60).map(f64::from).collect::<Vec<_>>());
assert!(pmo.is_ready());
pmo.reset();
assert!(!pmo.is_ready());
assert_eq!(pmo.update(1.0), None);
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=120)
.map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 8.0)
.collect();
let batch = Pmo::new(35, 20).unwrap().batch(&prices);
let mut b = Pmo::new(35, 20).unwrap();
let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
assert_eq!(batch, streamed);
}
}