Skip to main content

egui_charts/studies/builtin/
close_to_close_vol.rs

1use super::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/// Close-to-Close Volatility
9/// Standard deviation of log returns, annualized by sqrt(252)
10#[derive(Clone)]
11pub struct CloseToCloseVol {
12    period: usize,
13    values: IndicatorValues,
14    colors: Vec<Color32>,
15    visible: bool,
16}
17
18impl CloseToCloseVol {
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![palette::LIGHT_BLUE], // Purple
25            visible: true,
26        }
27    }
28}
29
30impl Default for CloseToCloseVol {
31    fn default() -> Self {
32        Self::new(20)
33    }
34}
35
36impl CloseToCloseVol {
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.len() < 2 {
41            for _ in data {
42                values.push(IndicatorValue::None);
43            }
44            return values;
45        }
46
47        // Calculate log returns
48        let mut log_returns = Vec::with_capacity(data.len());
49        log_returns.push(f64::NAN); // No return for first bar
50
51        for i in 1..data.len() {
52            if data[i - 1].close > 0.0 && data[i].close > 0.0 {
53                log_returns.push((data[i].close / data[i - 1].close).ln());
54            } else {
55                log_returns.push(f64::NAN);
56            }
57        }
58
59        // Rolling standard deviation of log returns
60        for i in 0..data.len() {
61            if i < self.period {
62                values.push(IndicatorValue::None);
63                continue;
64            }
65
66            let window = &log_returns[i + 1 - self.period..=i];
67            let valid: Vec<f64> = window.iter().filter(|x| !x.is_nan()).copied().collect();
68
69            if valid.len() < 2 {
70                values.push(IndicatorValue::None);
71                continue;
72            }
73
74            let mean = valid.iter().sum::<f64>() / valid.len() as f64;
75            let variance =
76                valid.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (valid.len() - 1) as f64;
77            let std_dev = variance.sqrt();
78
79            // Annualize: std_dev * sqrt(252) * 100 (as percentage)
80            let annualized = std_dev * 252.0_f64.sqrt() * 100.0;
81            values.push(IndicatorValue::Single(annualized));
82        }
83
84        values
85    }
86}
87
88impl Indicator for CloseToCloseVol {
89    fn source(&self) -> Option<crate::studies::StudySource> {
90        Some(crate::studies::StudySource::Price(
91            crate::model::PriceSource::Close,
92        ))
93    }
94
95    crate::studies::inputs::study_inputs!(self;
96        period: length("Period", 1, usize::MAX)
97    ; {});
98
99    fn value_format(&self) -> crate::scales::StudyFormat {
100        crate::scales::StudyFormat::Percent
101    }
102
103    fn name(&self) -> &str {
104        "C2C Vol"
105    }
106
107    fn desc(&self) -> &str {
108        "Close-to-Close Volatility - Annualized standard deviation of log returns"
109    }
110
111    fn calculate(&mut self, data: &[Bar]) {
112        self.update(data, 0);
113    }
114
115    fn update(&mut self, data: &[Bar], from: usize) {
116        let mut values = std::mem::take(&mut self.values);
117        update_window(
118            &mut values,
119            data,
120            from,
121            usable_behind(self.period, |b| b.close.is_finite()),
122            |data| over_finite_bars(data, |b| b.close.is_finite(), |data| self.compute(data)),
123        );
124        self.values = values;
125    }
126
127    fn is_incremental(&self) -> bool {
128        true
129    }
130
131    fn values(&self) -> &IndicatorValues {
132        &self.values
133    }
134
135    fn colors(&self) -> Vec<Color32> {
136        self.colors.clone()
137    }
138
139    fn set_colors(&mut self, colors: Vec<Color32>) {
140        if !colors.is_empty() {
141            self.colors = colors;
142        }
143    }
144
145    fn is_overlay(&self) -> bool {
146        false
147    }
148
149    fn is_visible(&self) -> bool {
150        self.visible
151    }
152
153    fn set_visible(&mut self, visible: bool) {
154        self.visible = visible;
155    }
156
157    fn clone_box(&self) -> Box<dyn Indicator> {
158        Box::new(self.clone())
159    }
160
161    fn line_names(&self) -> Vec<String> {
162        vec![format!("C2C Vol({})", self.period)]
163    }
164}