kestrel_chartkit/indicator/
moving_averages.rs1use std::collections::VecDeque;
2
3use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
4use crate::model::Bar;
5
6pub 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#[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
114pub 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
177pub 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
239pub 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
297pub 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
352pub 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}