1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
53pub struct GarmanKlassVolatility {
54 period: usize,
55 trading_periods: usize,
56 window: VecDeque<f64>,
57 sum: f64,
58 last: Option<f64>,
59}
60
61const GK_OC_COEFF: f64 = 0.386_294_361_119_890_6;
63
64impl GarmanKlassVolatility {
65 pub fn new(period: usize, trading_periods: usize) -> Result<Self> {
75 if period == 0 || trading_periods == 0 {
76 return Err(Error::PeriodZero);
77 }
78 Ok(Self {
79 period,
80 trading_periods,
81 window: VecDeque::with_capacity(period),
82 sum: 0.0,
83 last: None,
84 })
85 }
86
87 pub const fn periods(&self) -> (usize, usize) {
89 (self.period, self.trading_periods)
90 }
91
92 pub const fn value(&self) -> Option<f64> {
94 self.last
95 }
96}
97
98impl Indicator for GarmanKlassVolatility {
99 type Input = Candle;
100 type Output = f64;
101
102 fn update(&mut self, candle: Candle) -> Option<f64> {
103 let log_hl = (candle.high / candle.low).ln();
107 let log_co = (candle.close / candle.open).ln();
108 let sample = 0.5 * log_hl * log_hl - GK_OC_COEFF * log_co * log_co;
109
110 if self.window.len() == self.period {
111 let old = self.window.pop_front().expect("window is non-empty");
112 self.sum -= old;
113 }
114 self.window.push_back(sample);
115 self.sum += sample;
116
117 if self.window.len() < self.period {
118 return None;
119 }
120
121 let n = self.period as f64;
122 let variance = (self.sum / n).max(0.0);
127 let sigma = variance.sqrt();
128 let out = sigma * (self.trading_periods as f64).sqrt() * 100.0;
129 self.last = Some(out);
130 Some(out)
131 }
132
133 fn reset(&mut self) {
134 self.window.clear();
135 self.sum = 0.0;
136 self.last = None;
137 }
138
139 fn warmup_period(&self) -> usize {
140 self.period
141 }
142
143 fn is_ready(&self) -> bool {
144 self.last.is_some()
145 }
146
147 fn name(&self) -> &'static str {
148 "GarmanKlassVolatility"
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::traits::BatchExt;
156 use approx::assert_relative_eq;
157
158 fn candle(o: f64, h: f64, l: f64, c: f64, ts: i64) -> Candle {
159 Candle::new(o, h, l, c, 1.0, ts).unwrap()
160 }
161
162 #[test]
163 fn rejects_zero_period() {
164 assert!(matches!(
165 GarmanKlassVolatility::new(0, 252),
166 Err(Error::PeriodZero)
167 ));
168 assert!(matches!(
169 GarmanKlassVolatility::new(20, 0),
170 Err(Error::PeriodZero)
171 ));
172 }
173
174 #[test]
175 fn accessors_and_metadata() {
176 let gk = GarmanKlassVolatility::new(20, 252).unwrap();
177 assert_eq!(gk.periods(), (20, 252));
178 assert_eq!(gk.value(), None);
179 assert_eq!(gk.warmup_period(), 20);
180 assert_eq!(gk.name(), "GarmanKlassVolatility");
181 assert!(!gk.is_ready());
182 }
183
184 #[test]
185 fn zero_movement_yields_zero() {
186 let candles: Vec<Candle> = (0..30).map(|i| candle(10.0, 10.0, 10.0, 10.0, i)).collect();
188 let mut gk = GarmanKlassVolatility::new(14, 1).unwrap();
189 for v in gk.batch(&candles).into_iter().flatten() {
190 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
191 }
192 }
193
194 #[test]
195 fn constant_bar_shape_yields_constant_sigma() {
196 let candles: Vec<Candle> = (0..30).map(|i| candle(10.0, 11.0, 9.0, 10.2, i)).collect();
200 let log_hl = (11.0_f64 / 9.0_f64).ln();
201 let log_co = (10.2_f64 / 10.0_f64).ln();
202 let k = 0.5 * log_hl * log_hl - GK_OC_COEFF * log_co * log_co;
203 let expected = k.max(0.0).sqrt() * 100.0;
204
205 let mut gk = GarmanKlassVolatility::new(10, 1).unwrap();
206 let out = gk.batch(&candles);
207 for v in out.iter().skip(9).flatten() {
208 assert_relative_eq!(*v, expected, epsilon = 1e-9);
209 }
210 }
211
212 #[test]
213 fn output_is_non_negative() {
214 let mut gk = GarmanKlassVolatility::new(14, 252).unwrap();
215 let candles: Vec<Candle> = (0..200)
216 .map(|i| {
217 let base = 100.0 + (f64::from(i) * 0.3).sin() * 12.0;
218 let half = 0.5 + (f64::from(i) * 0.13).cos().abs() * 1.5;
219 let open = base - 0.1;
220 let close = base + 0.2;
221 candle(open, base + half, base - half, close, i64::from(i))
222 })
223 .collect();
224 for v in gk.batch(&candles).into_iter().flatten() {
225 assert!(v >= 0.0, "Garman-Klass must be non-negative: {v}");
226 }
227 }
228
229 #[test]
230 fn annualisation_scales_by_sqrt_trading_periods() {
231 let candles: Vec<Candle> = (0..40)
232 .map(|i| {
233 let base = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
234 let half = 1.0 + (f64::from(i) * 0.2).cos().abs();
235 candle(base, base + half, base - half, base + 0.3, i64::from(i))
236 })
237 .collect();
238 let raw = GarmanKlassVolatility::new(10, 1).unwrap().batch(&candles);
239 let annual = GarmanKlassVolatility::new(10, 252).unwrap().batch(&candles);
240 let scale = (252.0_f64).sqrt();
241 for (r, a) in raw.iter().zip(annual.iter()) {
242 assert_eq!(r.is_some(), a.is_some(), "warmup mismatch");
243 if let (Some(r), Some(a)) = (r, a) {
244 assert_relative_eq!(*a, r * scale, epsilon = 1e-9);
245 }
246 }
247 }
248
249 #[test]
250 fn first_emission_at_warmup_period() {
251 let candles: Vec<Candle> = (0..20).map(|i| candle(10.0, 11.0, 9.0, 10.2, i)).collect();
252 let mut gk = GarmanKlassVolatility::new(5, 1).unwrap();
253 let out = gk.batch(&candles);
254 for v in out.iter().take(4) {
255 assert!(v.is_none());
256 }
257 assert!(out[4].is_some());
258 }
259
260 #[test]
261 fn batch_equals_streaming() {
262 let candles: Vec<Candle> = (0..80)
263 .map(|i| {
264 let base = 100.0 + (f64::from(i) * 0.25).sin() * 6.0;
265 let half = 1.0 + (f64::from(i) * 0.15).cos().abs();
266 candle(base, base + half, base - half, base + 0.5, i64::from(i))
267 })
268 .collect();
269 let batch = GarmanKlassVolatility::new(14, 252).unwrap().batch(&candles);
270 let mut streamer = GarmanKlassVolatility::new(14, 252).unwrap();
271 let streamed: Vec<_> = candles.iter().map(|c| streamer.update(*c)).collect();
272 assert_eq!(batch, streamed);
273 }
274
275 #[test]
276 fn reset_clears_state() {
277 let candles: Vec<Candle> = (0..30).map(|i| candle(10.0, 11.0, 9.0, 10.2, i)).collect();
278 let mut gk = GarmanKlassVolatility::new(14, 252).unwrap();
279 gk.batch(&candles);
280 assert!(gk.is_ready());
281 gk.reset();
282 assert!(!gk.is_ready());
283 assert_eq!(gk.value(), None);
284 assert_eq!(gk.update(candles[0]), None);
285 }
286}