Skip to main content

wickra_core/indicators/
kama.rs

1//! Kaufman's Adaptive Moving Average (KAMA).
2
3use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8/// Kaufman's Adaptive Moving Average.
9///
10/// KAMA adapts its smoothing constant to volatility: efficient (trending) markets
11/// get a fast smoothing constant, choppy markets get a slow one. Parameters are
12/// the efficiency-ratio lookback (`er_period`, default 10), the fast EMA period
13/// (`fast`, default 2) and the slow EMA period (`slow`, default 30).
14///
15/// # Example
16///
17/// ```
18/// use wickra_core::{Indicator, Kama};
19///
20/// let mut indicator = Kama::new(10, 2, 30).unwrap();
21/// let mut last = None;
22/// for i in 0..80 {
23///     last = indicator.update(100.0 + f64::from(i));
24/// }
25/// assert!(last.is_some());
26/// ```
27#[derive(Debug, Clone)]
28pub struct Kama {
29    er_period: usize,
30    fast_sc: f64,
31    slow_sc: f64,
32    window: VecDeque<f64>,
33    state: Option<f64>,
34}
35
36impl Kama {
37    /// # Errors
38    /// Returns [`Error::PeriodZero`] / [`Error::InvalidPeriod`] for bad parameters.
39    pub fn new(er_period: usize, fast: usize, slow: usize) -> Result<Self> {
40        if er_period == 0 || fast == 0 || slow == 0 {
41            return Err(Error::PeriodZero);
42        }
43        if fast >= slow {
44            return Err(Error::InvalidPeriod {
45                message: "KAMA fast period must be strictly less than slow",
46            });
47        }
48        let fast_sc = 2.0 / (fast as f64 + 1.0);
49        let slow_sc = 2.0 / (slow as f64 + 1.0);
50        Ok(Self {
51            er_period,
52            fast_sc,
53            slow_sc,
54            window: VecDeque::with_capacity(er_period + 1),
55            state: None,
56        })
57    }
58
59    /// Classic Kaufman parameters: (10, 2, 30).
60    pub fn classic() -> Self {
61        Self::new(10, 2, 30).expect("classic KAMA parameters are valid")
62    }
63
64    /// Configured `(er_period, fast, slow)` periods.
65    pub fn periods(&self) -> (usize, f64, f64) {
66        (self.er_period, self.fast_sc, self.slow_sc)
67    }
68}
69
70impl Indicator for Kama {
71    type Input = f64;
72    type Output = f64;
73
74    #[inline]
75    fn update(&mut self, input: f64) -> Option<f64> {
76        if !input.is_finite() {
77            return None;
78        }
79        if self.window.len() == self.er_period + 1 {
80            self.window.pop_front();
81        }
82        self.window.push_back(input);
83
84        if self.window.len() < self.er_period + 1 {
85            return None;
86        }
87
88        let first = *self.window.front().expect("non-empty");
89        let last = *self.window.back().expect("non-empty");
90        let direction = (last - first).abs();
91        let volatility: f64 = self
92            .window
93            .iter()
94            .zip(self.window.iter().skip(1))
95            .map(|(a, b)| (b - a).abs())
96            .sum();
97
98        let er = if volatility == 0.0 {
99            0.0
100        } else {
101            direction / volatility
102        };
103        let sc = (er * (self.fast_sc - self.slow_sc) + self.slow_sc).powi(2);
104
105        let prev = self.state.unwrap_or(first);
106        let new = prev + sc * (input - prev);
107        self.state = Some(new);
108        Some(new)
109    }
110
111    fn reset(&mut self) {
112        self.window.clear();
113        self.state = None;
114    }
115
116    #[inline]
117    fn warmup_period(&self) -> usize {
118        self.er_period + 1
119    }
120
121    #[inline]
122    fn is_ready(&self) -> bool {
123        self.state.is_some()
124    }
125
126    #[inline]
127    fn name(&self) -> &'static str {
128        "KAMA"
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::traits::BatchExt;
136    use approx::assert_relative_eq;
137
138    /// Cover the `periods` accessor (65-67) and the Indicator-impl
139    /// `warmup_period` (115-117) + `name` (123-125). Existing tests
140    /// inspect KAMA output but never query the metadata.
141    #[test]
142    fn accessors_and_metadata() {
143        let k = Kama::classic();
144        let (er, fast, slow) = k.periods();
145        assert_eq!(er, 10);
146        assert!((fast - 2.0 / (2.0 + 1.0)).abs() < 1e-12);
147        assert!((slow - 2.0 / (30.0 + 1.0)).abs() < 1e-12);
148        assert_eq!(k.warmup_period(), 11);
149        assert_eq!(k.name(), "KAMA");
150    }
151
152    #[test]
153    fn constant_series_yields_constant_kama() {
154        let mut k = Kama::classic();
155        let out = k.batch(&[100.0_f64; 100]);
156        let last = out.iter().rev().flatten().next().unwrap();
157        assert_relative_eq!(*last, 100.0, epsilon = 1e-9);
158    }
159
160    #[test]
161    fn rejects_invalid_periods() {
162        assert!(Kama::new(0, 2, 30).is_err());
163        assert!(Kama::new(10, 30, 2).is_err()); // fast >= slow
164        assert!(Kama::new(10, 2, 2).is_err()); // fast == slow
165    }
166
167    #[test]
168    fn batch_equals_streaming() {
169        let prices: Vec<f64> = (1..=120)
170            .map(|i| (f64::from(i) * 0.2).sin() * 5.0 + f64::from(i) * 0.1)
171            .collect();
172        let mut a = Kama::classic();
173        let mut b = Kama::classic();
174        assert_eq!(
175            a.batch(&prices),
176            prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
177        );
178    }
179
180    #[test]
181    fn reset_clears_state() {
182        let mut k = Kama::classic();
183        k.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
184        assert!(k.is_ready());
185        k.reset();
186        assert!(!k.is_ready());
187    }
188
189    #[test]
190    fn ignores_non_finite_input() {
191        let mut k = Kama::classic();
192        k.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
193        let before = k.update(41.0);
194        assert!(before.is_some());
195        // Non-finite inputs return the last state without sliding the window.
196        assert_eq!(k.update(f64::NAN), None);
197        assert_eq!(k.update(f64::INFINITY), None);
198    }
199}