Skip to main content

egui_charts/studies/builtin/
ohlc_volatility.rs

1use super::over_finite_bars;
2use crate::model::Bar;
3use crate::studies::stepper::{update_window, usable_behind};
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7/// OHLC Volatility (Garman-Klass)
8/// Uses OHLC data for more efficient volatility estimation
9/// GK = 0.5 * ln(H/L)^2 - (2*ln(2)-1) * ln(C/O)^2
10#[derive(Clone)]
11pub struct OhlcVolatility {
12    period: usize,
13    values: IndicatorValues,
14    colors: Vec<Color32>,
15    visible: bool,
16}
17
18impl OhlcVolatility {
19    /// Create with a lookback of `period` bars, clamped to at least 1.
20    pub fn new(period: usize) -> Self {
21        Self {
22            period: period.max(1),
23            values: IndicatorValues::new(),
24            colors: vec![Color32::from_rgb(255, 82, 82)], // Violet
25            visible: true,
26        }
27    }
28}
29
30impl Default for OhlcVolatility {
31    fn default() -> Self {
32        Self::new(20)
33    }
34}
35
36impl OhlcVolatility {
37    /// The values over `data`, every bar of which has finite inputs.
38    fn compute(&self, data: &[Bar]) -> IndicatorValues {
39        let mut values = IndicatorValues::new();
40        if data.is_empty() {
41            return values;
42        }
43
44        let ln2 = 2.0_f64.ln();
45        let coeff = 2.0 * ln2 - 1.0;
46
47        // Calculate per-bar Garman-Klass values
48        let mut gk_values = Vec::with_capacity(data.len());
49
50        for bar in data {
51            if bar.low > 0.0 && bar.open > 0.0 {
52                let hl = (bar.high / bar.low).ln();
53                let co = (bar.close / bar.open).ln();
54                let gk = 0.5 * hl * hl - coeff * co * co;
55                gk_values.push(gk);
56            } else {
57                gk_values.push(f64::NAN);
58            }
59        }
60
61        // Rolling average over period, then sqrt and annualize
62        for i in 0..data.len() {
63            if i + 1 < self.period {
64                values.push(IndicatorValue::None);
65                continue;
66            }
67
68            let window = &gk_values[i + 1 - self.period..=i];
69            let valid: Vec<f64> = window.iter().filter(|x| !x.is_nan()).copied().collect();
70
71            if valid.is_empty() {
72                values.push(IndicatorValue::None);
73                continue;
74            }
75
76            let avg = valid.iter().sum::<f64>() / valid.len() as f64;
77            // sqrt of average GK, annualized, as percentage
78            let vol = if avg >= 0.0 {
79                avg.sqrt() * 252.0_f64.sqrt() * 100.0
80            } else {
81                0.0
82            };
83
84            values.push(IndicatorValue::Single(vol));
85        }
86
87        values
88    }
89}
90
91impl Indicator for OhlcVolatility {
92    crate::studies::inputs::study_inputs!(self;
93        period: length("Period", 1, usize::MAX)
94    ; {});
95
96    fn value_format(&self) -> crate::scales::StudyFormat {
97        crate::scales::StudyFormat::Percent
98    }
99
100    fn name(&self) -> &str {
101        "GK Vol"
102    }
103
104    fn desc(&self) -> &str {
105        "Garman-Klass OHLC Volatility - Efficient volatility estimator using OHLC"
106    }
107
108    fn calculate(&mut self, data: &[Bar]) {
109        self.update(data, 0);
110    }
111
112    fn update(&mut self, data: &[Bar], from: usize) {
113        let mut values = std::mem::take(&mut self.values);
114        update_window(
115            &mut values,
116            data,
117            from,
118            usable_behind(self.period - 1, |b| {
119                b.open.is_finite() && b.high.is_finite() && b.low.is_finite() && b.close.is_finite()
120            }),
121            |data| {
122                over_finite_bars(
123                    data,
124                    |b| {
125                        b.open.is_finite()
126                            && b.high.is_finite()
127                            && b.low.is_finite()
128                            && b.close.is_finite()
129                    },
130                    |data| self.compute(data),
131                )
132            },
133        );
134        self.values = values;
135    }
136
137    fn is_incremental(&self) -> bool {
138        true
139    }
140
141    fn values(&self) -> &IndicatorValues {
142        &self.values
143    }
144
145    fn colors(&self) -> Vec<Color32> {
146        self.colors.clone()
147    }
148
149    fn set_colors(&mut self, colors: Vec<Color32>) {
150        if !colors.is_empty() {
151            self.colors = colors;
152        }
153    }
154
155    fn is_overlay(&self) -> bool {
156        false
157    }
158
159    fn is_visible(&self) -> bool {
160        self.visible
161    }
162
163    fn set_visible(&mut self, visible: bool) {
164        self.visible = visible;
165    }
166
167    fn clone_box(&self) -> Box<dyn Indicator> {
168        Box::new(self.clone())
169    }
170
171    fn line_names(&self) -> Vec<String> {
172        vec![format!("GK Vol({})", self.period)]
173    }
174}