Skip to main content

kestrel_chartkit/indicator/
moving_averages.rs

1use std::collections::VecDeque;
2
3use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
4use crate::model::Bar;
5
6/// Simple Moving Average (SMA).
7pub struct SmaEngine {
8    period: usize,
9    closes: VecDeque<f64>,
10    alerts: Vec<IndicatorAlert>,
11}
12
13impl SmaEngine {
14    pub fn new(period: usize) -> Self {
15        Self {
16            period,
17            closes: VecDeque::new(),
18            alerts: Vec::new(),
19        }
20    }
21}
22
23impl Indicator for SmaEngine {
24    fn name(&self) -> &str {
25        "sma"
26    }
27
28    fn warmup_period(&self) -> usize {
29        self.period
30    }
31
32    fn reset(&mut self) {
33        self.closes.clear();
34        self.alerts.clear();
35    }
36
37    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
38        self.closes.push_back(bar.close);
39        if self.closes.len() > self.period {
40            self.closes.pop_front();
41        }
42
43        self.alerts.clear();
44        if self.closes.len() < self.period {
45            return None;
46        }
47
48        let sma = self.closes.iter().sum::<f64>() / self.period as f64;
49        Some(IndicatorOutput::new(sma))
50    }
51
52    fn alerts(&self) -> Vec<IndicatorAlert> {
53        self.alerts.clone()
54    }
55}
56
57/// Exponential Moving Average (EMA).
58#[derive(Debug, Clone)]
59pub struct EmaEngine {
60    period: usize,
61    current_ema: Option<f64>,
62    count: usize,
63    alerts: Vec<IndicatorAlert>,
64}
65
66impl EmaEngine {
67    pub fn new(period: usize) -> Self {
68        Self {
69            period,
70            current_ema: None,
71            count: 0,
72            alerts: Vec::new(),
73        }
74    }
75}
76
77impl Indicator for EmaEngine {
78    fn name(&self) -> &str {
79        "ema"
80    }
81
82    fn warmup_period(&self) -> usize {
83        self.period
84    }
85
86    fn reset(&mut self) {
87        self.current_ema = None;
88        self.count = 0;
89        self.alerts.clear();
90    }
91
92    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
93        self.count += 1;
94        let k = 2.0 / (self.period as f64 + 1.0);
95        let ema = match self.current_ema {
96            Some(prev) => bar.close * k + prev * (1.0 - k),
97            None => bar.close,
98        };
99        self.current_ema = Some(ema);
100
101        self.alerts.clear();
102        if self.count < self.period {
103            return None;
104        }
105
106        Some(IndicatorOutput::new(ema))
107    }
108
109    fn alerts(&self) -> Vec<IndicatorAlert> {
110        self.alerts.clone()
111    }
112}
113
114/// Weighted Moving Average (WMA).
115pub struct WmaEngine {
116    period: usize,
117    closes: VecDeque<f64>,
118    alerts: Vec<IndicatorAlert>,
119}
120
121impl WmaEngine {
122    pub fn new(period: usize) -> Self {
123        Self {
124            period,
125            closes: VecDeque::new(),
126            alerts: Vec::new(),
127        }
128    }
129}
130
131impl Indicator for WmaEngine {
132    fn name(&self) -> &str {
133        "wma"
134    }
135
136    fn warmup_period(&self) -> usize {
137        self.period
138    }
139
140    fn reset(&mut self) {
141        self.closes.clear();
142        self.alerts.clear();
143    }
144
145    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
146        self.closes.push_back(bar.close);
147        if self.closes.len() > self.period {
148            self.closes.pop_front();
149        }
150
151        self.alerts.clear();
152        if self.closes.len() < self.period {
153            return None;
154        }
155
156        let mut weight_sum = 0.0f64;
157        let mut weighted_val = 0.0f64;
158        for (i, &val) in self.closes.iter().enumerate() {
159            let w = (i + 1) as f64;
160            weighted_val += val * w;
161            weight_sum += w;
162        }
163
164        let wma = if weight_sum > 0.0 {
165            weighted_val / weight_sum
166        } else {
167            bar.close
168        };
169        Some(IndicatorOutput::new(wma))
170    }
171
172    fn alerts(&self) -> Vec<IndicatorAlert> {
173        self.alerts.clone()
174    }
175}
176
177/// Volume-Weighted Moving Average (VWMA).
178pub struct VwmaEngine {
179    period: usize,
180    bars: VecDeque<Bar>,
181    alerts: Vec<IndicatorAlert>,
182}
183
184impl VwmaEngine {
185    pub fn new(period: usize) -> Self {
186        Self {
187            period,
188            bars: VecDeque::new(),
189            alerts: Vec::new(),
190        }
191    }
192}
193
194impl Indicator for VwmaEngine {
195    fn name(&self) -> &str {
196        "vwma"
197    }
198
199    fn warmup_period(&self) -> usize {
200        self.period
201    }
202
203    fn reset(&mut self) {
204        self.bars.clear();
205        self.alerts.clear();
206    }
207
208    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
209        self.bars.push_back(bar.clone());
210        if self.bars.len() > self.period {
211            self.bars.pop_front();
212        }
213
214        self.alerts.clear();
215        if self.bars.len() < self.period {
216            return None;
217        }
218
219        let mut pv_sum = 0.0f64;
220        let mut v_sum = 0.0f64;
221        for b in &self.bars {
222            pv_sum += b.close * b.volume;
223            v_sum += b.volume;
224        }
225
226        let vwma = if v_sum > 0.0 {
227            pv_sum / v_sum
228        } else {
229            bar.close
230        };
231        Some(IndicatorOutput::new(vwma))
232    }
233
234    fn alerts(&self) -> Vec<IndicatorAlert> {
235        self.alerts.clone()
236    }
237}
238
239/// Hull Moving Average (HMA).
240pub struct HmaEngine {
241    period: usize,
242    wma_half: WmaEngine,
243    wma_full: WmaEngine,
244    wma_sqrt: WmaEngine,
245    alerts: Vec<IndicatorAlert>,
246}
247
248impl HmaEngine {
249    pub fn new(period: usize) -> Self {
250        let half = (period / 2).max(1);
251        let sqrt = ((period as f64).sqrt().round() as usize).max(1);
252        Self {
253            period,
254            wma_half: WmaEngine::new(half),
255            wma_full: WmaEngine::new(period),
256            wma_sqrt: WmaEngine::new(sqrt),
257            alerts: Vec::new(),
258        }
259    }
260}
261
262impl Indicator for HmaEngine {
263    fn name(&self) -> &str {
264        "hma"
265    }
266
267    fn warmup_period(&self) -> usize {
268        self.period + ((self.period as f64).sqrt().round() as usize)
269    }
270
271    fn reset(&mut self) {
272        self.wma_half.reset();
273        self.wma_full.reset();
274        self.wma_sqrt.reset();
275        self.alerts.clear();
276    }
277
278    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
279        let h_out = self.wma_half.on_bar(bar);
280        let f_out = self.wma_full.on_bar(bar);
281
282        self.alerts.clear();
283        if let (Some(h), Some(f)) = (h_out, f_out) {
284            let diff = 2.0 * h.value - f.value;
285            let synthetic_bar = Bar::new(bar.timestamp, diff, diff, diff, diff, 1.0);
286            return self.wma_sqrt.on_bar(&synthetic_bar);
287        }
288
289        None
290    }
291
292    fn alerts(&self) -> Vec<IndicatorAlert> {
293        self.alerts.clone()
294    }
295}
296
297/// Double Exponential Moving Average (DEMA).
298pub struct DemaEngine {
299    period: usize,
300    ema1: EmaEngine,
301    ema2: EmaEngine,
302    alerts: Vec<IndicatorAlert>,
303}
304
305impl DemaEngine {
306    pub fn new(period: usize) -> Self {
307        Self {
308            period,
309            ema1: EmaEngine::new(period),
310            ema2: EmaEngine::new(period),
311            alerts: Vec::new(),
312        }
313    }
314}
315
316impl Indicator for DemaEngine {
317    fn name(&self) -> &str {
318        "dema"
319    }
320
321    fn warmup_period(&self) -> usize {
322        self.period * 2
323    }
324
325    fn reset(&mut self) {
326        self.ema1.reset();
327        self.ema2.reset();
328        self.alerts.clear();
329    }
330
331    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
332        let e1_out = self.ema1.on_bar(bar);
333        self.alerts.clear();
334
335        if let Some(e1) = e1_out {
336            let synth_bar = Bar::new(bar.timestamp, e1.value, e1.value, e1.value, e1.value, 1.0);
337            let e2_out = self.ema2.on_bar(&synth_bar);
338            if let Some(e2) = e2_out {
339                let dema = 2.0 * e1.value - e2.value;
340                return Some(IndicatorOutput::new(dema));
341            }
342        }
343
344        None
345    }
346
347    fn alerts(&self) -> Vec<IndicatorAlert> {
348        self.alerts.clone()
349    }
350}
351
352/// Kaufman's Adaptive Moving Average (KAMA).
353pub struct KamaEngine {
354    period: usize,
355    fast_period: usize,
356    slow_period: usize,
357    closes: VecDeque<f64>,
358    current_kama: Option<f64>,
359    alerts: Vec<IndicatorAlert>,
360}
361
362impl KamaEngine {
363    pub fn new(period: usize, fast_period: usize, slow_period: usize) -> Self {
364        Self {
365            period,
366            fast_period,
367            slow_period,
368            closes: VecDeque::new(),
369            current_kama: None,
370            alerts: Vec::new(),
371        }
372    }
373}
374
375impl Indicator for KamaEngine {
376    fn name(&self) -> &str {
377        "kama"
378    }
379
380    fn warmup_period(&self) -> usize {
381        self.period + 1
382    }
383
384    fn reset(&mut self) {
385        self.closes.clear();
386        self.current_kama = None;
387        self.alerts.clear();
388    }
389
390    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
391        self.closes.push_back(bar.close);
392        if self.closes.len() > self.period + 1 {
393            self.closes.pop_front();
394        }
395
396        self.alerts.clear();
397        if self.closes.len() < self.period + 1 {
398            return None;
399        }
400
401        let change = (self.closes.back().unwrap() - self.closes.front().unwrap()).abs();
402        let mut volatility = 0.0f64;
403        for pair in self.closes.iter().collect::<Vec<_>>().windows(2) {
404            volatility += (*pair[1] - *pair[0]).abs();
405        }
406
407        let er = if volatility > 0.0 {
408            change / volatility
409        } else {
410            0.0
411        };
412
413        let fast_sc = 2.0 / (self.fast_period as f64 + 1.0);
414        let slow_sc = 2.0 / (self.slow_period as f64 + 1.0);
415        let sc = (er * (fast_sc - slow_sc) + slow_sc).powi(2);
416
417        let kama = match self.current_kama {
418            Some(prev) => prev + sc * (bar.close - prev),
419            None => bar.close,
420        };
421        self.current_kama = Some(kama);
422
423        Some(IndicatorOutput::new(kama))
424    }
425
426    fn alerts(&self) -> Vec<IndicatorAlert> {
427        self.alerts.clone()
428    }
429}