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