Skip to main content

egui_charts/studies/builtin/
vortex.rs

1use super::over_finite_bars;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::{update_window, usable_behind};
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Vortex Indicator (VI)
9/// Identifies trend direction and strength
10/// Uses True Range and directional movement concepts
11#[derive(Clone)]
12pub struct VortexIndicator {
13    /// Lookback period (typically 14)
14    period: usize,
15    values: IndicatorValues,
16    /// Colors: [+VI (bullish), -VI (bearish)]
17    colors: Vec<Color32>,
18    visible: bool,
19}
20
21impl VortexIndicator {
22    /// Create with a lookback of `period` bars, clamped to at least 1.
23    pub fn new(period: usize) -> Self {
24        Self {
25            period: period.max(1),
26            values: IndicatorValues::new(),
27            colors: vec![
28                palette::BLUE, // +VI
29                palette::PINK, // -VI
30            ],
31            visible: true,
32        }
33    }
34
35    /// Set plot colors in the order returned by [`Indicator::line_names`].
36    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
37        self.colors = colors;
38        self
39    }
40}
41
42/// Construct with the conventional default parameters.
43impl Default for VortexIndicator {
44    fn default() -> Self {
45        Self::new(14)
46    }
47}
48
49impl VortexIndicator {
50    /// The values over `data`, every bar of which has finite inputs.
51    fn compute(&self, data: &[Bar]) -> IndicatorValues {
52        let mut values = IndicatorValues::new();
53        if data.len() < self.period.saturating_add(1) {
54            for _ in 0..data.len() {
55                values.push(IndicatorValue::None);
56            }
57            return values;
58        }
59
60        // Calculate True Range, +VM, -VM for each bar
61        let mut tr_values = Vec::with_capacity(data.len());
62        let mut vm_plus = Vec::with_capacity(data.len());
63        let mut vm_minus = Vec::with_capacity(data.len());
64
65        tr_values.push(0.0);
66        vm_plus.push(0.0);
67        vm_minus.push(0.0);
68
69        for i in 1..data.len() {
70            let current = &data[i];
71            let prev = &data[i - 1];
72
73            // True Range = max(High - Low, |High - Prev Close|, |Low - Prev Close|)
74            let tr = (current.high - current.low)
75                .max((current.high - prev.close).abs())
76                .max((current.low - prev.close).abs());
77
78            // +VM = |Current High - Previous Low|
79            let vm_p = (current.high - prev.low).abs();
80
81            // -VM = |Current Low - Previous High|
82            let vm_m = (current.low - prev.high).abs();
83
84            tr_values.push(tr);
85            vm_plus.push(vm_p);
86            vm_minus.push(vm_m);
87        }
88
89        // Calculate VI+ and VI-
90        for i in 0..data.len() {
91            if i < self.period {
92                values.push(IndicatorValue::None);
93            } else {
94                // Sum over period
95                let tr_sum: f64 = tr_values[i + 1 - self.period..=i].iter().sum();
96                let vm_plus_sum: f64 = vm_plus[i + 1 - self.period..=i].iter().sum();
97                let vm_minus_sum: f64 = vm_minus[i + 1 - self.period..=i].iter().sum();
98
99                if tr_sum == 0.0 {
100                    values.push(IndicatorValue::None);
101                } else {
102                    let vi_plus = vm_plus_sum / tr_sum;
103                    let vi_minus = vm_minus_sum / tr_sum;
104                    values.push_lines(&[vi_plus, vi_minus]);
105                }
106            }
107        }
108
109        values
110    }
111}
112
113impl Indicator for VortexIndicator {
114    crate::studies::inputs::study_inputs!(self;
115        period: length("Period", 1, usize::MAX)
116    ; {});
117
118    fn name(&self) -> &str {
119        "Vortex"
120    }
121
122    fn desc(&self) -> &str {
123        "Vortex Indicator - Identifies trend direction"
124    }
125
126    fn calculate(&mut self, data: &[Bar]) {
127        self.update(data, 0);
128    }
129
130    fn update(&mut self, data: &[Bar], from: usize) {
131        let mut values = std::mem::take(&mut self.values);
132        update_window(
133            &mut values,
134            data,
135            from,
136            usable_behind(self.period, |b| {
137                b.high.is_finite() && b.low.is_finite() && b.close.is_finite()
138            }),
139            |data| {
140                over_finite_bars(
141                    data,
142                    |b| b.high.is_finite() && b.low.is_finite() && b.close.is_finite(),
143                    |data| self.compute(data),
144                )
145            },
146        );
147        self.values = values;
148    }
149
150    fn is_incremental(&self) -> bool {
151        true
152    }
153
154    fn values(&self) -> &IndicatorValues {
155        &self.values
156    }
157
158    fn colors(&self) -> Vec<Color32> {
159        self.colors.clone()
160    }
161
162    fn set_colors(&mut self, colors: Vec<Color32>) {
163        if colors.len() >= 2 {
164            self.colors = colors;
165        }
166    }
167
168    fn is_overlay(&self) -> bool {
169        false
170    }
171
172    fn is_visible(&self) -> bool {
173        self.visible
174    }
175
176    fn set_visible(&mut self, visible: bool) {
177        self.visible = visible;
178    }
179
180    fn clone_box(&self) -> Box<dyn Indicator> {
181        Box::new(self.clone())
182    }
183
184    fn line_names(&self) -> Vec<String> {
185        vec![
186            format!("+VI({})", self.period),
187            format!("-VI({})", self.period),
188        ]
189    }
190}