wickra_core/indicators/
kama.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[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 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 pub fn classic() -> Self {
61 Self::new(10, 2, 30).expect("classic KAMA parameters are valid")
62 }
63
64 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 #[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()); assert!(Kama::new(10, 2, 2).is_err()); }
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 assert_eq!(k.update(f64::NAN), None);
197 assert_eq!(k.update(f64::INFINITY), None);
198 }
199}