Skip to main content

kestrel_chartkit/indicator/
vortex.rs

1use super::{Indicator, IndicatorAlert, IndicatorOutput};
2use crate::model::Bar;
3use std::collections::{HashMap, VecDeque};
4
5/// Vortex Indicator: +VI and -VI.
6///
7/// From the second bar on, `VM+ = |high - prev_low|`, `VM- = |low - prev_high|` and the true
8/// range; `+VI = sum(VM+) / sum(TR)` and `-VI = sum(VM-) / sum(TR)` over the last `period` bars,
9/// the sum of true ranges floored at `1e-8`.
10///
11/// `value` and `extra["vi_plus"]`: +VI; `extra["vi_minus"]`: -VI. First output: with the
12/// `period + 1`-th bar. [`Indicator::reset`] clears the windows.
13#[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}