Skip to main content

quantwave_core/indicators/
kama.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as VecDeque;
4
5/// Kaufman's Adaptive Moving Average (KAMA)
6///
7/// KAMA is an adaptive moving average that adjusts its smoothing based on the
8/// efficiency of price movement (signal-to-noise ratio).
9#[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        // Efficiency Ratio (ER)
56        // Signal = abs(Price - Price[N])
57        let oldest = self.window.back().copied().unwrap_or(input);
58        let signal = (input - oldest).abs();
59
60        // Noise = sum(abs(Price - Price[1]), N)
61        let mut noise = 0.0;
62        for i in 0..self.period {
63            noise += (self.window[i] - self.window[i + 1]).abs();
64        }
65
66        let er = if noise != 0.0 { signal / noise } else { 0.0 };
67
68        // Smoothing Constant (SC)
69        let sc = (er * (self.fast_sc - self.slow_sc) + self.slow_sc).powi(2);
70
71        // KAMA
72        let prev = self.prev_kama.unwrap_or(input);
73        let kama = prev + sc * (input - prev);
74        self.prev_kama = Some(kama);
75
76        kama
77    }
78}
79
80pub const KAMA_METADATA: IndicatorMetadata = IndicatorMetadata {
81    name: "KAMA",
82    description: "Kaufman's Adaptive Moving Average adjusts its sensitivity based on market volatility.",
83    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.",
84    keywords: &["moving-average", "adaptive", "smoothing", "classic"],
85    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.",
86    params: &[
87        ParamDef {
88            name: "period",
89            default: "10",
90            description: "Efficiency Ratio lookback period",
91        },
92        ParamDef {
93            name: "fast_period",
94            default: "2",
95            description: "Fastest smoothing period",
96        },
97        ParamDef {
98            name: "slow_period",
99            default: "30",
100            description: "Slowest smoothing period",
101        },
102    ],
103    formula_source: "https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:kaufman_s_adaptive_moving_average",
104    formula_latex: r#"
105\[
106ER = \frac{|Price - Price_{t-n}|}{\sum |Price - Price_{t-1}|}
107\]
108\[
109SC = [ER(FastSC - SlowSC) + SlowSC]^2
110\]
111\[
112KAMA = KAMA_{t-1} + SC(Price - KAMA_{t-1})
113\]
114"#,
115    gold_standard_file: "kama.json",
116    category: "Classic",
117};
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::traits::Next;
123    use proptest::prelude::*;
124
125    #[test]
126    fn test_kama_basic() {
127        let mut kama = Kama::new(10, 2, 30);
128        let inputs = vec![
129            10.0, 11.0, 10.5, 12.0, 13.0, 14.0, 13.5, 15.0, 16.0, 17.0, 18.0, 19.0,
130        ];
131        for input in inputs {
132            let res = kama.next(input);
133            assert!(!res.is_nan());
134        }
135    }
136
137    proptest! {
138        #[test]
139        fn test_kama_parity(
140            inputs in prop::collection::vec(1.0..100.0, 50..100),
141        ) {
142            let period = 10;
143            let mut kama = Kama::new(period, 2, 30);
144            let streaming_results: Vec<f64> = inputs.iter().map(|&x| kama.next(x)).collect();
145
146            let mut prev_kama = None;
147            let fast_sc = 2.0 / (2.0 + 1.0);
148            let slow_sc = 2.0 / (30.0 + 1.0);
149
150            for (i, &input) in inputs.iter().enumerate() {
151                if i < period {
152                    if prev_kama.is_none() { prev_kama = Some(input); }
153                    approx::assert_relative_eq!(streaming_results[i], input, epsilon = 1e-10);
154                    continue;
155                }
156
157                let signal = (input - inputs[i - period]).abs();
158                let mut noise = 0.0;
159                for j in 0..period {
160                    noise += (inputs[i-j] - inputs[i-j-1]).abs();
161                }
162
163                let er = if noise != 0.0 { signal / noise } else { 0.0 };
164                let sc = (er * (fast_sc - slow_sc) + slow_sc).powi(2);
165                let current_kama = prev_kama.unwrap() + sc * (input - prev_kama.unwrap());
166
167                approx::assert_relative_eq!(streaming_results[i], current_kama, epsilon = 1e-10);
168                prev_kama = Some(current_kama);
169            }
170        }
171    }
172}