egui_charts/studies/builtin/
kama.rs1use 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#[derive(Clone)]
41pub struct KAMA {
42 period: usize,
44 fast_period: usize,
46 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 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 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 pub fn with_color(mut self, color: Color32) -> Self {
91 self.color = color;
92 self
93 }
94}
95
96impl 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 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 #[test]
216 fn calculate_matches_kaufman_reference() {
217 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 (
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}