Skip to main content

egui_charts/studies/builtin/
relative_vigor.rs

1use super::over_finite_bars;
2use crate::model::Bar;
3use crate::studies::stepper::{rolling_mean, update_window, usable_behind};
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7/// Relative Vigor Index (RVI)
8/// Measures conviction of price movement
9/// Uses the relationship between close-open vs high-low
10#[derive(Clone)]
11pub struct RelativeVigorIndex {
12    /// Period for smoothing (typically 10)
13    period: usize,
14    values: IndicatorValues,
15    /// Colors: [RVI, Signal]
16    colors: Vec<Color32>,
17    visible: bool,
18}
19
20impl RelativeVigorIndex {
21    /// Create with a lookback of `period` bars, clamped to at least 1.
22    pub fn new(period: usize) -> Self {
23        Self {
24            period: period.max(1),
25            values: IndicatorValues::new(),
26            colors: vec![
27                Color32::from_rgb(0, 128, 0), // RVI
28                Color32::from_rgb(255, 0, 0), // Signal
29            ],
30            visible: true,
31        }
32    }
33
34    /// Set plot colors in the order returned by [`Indicator::line_names`].
35    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
36        self.colors = colors;
37        self
38    }
39
40    fn swma(data: &[f64]) -> Vec<f64> {
41        // Symmetrically weighted moving avg (weights: 1, 2, 2, 1)
42        let mut result = Vec::with_capacity(data.len());
43
44        for i in 0..data.len() {
45            if i < 3 {
46                result.push(f64::NAN);
47            } else {
48                let swma = (data[i - 3] + 2.0 * data[i - 2] + 2.0 * data[i - 1] + data[i]) / 6.0;
49                result.push(swma);
50            }
51        }
52
53        result
54    }
55}
56
57impl Default for RelativeVigorIndex {
58    fn default() -> Self {
59        Self::new(10)
60    }
61}
62
63impl RelativeVigorIndex {
64    /// The values over `data`, every bar of which has finite inputs.
65    fn compute(&self, data: &[Bar]) -> IndicatorValues {
66        let mut values = IndicatorValues::new();
67        // Calculate (Close - Open) for numerator
68        let close_open: Vec<f64> = data.iter().map(|b| b.close - b.open).collect();
69
70        // Calculate (High - Low) for denominator
71        let high_low: Vec<f64> = data.iter().map(|b| b.high - b.low).collect();
72
73        // Apply SWMA smoothing
74        let smoothed_num = Self::swma(&close_open);
75        let smoothed_den = Self::swma(&high_low);
76
77        // Calculate running sums over period
78        let num_sum = rolling_mean(&smoothed_num, self.period);
79        let den_sum = rolling_mean(&smoothed_den, self.period);
80
81        // Calculate RVI
82        let mut rvi = Vec::with_capacity(data.len());
83        for i in 0..data.len() {
84            if num_sum[i].is_nan() || den_sum[i].is_nan() || den_sum[i] == 0.0 {
85                rvi.push(f64::NAN);
86            } else {
87                rvi.push(num_sum[i] / den_sum[i]);
88            }
89        }
90
91        // Calculate signal line (SWMA of RVI)
92        let signal = Self::swma(&rvi);
93
94        // Store results
95        for i in 0..data.len() {
96            if rvi[i].is_nan() {
97                values.push(IndicatorValue::None);
98            } else {
99                values.push_lines(&[rvi[i], signal[i]]);
100            }
101        }
102
103        values
104    }
105}
106
107impl Indicator for RelativeVigorIndex {
108    crate::studies::inputs::study_inputs!(self;
109        period: length("Period", 1, usize::MAX)
110    ; {});
111
112    fn name(&self) -> &str {
113        "RVI"
114    }
115
116    fn desc(&self) -> &str {
117        "Relative Vigor Index - Price movement conviction"
118    }
119
120    fn calculate(&mut self, data: &[Bar]) {
121        self.update(data, 0);
122    }
123
124    fn update(&mut self, data: &[Bar], from: usize) {
125        let mut values = std::mem::take(&mut self.values);
126        update_window(
127            &mut values,
128            data,
129            from,
130            usable_behind(self.period.saturating_add(5), |b| {
131                [b.open, b.high, b.low, b.close]
132                    .iter()
133                    .all(|x| x.is_finite())
134            }),
135            |data| {
136                over_finite_bars(
137                    data,
138                    |b| {
139                        [b.open, b.high, b.low, b.close]
140                            .iter()
141                            .all(|x| x.is_finite())
142                    },
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![format!("RVI({})", self.period), "Signal".to_string()]
186    }
187}