egui_charts/studies/builtin/
relative_volatility_index.rs1use crate::model::Bar;
32use crate::studies::palette;
33use crate::studies::stepper::Stepper;
34use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
35use egui::Color32;
36
37const SMOOTHING: f64 = 14.0;
39
40#[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 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}