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