kestrel_chartkit/indicator/
vortex.rs1use super::{Indicator, IndicatorAlert, IndicatorOutput};
2use crate::model::Bar;
3use std::collections::{HashMap, VecDeque};
4
5#[derive(Debug, Clone)]
14pub struct VortexEngine {
15 period: usize,
16 prev_bar: Option<Bar>,
17 vm_plus_window: VecDeque<f64>,
18 vm_minus_window: VecDeque<f64>,
19 tr_window: VecDeque<f64>,
20}
21
22impl VortexEngine {
23 pub fn new(period: usize) -> Self {
24 Self {
25 period: period.max(1),
26 prev_bar: None,
27 vm_plus_window: VecDeque::with_capacity(period),
28 vm_minus_window: VecDeque::with_capacity(period),
29 tr_window: VecDeque::with_capacity(period),
30 }
31 }
32}
33
34impl Indicator for VortexEngine {
35 fn name(&self) -> &str {
36 "vortex"
37 }
38
39 fn warmup_period(&self) -> usize {
40 self.period + 1
41 }
42
43 fn reset(&mut self) {
44 self.prev_bar = None;
45 self.vm_plus_window.clear();
46 self.vm_minus_window.clear();
47 self.tr_window.clear();
48 }
49
50 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
51 let prev = match &self.prev_bar {
52 Some(p) => p.clone(),
53 None => {
54 self.prev_bar = Some(bar.clone());
55 return None;
56 }
57 };
58 self.prev_bar = Some(bar.clone());
59
60 let vm_plus = (bar.high - prev.low).abs();
61 let vm_minus = (bar.low - prev.high).abs();
62 let tr = (bar.high - bar.low)
63 .max((bar.high - prev.close).abs())
64 .max((bar.low - prev.close).abs());
65
66 self.vm_plus_window.push_back(vm_plus);
67 self.vm_minus_window.push_back(vm_minus);
68 self.tr_window.push_back(tr);
69
70 if self.vm_plus_window.len() > self.period {
71 self.vm_plus_window.pop_front();
72 self.vm_minus_window.pop_front();
73 self.tr_window.pop_front();
74 }
75
76 if self.vm_plus_window.len() < self.period {
77 return None;
78 }
79
80 let sum_vm_plus: f64 = self.vm_plus_window.iter().sum();
81 let sum_vm_minus: f64 = self.vm_minus_window.iter().sum();
82 let sum_tr: f64 = self.tr_window.iter().sum::<f64>().max(1e-8);
83
84 let vi_plus = sum_vm_plus / sum_tr;
85 let vi_minus = sum_vm_minus / sum_tr;
86
87 let mut extra = HashMap::new();
88 extra.insert("vi_plus".to_string(), vi_plus);
89 extra.insert("vi_minus".to_string(), vi_minus);
90
91 Some(IndicatorOutput::with_extra(vi_plus, extra))
92 }
93
94 fn alerts(&self) -> Vec<IndicatorAlert> {
95 Vec::new()
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn test_vortex_indicator() {
105 let mut vi = VortexEngine::new(14);
106 let mut out = None;
107 for i in 0..20 {
108 let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
109 out = vi.on_bar(&b);
110 }
111 assert!(out.is_some());
112 let o = out.unwrap();
113 assert!(o.extra.contains_key("vi_plus"));
114 assert!(o.extra.contains_key("vi_minus"));
115 }
116}