use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct Kama {
er_period: usize,
fast_sc: f64,
slow_sc: f64,
window: VecDeque<f64>,
state: Option<f64>,
}
impl Kama {
pub fn new(er_period: usize, fast: usize, slow: usize) -> Result<Self> {
if er_period == 0 || fast == 0 || slow == 0 {
return Err(Error::PeriodZero);
}
if fast >= slow {
return Err(Error::InvalidPeriod {
message: "KAMA fast period must be strictly less than slow",
});
}
let fast_sc = 2.0 / (fast as f64 + 1.0);
let slow_sc = 2.0 / (slow as f64 + 1.0);
Ok(Self {
er_period,
fast_sc,
slow_sc,
window: VecDeque::with_capacity(er_period + 1),
state: None,
})
}
pub fn classic() -> Self {
Self::new(10, 2, 30).expect("classic KAMA parameters are valid")
}
pub fn periods(&self) -> (usize, f64, f64) {
(self.er_period, self.fast_sc, self.slow_sc)
}
}
impl Indicator for Kama {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.state;
}
if self.window.len() == self.er_period + 1 {
self.window.pop_front();
}
self.window.push_back(input);
if self.window.len() < self.er_period + 1 {
return None;
}
let first = *self.window.front().expect("non-empty");
let last = *self.window.back().expect("non-empty");
let direction = (last - first).abs();
let volatility: f64 = self
.window
.iter()
.zip(self.window.iter().skip(1))
.map(|(a, b)| (b - a).abs())
.sum();
let er = if volatility == 0.0 {
0.0
} else {
direction / volatility
};
let sc = (er * (self.fast_sc - self.slow_sc) + self.slow_sc).powi(2);
let prev = self.state.unwrap_or(first);
let new = prev + sc * (input - prev);
self.state = Some(new);
Some(new)
}
fn reset(&mut self) {
self.window.clear();
self.state = None;
}
fn warmup_period(&self) -> usize {
self.er_period + 1
}
fn is_ready(&self) -> bool {
self.state.is_some()
}
fn name(&self) -> &'static str {
"KAMA"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn constant_series_yields_constant_kama() {
let mut k = Kama::classic();
let out = k.batch(&[100.0_f64; 100]);
let last = out.iter().rev().flatten().next().unwrap();
assert_relative_eq!(*last, 100.0, epsilon = 1e-9);
}
#[test]
fn rejects_invalid_periods() {
assert!(Kama::new(0, 2, 30).is_err());
assert!(Kama::new(10, 30, 2).is_err()); assert!(Kama::new(10, 2, 2).is_err()); }
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=120)
.map(|i| (f64::from(i) * 0.2).sin() * 5.0 + f64::from(i) * 0.1)
.collect();
let mut a = Kama::classic();
let mut b = Kama::classic();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut k = Kama::classic();
k.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
assert!(k.is_ready());
k.reset();
assert!(!k.is_ready());
}
}