quantwave_core/indicators/
kama.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as VecDeque;
4
5#[derive(Debug, Clone)]
10pub struct Kama {
11 period: usize,
12 fast_sc: f64,
13 slow_sc: f64,
14 window: VecDeque<f64>,
15 prev_kama: Option<f64>,
16}
17
18impl Kama {
19 pub fn new(period: usize, fast_period: usize, slow_period: usize) -> Self {
20 let fast_sc = 2.0 / (fast_period as f64 + 1.0);
21 let slow_sc = 2.0 / (slow_period as f64 + 1.0);
22
23 Self {
24 period,
25 fast_sc,
26 slow_sc,
27 window: VecDeque::with_capacity(period + 1),
28 prev_kama: None,
29 }
30 }
31}
32
33impl Default for Kama {
34 fn default() -> Self {
35 Self::new(10, 2, 30)
36 }
37}
38
39impl Next<f64> for Kama {
40 type Output = f64;
41
42 fn next(&mut self, input: f64) -> Self::Output {
43 self.window.push_front(input);
44 if self.window.len() > self.period + 1 {
45 self.window.pop_back();
46 }
47
48 if self.window.len() <= self.period {
49 if self.prev_kama.is_none() {
50 self.prev_kama = Some(input);
51 }
52 return input;
53 }
54
55 let signal = (input - self.window.back().unwrap()).abs();
58
59 let mut noise = 0.0;
61 for i in 0..self.period {
62 noise += (self.window[i] - self.window[i + 1]).abs();
63 }
64
65 let er = if noise != 0.0 { signal / noise } else { 0.0 };
66
67 let sc = (er * (self.fast_sc - self.slow_sc) + self.slow_sc).powi(2);
69
70 let prev = self.prev_kama.unwrap_or(input);
72 let kama = prev + sc * (input - prev);
73 self.prev_kama = Some(kama);
74
75 kama
76 }
77}
78
79pub const KAMA_METADATA: IndicatorMetadata = IndicatorMetadata {
80 name: "KAMA",
81 description: "Kaufman's Adaptive Moving Average adjusts its sensitivity based on market volatility.",
82 usage: "Use as an adaptive moving average that is fast in trending markets and slow in choppy, sideways conditions. Reduces whipsaws that plague fixed-period moving averages in ranging markets.",
83 keywords: &["moving-average", "adaptive", "smoothing", "classic"],
84 ehlers_summary: "Perry Kaufman designed KAMA using an Efficiency Ratio that measures how directionally price has moved versus total path length. A high ratio (strong trend) produces a fast-reacting EMA; a low ratio (choppy market) produces a near-flat line, dramatically reducing false signals during consolidation. — New Trading Systems and Methods, 4th ed.",
85 params: &[
86 ParamDef {
87 name: "period",
88 default: "10",
89 description: "Efficiency Ratio lookback period",
90 },
91 ParamDef {
92 name: "fast_period",
93 default: "2",
94 description: "Fastest smoothing period",
95 },
96 ParamDef {
97 name: "slow_period",
98 default: "30",
99 description: "Slowest smoothing period",
100 },
101 ],
102 formula_source: "https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:kaufman_s_adaptive_moving_average",
103 formula_latex: r#"
104\[
105ER = \frac{|Price - Price_{t-n}|}{\sum |Price - Price_{t-1}|}
106\]
107\[
108SC = [ER(FastSC - SlowSC) + SlowSC]^2
109\]
110\[
111KAMA = KAMA_{t-1} + SC(Price - KAMA_{t-1})
112\]
113"#,
114 gold_standard_file: "kama.json",
115 category: "Classic",
116};
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::traits::Next;
122 use proptest::prelude::*;
123
124 #[test]
125 fn test_kama_basic() {
126 let mut kama = Kama::new(10, 2, 30);
127 let inputs = vec![
128 10.0, 11.0, 10.5, 12.0, 13.0, 14.0, 13.5, 15.0, 16.0, 17.0, 18.0, 19.0,
129 ];
130 for input in inputs {
131 let res = kama.next(input);
132 assert!(!res.is_nan());
133 }
134 }
135
136 proptest! {
137 #[test]
138 fn test_kama_parity(
139 inputs in prop::collection::vec(1.0..100.0, 50..100),
140 ) {
141 let period = 10;
142 let mut kama = Kama::new(period, 2, 30);
143 let streaming_results: Vec<f64> = inputs.iter().map(|&x| kama.next(x)).collect();
144
145 let mut prev_kama = None;
146 let fast_sc = 2.0 / (2.0 + 1.0);
147 let slow_sc = 2.0 / (30.0 + 1.0);
148
149 for (i, &input) in inputs.iter().enumerate() {
150 if i < period {
151 if prev_kama.is_none() { prev_kama = Some(input); }
152 approx::assert_relative_eq!(streaming_results[i], input, epsilon = 1e-10);
153 continue;
154 }
155
156 let signal = (input - inputs[i - period]).abs();
157 let mut noise = 0.0;
158 for j in 0..period {
159 noise += (inputs[i-j] - inputs[i-j-1]).abs();
160 }
161
162 let er = if noise != 0.0 { signal / noise } else { 0.0 };
163 let sc = (er * (fast_sc - slow_sc) + slow_sc).powi(2);
164 let current_kama = prev_kama.unwrap() + sc * (input - prev_kama.unwrap());
165
166 approx::assert_relative_eq!(streaming_results[i], current_kama, epsilon = 1e-10);
167 prev_kama = Some(current_kama);
168 }
169 }
170 }
171}