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