Skip to main content

egui_charts/studies/builtin/
kama.rs

1//! Kaufman Adaptive Moving Average (KAMA) indicator.
2//!
3//! KAMA is an adaptive moving average that adjusts its smoothing speed based
4//! on the market's efficiency ratio. In trending markets it becomes more
5//! responsive; in choppy, ranging markets it slows down to filter noise.
6//!
7//! # Formula
8//!
9//! ```text
10//! Efficiency Ratio (ER) = |Close - Close[N]| / Sum(|Close[i] - Close[i-1]|, N)
11//! Smoothing Constant (SC) = [ER * (fast_sc - slow_sc) + slow_sc]^2
12//! KAMA[t] = KAMA[t-1] + SC * (Close[t] - KAMA[t-1])
13//! ```
14//!
15//! # Default parameters
16//!
17//! `KAMA::new(10)` with fast period 2 and slow period 30.
18//!
19//! # Example
20//!
21//! ```rust
22//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
23//! use egui_charts::studies::{KAMA, Indicator};
24//!
25//! let mut kama = KAMA::new(10);
26//! kama.calculate(&bars);
27//! ```
28
29use crate::model::Bar;
30use crate::studies::palette;
31use crate::studies::stepper::Stepper;
32use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
33use egui::Color32;
34use std::collections::VecDeque;
35
36/// Kaufman Adaptive Moving Average indicator.
37///
38/// Adapts its smoothing speed to market conditions via the efficiency
39/// ratio. Overlay indicator drawn on the price chart.
40#[derive(Clone)]
41pub struct KAMA {
42    /// Period for Efficiency Ratio calculation.
43    period: usize,
44    /// Fastest EMA period (typically 2).
45    fast_period: usize,
46    /// Slowest EMA period (typically 30).
47    slow_period: usize,
48    values: IndicatorValues,
49    stepper: Stepper<State>,
50    color: Color32,
51    visible: bool,
52}
53
54#[derive(Clone, Default)]
55struct State {
56    closes: VecDeque<f64>,
57    seed_sum: f64,
58    volatility: f64,
59    kama: Option<f64>,
60}
61
62impl KAMA {
63    /// Create a new KAMA indicator.
64    ///
65    /// # Arguments
66    /// * `period` -- Efficiency ratio lookback period (default: 10).
67    ///
68    /// Fast/slow periods default to 2 and 30 respectively. Use
69    /// [`with_periods`](Self::with_periods) to customise.
70    pub fn new(period: usize) -> Self {
71        Self {
72            period: period.max(1),
73            fast_period: 2,
74            slow_period: 30,
75            values: IndicatorValues::new(),
76            stepper: Stepper::default(),
77            color: palette::BLUE,
78            visible: true,
79        }
80    }
81
82    /// Set custom fast and slow EMA periods.
83    pub fn with_periods(mut self, fast: usize, slow: usize) -> Self {
84        self.fast_period = fast.max(1);
85        self.slow_period = slow.max(1);
86        self
87    }
88
89    /// Set a custom line colour (builder pattern).
90    pub fn with_color(mut self, color: Color32) -> Self {
91        self.color = color;
92        self
93    }
94}
95
96/// Construct with the conventional default parameters.
97impl Default for KAMA {
98    fn default() -> Self {
99        Self::new(10)
100    }
101}
102
103impl Indicator for KAMA {
104    fn source(&self) -> Option<crate::studies::StudySource> {
105        Some(crate::studies::StudySource::Price(
106            crate::model::PriceSource::Close,
107        ))
108    }
109
110    crate::studies::inputs::study_inputs!(self;
111        period: length("Period", 1, usize::MAX),
112        fast_period: length("Fast period", 1, usize::MAX),
113        slow_period: length("Slow period", 1, usize::MAX)
114    ; {});
115
116    fn name(&self) -> &str {
117        "KAMA"
118    }
119
120    fn desc(&self) -> &str {
121        "Kaufman Adaptive Moving Avg - Adjusts smoothing based on market efficiency"
122    }
123
124    fn calculate(&mut self, data: &[Bar]) {
125        self.update(data, 0);
126    }
127
128    fn update(&mut self, data: &[Bar], from: usize) {
129        let fast = 2.0 / (self.fast_period as f64 + 1.0);
130        let slow = 2.0 / (self.slow_period as f64 + 1.0);
131        self.stepper
132            .run(data, from, &mut self.values, |state, bar| {
133                let close = bar.close;
134                if !close.is_finite() {
135                    return IndicatorValue::None;
136                }
137                if let Some(previous) = state.closes.back() {
138                    state.volatility += (close - previous).abs();
139                }
140                state.closes.push_back(close);
141                let next = if let Some(previous) = state.kama {
142                    // N + 1 closes supply exactly N price changes.
143                    let first = state.closes.pop_front().unwrap();
144                    let er = if state.volatility > 0.0 {
145                        (close - first).abs() / state.volatility
146                    } else {
147                        0.0
148                    };
149                    let sc = (er * (fast - slow) + slow).powi(2);
150                    state.volatility =
151                        (state.volatility - (state.closes.front().unwrap() - first).abs()).max(0.0);
152                    previous + sc * (close - previous)
153                } else {
154                    state.seed_sum += close;
155                    if state.closes.len() < self.period {
156                        return IndicatorValue::None;
157                    }
158                    state.seed_sum / self.period as f64
159                };
160                state.kama = Some(next);
161                IndicatorValue::Single(next)
162            });
163    }
164
165    fn is_incremental(&self) -> bool {
166        true
167    }
168
169    fn values(&self) -> &IndicatorValues {
170        &self.values
171    }
172
173    fn colors(&self) -> Vec<Color32> {
174        vec![self.color]
175    }
176
177    fn set_colors(&mut self, colors: Vec<Color32>) {
178        if !colors.is_empty() {
179            self.color = colors[0];
180        }
181    }
182
183    fn is_overlay(&self) -> bool {
184        true
185    }
186
187    fn is_visible(&self) -> bool {
188        self.visible
189    }
190
191    fn set_visible(&mut self, visible: bool) {
192        self.visible = visible;
193    }
194
195    fn clone_box(&self) -> Box<dyn Indicator> {
196        Box::new(self.clone())
197    }
198
199    fn line_names(&self) -> Vec<String> {
200        vec![format!(
201            "KAMA({}, {}, {})",
202            self.period, self.fast_period, self.slow_period
203        )]
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use crate::studies::test_support::make_bar;
211
212    /// Expected values from Kaufman's definition (n price changes per
213    /// efficiency ratio), seeded with the simple average of the first n
214    /// closes; fast 2, slow 30.
215    #[test]
216    fn calculate_matches_kaufman_reference() {
217        // (name, period, closes, expected KAMA per bar)
218        type Case = (&'static str, usize, &'static [f64], &'static [Option<f64>]);
219        let cases: &[Case] = &[
220            (
221                "choppy",
222                4,
223                &[10.0, 11.0, 10.5, 12.0, 11.0, 13.0, 12.5, 14.0, 13.0, 13.5],
224                &[
225                    None,
226                    None,
227                    None,
228                    Some(10.875),
229                    Some(10.88078101514626),
230                    Some(11.078408170242067),
231                    Some(11.210978306235772),
232                    Some(11.47106771973126),
233                    Some(11.613647858472564),
234                    Some(11.656395577253855),
235                ],
236            ),
237            (
238                "trend",
239                3,
240                &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
241                &[
242                    None,
243                    None,
244                    Some(2.0),
245                    Some(2.8888888888888884),
246                    Some(3.8271604938271597),
247                    Some(4.792866941015088),
248                ],
249            ),
250            (
251                "flat",
252                3,
253                &[7.0; 5],
254                &[None, None, Some(7.0), Some(7.0), Some(7.0)],
255            ),
256            ("short", 5, &[1.0, 2.0, 3.0], &[None, None, None]),
257            // The NaN close has no value; the next efficiency ratio spans the closes around it.
258            (
259                "NaN close",
260                2,
261                &[1.0, 2.0, f64::NAN, 4.0, 5.0, 6.0, 7.0],
262                &[
263                    None,
264                    Some(1.5),
265                    None,
266                    Some(2.611111111111111),
267                    Some(3.6728395061728394),
268                    Some(4.707133058984911),
269                    Some(5.726185032769395),
270                ],
271            ),
272        ];
273
274        for (name, period, closes, expected) in cases {
275            let data: Vec<Bar> = closes.iter().map(|&c| make_bar(c)).collect();
276            let mut kama = KAMA::new(*period);
277            for len in 0..=data.len() {
278                kama.update(&data[..len], len.saturating_sub(1));
279            }
280            assert_eq!(kama.values.len(), expected.len(), "{name}");
281            for (i, (got, want)) in kama
282                .values
283                .to_rows()
284                .iter()
285                .zip(expected.iter())
286                .enumerate()
287            {
288                match (got, want) {
289                    (IndicatorValue::None, None) => {}
290                    (IndicatorValue::Single(g), Some(w)) => {
291                        assert!((g - w).abs() < 1e-12, "{name}[{i}]: got {g}, want {w}")
292                    }
293                    _ => panic!("{name}[{i}]: got {got:?}, want {want:?}"),
294                }
295            }
296        }
297    }
298}