egui_charts/studies/builtin/
historical_volatility.rs1use super::{clamp_param, 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#[derive(Clone)]
12pub struct HistoricalVolatility {
13 period: usize,
15 trading_days: f64,
17 values: IndicatorValues,
18 color: Color32,
19 visible: bool,
20}
21
22impl HistoricalVolatility {
23 pub fn new(period: usize) -> Self {
25 Self {
26 period: period.max(1),
27 trading_days: 252.0, values: IndicatorValues::new(),
29 color: palette::BLUE, visible: true,
31 }
32 }
33
34 pub fn with_trading_days(mut self, days: f64) -> Self {
37 self.trading_days = clamp_param(days, 0.0, f64::MAX);
38 self
39 }
40
41 pub fn for_crypto(mut self) -> Self {
43 self.trading_days = 365.0;
44 self
45 }
46
47 pub fn with_color(mut self, color: Color32) -> Self {
49 self.color = color;
50 self
51 }
52}
53
54impl Default for HistoricalVolatility {
56 fn default() -> Self {
57 Self::new(10)
58 }
59}
60
61impl HistoricalVolatility {
62 fn compute(&self, data: &[Bar]) -> IndicatorValues {
64 let mut values = IndicatorValues::new();
65 if data.len() < self.period.saturating_add(1) {
66 for _ in 0..data.len() {
67 values.push(IndicatorValue::None);
68 }
69 return values;
70 }
71
72 let mut log_returns = Vec::with_capacity(data.len());
74 log_returns.push(f64::NAN); for i in 1..data.len() {
77 if data[i - 1].close <= 0.0 || data[i].close <= 0.0 {
78 log_returns.push(f64::NAN);
79 } else {
80 let ret = (data[i].close / data[i - 1].close).ln();
81 log_returns.push(ret);
82 }
83 }
84
85 for i in 0..data.len() {
87 if i < self.period {
88 values.push(IndicatorValue::None);
89 } else {
90 let window = &log_returns[i + 1 - self.period..=i];
91
92 let valid_returns: Vec<f64> =
94 window.iter().filter(|&&x| !x.is_nan()).copied().collect();
95
96 if valid_returns.len() < 2 {
97 values.push(IndicatorValue::None);
98 continue;
99 }
100
101 let mean = valid_returns.iter().sum::<f64>() / valid_returns.len() as f64;
103
104 let variance = valid_returns
106 .iter()
107 .map(|&x| (x - mean).powi(2))
108 .sum::<f64>()
109 / (valid_returns.len() - 1) as f64;
110
111 let std_dev = variance.sqrt();
113
114 let annualized_hv = std_dev * self.trading_days.sqrt() * 100.0;
116
117 values.push(IndicatorValue::Single(annualized_hv));
118 }
119 }
120
121 values
122 }
123}
124
125impl Indicator for HistoricalVolatility {
126 fn source(&self) -> Option<crate::studies::StudySource> {
127 Some(crate::studies::StudySource::Price(
128 crate::model::PriceSource::Close,
129 ))
130 }
131
132 crate::studies::inputs::study_inputs!(self;
133 period: length("Period", 1, usize::MAX),
134 trading_days: number("Trading days", 0.0, f64::MAX)
135 ; {});
136
137 fn value_format(&self) -> crate::scales::StudyFormat {
138 crate::scales::StudyFormat::Percent
139 }
140
141 fn name(&self) -> &str {
142 "HV"
143 }
144
145 fn desc(&self) -> &str {
146 "Historical Volatility - Annualized standard deviation of returns"
147 }
148
149 fn calculate(&mut self, data: &[Bar]) {
150 self.update(data, 0);
151 }
152
153 fn update(&mut self, data: &[Bar], from: usize) {
154 let mut values = std::mem::take(&mut self.values);
155 update_window(
156 &mut values,
157 data,
158 from,
159 usable_behind(self.period, |b| b.close.is_finite()),
160 |data| over_finite_bars(data, |b| b.close.is_finite(), |data| self.compute(data)),
161 );
162 self.values = values;
163 }
164
165 fn is_incremental(&self) -> bool {
166 true
167 }
168
169 fn values(&self) -> &IndicatorValues {
170 &self.values
171 }
172
173 fn colors(&self) -> Vec<Color32> {
174 vec![self.color]
175 }
176
177 fn set_colors(&mut self, colors: Vec<Color32>) {
178 if !colors.is_empty() {
179 self.color = colors[0];
180 }
181 }
182
183 fn is_overlay(&self) -> bool {
184 false
185 }
186
187 fn is_visible(&self) -> bool {
188 self.visible
189 }
190
191 fn set_visible(&mut self, visible: bool) {
192 self.visible = visible;
193 }
194
195 fn clone_box(&self) -> Box<dyn Indicator> {
196 Box::new(self.clone())
197 }
198
199 fn line_names(&self) -> Vec<String> {
200 vec![format!("HV({})", self.period)]
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::studies::test_support::make_bar;
208
209 #[test]
210 fn test_hv_no_volatility() {
211 let mut hv = HistoricalVolatility::new(3);
212
213 let data = vec![
215 make_bar(100.0),
216 make_bar(100.0),
217 make_bar(100.0),
218 make_bar(100.0),
219 make_bar(100.0),
220 ];
221
222 hv.calculate(&data);
223
224 let rows = hv.values.to_rows();
226 let IndicatorValue::Single(v) = rows.last().unwrap() else {
227 panic!("no value: {:?}", hv.values.to_rows().last().unwrap());
228 };
229 assert!(v.abs() < 0.01, "HV should be ~0 for constant prices");
230 }
231
232 #[test]
233 fn test_hv_high_volatility() {
234 let mut hv = HistoricalVolatility::new(3);
235
236 let data = vec![
238 make_bar(100.0),
239 make_bar(120.0),
240 make_bar(80.0),
241 make_bar(130.0),
242 make_bar(70.0),
243 ];
244
245 hv.calculate(&data);
246
247 for bar in 0..3 {
248 assert!(
249 matches!(hv.values.to_rows()[bar], IndicatorValue::None),
250 "warm-up bar {bar}"
251 );
252 }
253 let rows = hv.values.to_rows();
255 let IndicatorValue::Single(v) = rows.last().unwrap() else {
256 panic!("no value: {:?}", hv.values.to_rows().last().unwrap());
257 };
258 assert!(*v > 100.0, "HV should be high for volatile prices, got {v}");
259 }
260}