Skip to main content

egui_charts/studies/builtin/
relative_volatility_index.rs

1//! Relative Volatility Index (RVI), Donald Dorsey (1993).
2//!
3//! Built like the RSI, but it accumulates the standard deviation of the close instead of the size
4//! of the price change: volatility on rising bars against volatility on falling bars.
5//!
6//! # Formula
7//!
8//! ```text
9//! sd   = population standard deviation of the close over `length`
10//! up   = EMA(close > previous close ? sd : 0, 14)
11//! down = EMA(close > previous close ? 0 : sd, 14)
12//! RVI  = 100 * up / (up + down)
13//! ```
14//!
15//! An unchanged close counts as a falling bar. Both averages start at their first input, on the
16//! first bar with a full window and a previous close (bar `length - 1`, bar 1 for a length of 1).
17//! A bar where both averages are zero has no value. Length defaults to 10; the smoothing length is
18//! fixed at 14. Levels at 80, 50 and 20 with the band between 80 and 20 filled.
19//!
20//! # Example
21//! ```
22//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
23//! use egui_charts::studies::{Indicator, RelativeVolatilityIndex};
24//!
25//! let mut rvi = RelativeVolatilityIndex::default();
26//! rvi.calculate(&bars);
27//! let value = rvi.values().value(0, 30).unwrap();
28//! assert!((0.0..=100.0).contains(&value));
29//! ```
30
31use crate::model::Bar;
32use crate::studies::palette;
33use crate::studies::stepper::Stepper;
34use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
35use egui::Color32;
36
37/// Length of the exponential averages of the up and down volatility.
38const SMOOTHING: f64 = 14.0;
39
40/// Relative Volatility Index, a 0 to 100 oscillator in a separate pane.
41#[derive(Clone)]
42pub struct RelativeVolatilityIndex {
43    length: usize,
44    values: IndicatorValues,
45    stepper: Stepper<State>,
46    color: Color32,
47    fill_color: Color32,
48    visible: bool,
49}
50
51#[derive(Clone, Default)]
52struct State {
53    closes: std::collections::VecDeque<f64>,
54    previous: Option<f64>,
55    smoothed: Option<(f64, f64)>,
56}
57
58impl RelativeVolatilityIndex {
59    /// Create an RVI over a standard deviation of `length` closes (10 by convention).
60    pub fn new(length: usize) -> Self {
61        Self {
62            length: length.max(1),
63            values: IndicatorValues::new(),
64            stepper: Stepper::default(),
65            color: palette::PURPLE,
66            fill_color: palette::PURPLE.gamma_multiply_u8(26),
67            visible: true,
68        }
69    }
70}
71
72impl Default for RelativeVolatilityIndex {
73    fn default() -> Self {
74        Self::new(10)
75    }
76}
77
78impl Indicator for RelativeVolatilityIndex {
79    fn level_fill_color(&self) -> Color32 {
80        self.fill_color
81    }
82
83    fn source(&self) -> Option<crate::studies::StudySource> {
84        Some(crate::studies::StudySource::Price(
85            crate::model::PriceSource::Close,
86        ))
87    }
88
89    crate::studies::inputs::study_inputs!(self;
90        length: length("Length", 1, usize::MAX)
91    ; {});
92
93    fn name(&self) -> &str {
94        "Relative Volatility Index"
95    }
96
97    fn desc(&self) -> &str {
98        "Relative Volatility Index - Direction of volatility (0-100)"
99    }
100
101    fn calculate(&mut self, data: &[Bar]) {
102        self.update(data, 0);
103    }
104
105    fn update(&mut self, data: &[Bar], from: usize) {
106        self.stepper
107            .run(data, from, &mut self.values, |state, bar| {
108                if !bar.close.is_finite() {
109                    return IndicatorValue::None;
110                }
111                let previous = state.previous.replace(bar.close);
112                state.closes.push_back(bar.close);
113                if state.closes.len() > self.length {
114                    state.closes.pop_front();
115                }
116                let Some(previous) = previous.filter(|_| state.closes.len() == self.length) else {
117                    return IndicatorValue::None;
118                };
119                let mean = state.closes.iter().sum::<f64>() / self.length as f64;
120                let variance = state
121                    .closes
122                    .iter()
123                    .map(|close| (close - mean).powi(2))
124                    .sum::<f64>()
125                    / self.length as f64;
126                let sd = variance.sqrt();
127                let (up, down) = if bar.close > previous {
128                    (sd, 0.0)
129                } else {
130                    (0.0, sd)
131                };
132                let alpha = 2.0 / (SMOOTHING + 1.0);
133                let (up, down) = match state.smoothed {
134                    Some((u, d)) => (u + (up - u) * alpha, d + (down - d) * alpha),
135                    None => (up, down),
136                };
137                state.smoothed = Some((up, down));
138                if up + down > 0.0 {
139                    IndicatorValue::Single(100.0 * up / (up + down))
140                } else {
141                    IndicatorValue::None
142                }
143            });
144    }
145
146    fn is_incremental(&self) -> bool {
147        true
148    }
149
150    fn values(&self) -> &IndicatorValues {
151        &self.values
152    }
153
154    fn colors(&self) -> Vec<Color32> {
155        vec![self.color, self.fill_color]
156    }
157
158    fn set_colors(&mut self, colors: Vec<Color32>) {
159        if let Some(&color) = colors.first() {
160            self.color = color;
161            self.fill_color = colors.get(1).copied().unwrap_or_else(|| {
162                Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 16)
163            });
164        }
165    }
166
167    fn pane(&self) -> crate::studies::StudyPane {
168        let gray = palette::GRAY;
169        crate::studies::StudyPane {
170            height: None,
171            range: Some((0.0, 100.0)),
172            levels: vec![
173                (80.0, gray, "Upper Band"),
174                (50.0, gray.gamma_multiply_u8(128), "Middle Band"),
175                (20.0, gray, "Lower Band"),
176            ],
177            fill_between_levels: true,
178        }
179    }
180
181    fn is_overlay(&self) -> bool {
182        false
183    }
184
185    fn is_visible(&self) -> bool {
186        self.visible
187    }
188
189    fn set_visible(&mut self, visible: bool) {
190        self.visible = visible;
191    }
192
193    fn clone_box(&self) -> Box<dyn Indicator> {
194        Box::new(self.clone())
195    }
196
197    fn line_names(&self) -> Vec<String> {
198        vec![format!("RVI({})", self.length)]
199    }
200}