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)]
51pub struct RogersSatchellVolatility {
52 period: usize,
53 trading_periods: usize,
54 window: VecDeque<f64>,
55 sum: RollingSum,
56 last: Option<f64>,
57}
58
59impl RogersSatchellVolatility {
60 pub fn new(period: usize, trading_periods: usize) -> Result<Self> {
70 if period == 0 || trading_periods == 0 {
71 return Err(Error::PeriodZero);
72 }
73 Ok(Self {
74 period,
75 trading_periods,
76 window: VecDeque::with_capacity(period),
77 sum: RollingSum::new(),
78 last: None,
79 })
80 }
81
82 pub const fn periods(&self) -> (usize, usize) {
84 (self.period, self.trading_periods)
85 }
86
87 pub const fn value(&self) -> Option<f64> {
89 self.last
90 }
91}
92
93impl Indicator for RogersSatchellVolatility {
94 type Input = Candle;
95 type Output = f64;
96
97 #[inline]
98 fn update(&mut self, candle: Candle) -> Option<f64> {
99 let log_hc = (candle.high / candle.close).ln();
106 let log_ho = (candle.high / candle.open).ln();
107 let log_lc = (candle.low / candle.close).ln();
108 let log_lo = (candle.low / candle.open).ln();
109 let sample = log_hc.mul_add(log_ho, log_lc * log_lo);
110
111 if self.window.len() == self.period {
112 let old = self.window.pop_front().expect("window is non-empty");
113 self.sum.evict(old);
114 }
115 self.window.push_back(sample);
116 self.sum.push(sample);
117 if self.sum.needs_reseed(self.period) {
118 self.sum.reseed(self.window.iter().copied());
119 }
120
121 if self.window.len() < self.period {
122 return None;
123 }
124
125 let n = self.period as f64;
126 let variance = (self.sum.value() / n).max(0.0);
129 let sigma = variance.sqrt();
130 let out = sigma * (self.trading_periods as f64).sqrt() * 100.0;
131 self.last = Some(out);
132 Some(out)
133 }
134
135 fn reset(&mut self) {
136 self.window.clear();
137 self.sum.reset();
138 self.last = None;
139 }
140
141 #[inline]
142 fn warmup_period(&self) -> usize {
143 self.period
144 }
145
146 #[inline]
147 fn is_ready(&self) -> bool {
148 self.last.is_some()
149 }
150
151 #[inline]
152 fn name(&self) -> &'static str {
153 "RogersSatchellVolatility"
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::traits::BatchExt;
161 use approx::assert_relative_eq;
162
163 fn candle(o: f64, h: f64, l: f64, c: f64, ts: i64) -> Candle {
164 Candle::new(o, h, l, c, 1.0, ts).unwrap()
165 }
166
167 #[test]
168 fn rejects_zero_period() {
169 assert!(matches!(
170 RogersSatchellVolatility::new(0, 252),
171 Err(Error::PeriodZero)
172 ));
173 assert!(matches!(
174 RogersSatchellVolatility::new(20, 0),
175 Err(Error::PeriodZero)
176 ));
177 }
178
179 #[test]
180 fn accessors_and_metadata() {
181 let rs = RogersSatchellVolatility::new(20, 252).unwrap();
182 assert_eq!(rs.periods(), (20, 252));
183 assert_eq!(rs.value(), None);
184 assert_eq!(rs.warmup_period(), 20);
185 assert_eq!(rs.name(), "RogersSatchellVolatility");
186 assert!(!rs.is_ready());
187 }
188
189 #[test]
190 fn zero_movement_yields_zero() {
191 let candles: Vec<Candle> = (0..30).map(|i| candle(10.0, 10.0, 10.0, 10.0, i)).collect();
192 let mut rs = RogersSatchellVolatility::new(14, 1).unwrap();
193 for v in rs.batch(&candles).into_iter().flatten() {
194 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
195 }
196 }
197
198 #[test]
199 fn constant_bar_shape_yields_constant_sigma() {
200 let candles: Vec<Candle> = (0..30).map(|i| candle(10.0, 11.0, 9.0, 10.5, i)).collect();
202 let log_hc = (11.0_f64 / 10.5_f64).ln();
203 let log_ho = (11.0_f64 / 10.0_f64).ln();
204 let log_lc = (9.0_f64 / 10.5_f64).ln();
205 let log_lo = (9.0_f64 / 10.0_f64).ln();
206 let k = log_hc * log_ho + log_lc * log_lo;
207 let expected = k.max(0.0).sqrt() * 100.0;
208
209 let mut rs = RogersSatchellVolatility::new(10, 1).unwrap();
210 let out = rs.batch(&candles);
211 for v in out.iter().skip(9).flatten() {
212 assert_relative_eq!(*v, expected, epsilon = 1e-9);
213 }
214 }
215
216 #[test]
217 fn output_is_non_negative() {
218 let mut rs = RogersSatchellVolatility::new(14, 252).unwrap();
219 let candles: Vec<Candle> = (0..200)
220 .map(|i| {
221 let base = 100.0 + (f64::from(i) * 0.3).sin() * 12.0;
222 let half = 0.5 + (f64::from(i) * 0.13).cos().abs() * 1.5;
223 let open = base - 0.1;
224 let close = base + 0.2;
225 candle(open, base + half, base - half, close, i64::from(i))
226 })
227 .collect();
228 for v in rs.batch(&candles).into_iter().flatten() {
229 assert!(v >= 0.0, "Rogers-Satchell must be non-negative: {v}");
230 }
231 }
232
233 #[test]
234 fn annualisation_scales_by_sqrt_trading_periods() {
235 let candles: Vec<Candle> = (0..40)
236 .map(|i| {
237 let base = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
238 let half = 1.0 + (f64::from(i) * 0.2).cos().abs();
239 candle(base, base + half, base - half, base + 0.3, i64::from(i))
240 })
241 .collect();
242 let raw = RogersSatchellVolatility::new(10, 1)
243 .unwrap()
244 .batch(&candles);
245 let annual = RogersSatchellVolatility::new(10, 252)
246 .unwrap()
247 .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.5, i)).collect();
260 let mut rs = RogersSatchellVolatility::new(5, 1).unwrap();
261 let out = rs.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 = RogersSatchellVolatility::new(14, 252)
278 .unwrap()
279 .batch(&candles);
280 let mut streamer = RogersSatchellVolatility::new(14, 252).unwrap();
281 let streamed: Vec<_> = candles.iter().map(|c| streamer.update(*c)).collect();
282 assert_eq!(batch, streamed);
283 }
284
285 #[test]
286 fn reset_clears_state() {
287 let candles: Vec<Candle> = (0..30).map(|i| candle(10.0, 11.0, 9.0, 10.5, i)).collect();
288 let mut rs = RogersSatchellVolatility::new(14, 252).unwrap();
289 rs.batch(&candles);
290 assert!(rs.is_ready());
291 rs.reset();
292 assert!(!rs.is_ready());
293 assert_eq!(rs.value(), None);
294 assert_eq!(rs.update(candles[0]), None);
295 }
296}