kestrel_chartkit/indicator/
moving_averages.rs1use std::collections::VecDeque;
2
3use crate::indicator::smoothing::{Ema, EmaInit};
4use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
5use crate::model::Bar;
6
7pub struct SmaEngine {
14 period: usize,
15 closes: VecDeque<f64>,
16 alerts: Vec<IndicatorAlert>,
17}
18
19impl SmaEngine {
20 pub fn new(period: usize) -> Self {
21 Self {
22 period,
23 closes: VecDeque::new(),
24 alerts: Vec::new(),
25 }
26 }
27}
28
29impl Indicator for SmaEngine {
30 fn name(&self) -> &str {
31 "sma"
32 }
33
34 fn warmup_period(&self) -> usize {
35 self.period
36 }
37
38 fn reset(&mut self) {
39 self.closes.clear();
40 self.alerts.clear();
41 }
42
43 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
44 self.closes.push_back(bar.close);
45 if self.closes.len() > self.period {
46 self.closes.pop_front();
47 }
48
49 self.alerts.clear();
50 if self.closes.len() < self.period {
51 return None;
52 }
53
54 let sma = self.closes.iter().sum::<f64>() / self.period as f64;
55 Some(IndicatorOutput::new(sma))
56 }
57
58 fn alerts(&self) -> Vec<IndicatorAlert> {
59 self.alerts.clone()
60 }
61}
62
63#[derive(Debug, Clone)]
73pub struct EmaEngine {
74 period: usize,
75 ema: Ema,
76 count: usize,
77 alerts: Vec<IndicatorAlert>,
78}
79
80impl EmaEngine {
81 pub fn new(period: usize) -> Self {
82 Self {
83 period,
84 ema: Ema::new(period),
85 count: 0,
86 alerts: Vec::new(),
87 }
88 }
89
90 pub fn with_init(mut self, init: EmaInit) -> Self {
94 self.ema = self.ema.with_init(init);
95 self
96 }
97
98 pub fn init(&self) -> EmaInit {
99 self.ema.init()
100 }
101}
102
103impl Indicator for EmaEngine {
104 fn name(&self) -> &str {
105 "ema"
106 }
107
108 fn warmup_period(&self) -> usize {
109 self.period
110 }
111
112 fn reset(&mut self) {
113 self.ema.reset();
114 self.count = 0;
115 self.alerts.clear();
116 }
117
118 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
119 self.count += 1;
120 let ema = self.ema.update(bar.close);
121
122 self.alerts.clear();
123 if self.count < self.period {
124 return None;
125 }
126
127 ema.map(IndicatorOutput::new)
128 }
129
130 fn alerts(&self) -> Vec<IndicatorAlert> {
131 self.alerts.clone()
132 }
133}
134
135pub struct WmaEngine {
142 period: usize,
143 closes: VecDeque<f64>,
144 alerts: Vec<IndicatorAlert>,
145}
146
147impl WmaEngine {
148 pub fn new(period: usize) -> Self {
149 Self {
150 period,
151 closes: VecDeque::new(),
152 alerts: Vec::new(),
153 }
154 }
155}
156
157impl Indicator for WmaEngine {
158 fn name(&self) -> &str {
159 "wma"
160 }
161
162 fn warmup_period(&self) -> usize {
163 self.period
164 }
165
166 fn reset(&mut self) {
167 self.closes.clear();
168 self.alerts.clear();
169 }
170
171 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
172 self.closes.push_back(bar.close);
173 if self.closes.len() > self.period {
174 self.closes.pop_front();
175 }
176
177 self.alerts.clear();
178 if self.closes.len() < self.period {
179 return None;
180 }
181
182 let mut weight_sum = 0.0f64;
183 let mut weighted_val = 0.0f64;
184 for (i, &val) in self.closes.iter().enumerate() {
185 let w = (i + 1) as f64;
186 weighted_val += val * w;
187 weight_sum += w;
188 }
189
190 let wma = if weight_sum > 0.0 {
191 weighted_val / weight_sum
192 } else {
193 bar.close
194 };
195 Some(IndicatorOutput::new(wma))
196 }
197
198 fn alerts(&self) -> Vec<IndicatorAlert> {
199 self.alerts.clone()
200 }
201}
202
203pub struct VwmaEngine {
210 period: usize,
211 bars: VecDeque<Bar>,
212 alerts: Vec<IndicatorAlert>,
213}
214
215impl VwmaEngine {
216 pub fn new(period: usize) -> Self {
217 Self {
218 period,
219 bars: VecDeque::new(),
220 alerts: Vec::new(),
221 }
222 }
223}
224
225impl Indicator for VwmaEngine {
226 fn name(&self) -> &str {
227 "vwma"
228 }
229
230 fn warmup_period(&self) -> usize {
231 self.period
232 }
233
234 fn reset(&mut self) {
235 self.bars.clear();
236 self.alerts.clear();
237 }
238
239 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
240 self.bars.push_back(bar.clone());
241 if self.bars.len() > self.period {
242 self.bars.pop_front();
243 }
244
245 self.alerts.clear();
246 if self.bars.len() < self.period {
247 return None;
248 }
249
250 let mut pv_sum = 0.0f64;
251 let mut v_sum = 0.0f64;
252 for b in &self.bars {
253 pv_sum += b.close * b.volume;
254 v_sum += b.volume;
255 }
256
257 let vwma = if v_sum > 0.0 {
258 pv_sum / v_sum
259 } else {
260 bar.close
261 };
262 Some(IndicatorOutput::new(vwma))
263 }
264
265 fn alerts(&self) -> Vec<IndicatorAlert> {
266 self.alerts.clone()
267 }
268}
269
270pub struct HmaEngine {
280 period: usize,
281 wma_half: WmaEngine,
282 wma_full: WmaEngine,
283 wma_sqrt: WmaEngine,
284 alerts: Vec<IndicatorAlert>,
285}
286
287impl HmaEngine {
288 pub fn new(period: usize) -> Self {
289 let half = (period / 2).max(1);
290 let sqrt = ((period as f64).sqrt().round() as usize).max(1);
291 Self {
292 period,
293 wma_half: WmaEngine::new(half),
294 wma_full: WmaEngine::new(period),
295 wma_sqrt: WmaEngine::new(sqrt),
296 alerts: Vec::new(),
297 }
298 }
299}
300
301impl Indicator for HmaEngine {
302 fn name(&self) -> &str {
303 "hma"
304 }
305
306 fn warmup_period(&self) -> usize {
307 self.period + ((self.period as f64).sqrt().round() as usize)
308 }
309
310 fn reset(&mut self) {
311 self.wma_half.reset();
312 self.wma_full.reset();
313 self.wma_sqrt.reset();
314 self.alerts.clear();
315 }
316
317 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
318 let h_out = self.wma_half.on_bar(bar);
319 let f_out = self.wma_full.on_bar(bar);
320
321 self.alerts.clear();
322 if let (Some(h), Some(f)) = (h_out, f_out) {
323 let diff = 2.0 * h.value - f.value;
324 let synthetic_bar = Bar::new(bar.timestamp, diff, diff, diff, diff, 1.0);
325 return self.wma_sqrt.on_bar(&synthetic_bar);
326 }
327
328 None
329 }
330
331 fn alerts(&self) -> Vec<IndicatorAlert> {
332 self.alerts.clone()
333 }
334}
335
336pub struct DemaEngine {
349 period: usize,
350 ema1: EmaEngine,
351 ema2: EmaEngine,
352 alerts: Vec<IndicatorAlert>,
353}
354
355impl DemaEngine {
356 pub fn new(period: usize) -> Self {
357 Self {
358 period,
359 ema1: EmaEngine::new(period),
360 ema2: EmaEngine::new(period),
361 alerts: Vec::new(),
362 }
363 }
364}
365
366impl Indicator for DemaEngine {
367 fn name(&self) -> &str {
368 "dema"
369 }
370
371 fn warmup_period(&self) -> usize {
372 self.period * 2
373 }
374
375 fn reset(&mut self) {
376 self.ema1.reset();
377 self.ema2.reset();
378 self.alerts.clear();
379 }
380
381 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
382 let e1_out = self.ema1.on_bar(bar);
383 self.alerts.clear();
384
385 if let Some(e1) = e1_out {
386 let synth_bar = Bar::new(bar.timestamp, e1.value, e1.value, e1.value, e1.value, 1.0);
387 let e2_out = self.ema2.on_bar(&synth_bar);
388 if let Some(e2) = e2_out {
389 let dema = 2.0 * e1.value - e2.value;
390 return Some(IndicatorOutput::new(dema));
391 }
392 }
393
394 None
395 }
396
397 fn alerts(&self) -> Vec<IndicatorAlert> {
398 self.alerts.clone()
399 }
400}
401
402pub struct KamaEngine {
419 period: usize,
420 fast_period: usize,
421 slow_period: usize,
422 closes: VecDeque<f64>,
423 current_kama: Option<f64>,
424 alerts: Vec<IndicatorAlert>,
425}
426
427impl KamaEngine {
428 pub fn new(period: usize, fast_period: usize, slow_period: usize) -> Self {
429 Self {
430 period,
431 fast_period,
432 slow_period,
433 closes: VecDeque::new(),
434 current_kama: None,
435 alerts: Vec::new(),
436 }
437 }
438}
439
440impl Indicator for KamaEngine {
441 fn name(&self) -> &str {
442 "kama"
443 }
444
445 fn warmup_period(&self) -> usize {
446 self.period + 1
447 }
448
449 fn reset(&mut self) {
450 self.closes.clear();
451 self.current_kama = None;
452 self.alerts.clear();
453 }
454
455 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
456 self.closes.push_back(bar.close);
457 if self.closes.len() > self.period + 1 {
458 self.closes.pop_front();
459 }
460
461 self.alerts.clear();
462 if self.closes.len() < self.period + 1 {
463 return None;
464 }
465
466 let change = (self.closes.back().unwrap() - self.closes.front().unwrap()).abs();
467 let mut volatility = 0.0f64;
468 for pair in self.closes.iter().collect::<Vec<_>>().windows(2) {
469 volatility += (*pair[1] - *pair[0]).abs();
470 }
471
472 let er = if volatility > 0.0 {
473 change / volatility
474 } else {
475 0.0
476 };
477
478 let fast_sc = 2.0 / (self.fast_period as f64 + 1.0);
479 let slow_sc = 2.0 / (self.slow_period as f64 + 1.0);
480 let sc = (er * (fast_sc - slow_sc) + slow_sc).powi(2);
481
482 let kama = match self.current_kama {
483 Some(prev) => prev + sc * (bar.close - prev),
484 None => bar.close,
485 };
486 self.current_kama = Some(kama);
487
488 Some(IndicatorOutput::new(kama))
489 }
490
491 fn alerts(&self) -> Vec<IndicatorAlert> {
492 self.alerts.clone()
493 }
494}