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, Copy, PartialEq)]
13pub struct VolumeWeightedSrOutput {
14 pub support: f64,
16 pub resistance: f64,
18}
19
20#[derive(Debug, Clone)]
55pub struct VolumeWeightedSr {
56 period: usize,
57 highs: VecDeque<f64>,
58 lows: VecDeque<f64>,
59 volumes: VecDeque<f64>,
60 sum_hv: RollingSum,
61 sum_lv: RollingSum,
62 sum_v: RollingSum,
63 sum_h: RollingSum,
64 sum_l: RollingSum,
65 last: Option<VolumeWeightedSrOutput>,
66}
67
68impl VolumeWeightedSr {
69 pub fn new(period: usize) -> Result<Self> {
75 if period == 0 {
76 return Err(Error::PeriodZero);
77 }
78 if period > crate::error::MAX_PERIOD {
79 return Err(Error::InvalidPeriod {
80 message: crate::error::PERIOD_ABOVE_MAX,
81 });
82 }
83 Ok(Self {
84 period,
85 highs: VecDeque::with_capacity(period),
86 lows: VecDeque::with_capacity(period),
87 volumes: VecDeque::with_capacity(period),
88 sum_hv: RollingSum::new(),
89 sum_lv: RollingSum::new(),
90 sum_v: RollingSum::new(),
91 sum_h: RollingSum::new(),
92 sum_l: RollingSum::new(),
93 last: None,
94 })
95 }
96
97 pub const fn period(&self) -> usize {
99 self.period
100 }
101
102 pub const fn value(&self) -> Option<VolumeWeightedSrOutput> {
104 self.last
105 }
106}
107
108impl Indicator for VolumeWeightedSr {
109 type Input = Candle;
110 type Output = VolumeWeightedSrOutput;
111
112 #[inline]
113 fn update(&mut self, candle: Candle) -> Option<VolumeWeightedSrOutput> {
114 if self.highs.len() == self.period {
115 let h = self.highs.pop_front().expect("non-empty");
116 let l = self.lows.pop_front().expect("non-empty");
117 let v = self.volumes.pop_front().expect("non-empty");
118 self.sum_hv.evict(h * v);
119 self.sum_lv.evict(l * v);
120 self.sum_v.evict(v);
121 self.sum_h.evict(h);
122 self.sum_l.evict(l);
123 }
124 self.highs.push_back(candle.high);
125 self.lows.push_back(candle.low);
126 self.volumes.push_back(candle.volume);
127 self.sum_hv.push(candle.high * candle.volume);
128 self.sum_lv.push(candle.low * candle.volume);
129 self.sum_v.push(candle.volume);
130 self.sum_h.push(candle.high);
131 self.sum_l.push(candle.low);
132 if self.sum_v.needs_reseed(self.period) {
133 let volumes = &self.volumes;
134 self.sum_hv
135 .reseed(self.highs.iter().zip(volumes).map(|(h, v)| h * v));
136 self.sum_lv
137 .reseed(self.lows.iter().zip(volumes).map(|(l, v)| l * v));
138 self.sum_v.reseed(volumes.iter().copied());
139 self.sum_h.reseed(self.highs.iter().copied());
140 self.sum_l.reseed(self.lows.iter().copied());
141 }
142 if self.highs.len() < self.period {
143 return None;
144 }
145 let n = self.period as f64;
146 let total_volume = self.sum_v.value();
147 let (support, resistance) = if total_volume > 0.0 {
148 (
149 self.sum_lv.value() / total_volume,
150 self.sum_hv.value() / total_volume,
151 )
152 } else {
153 (self.sum_l.value() / n, self.sum_h.value() / n)
154 };
155 let out = VolumeWeightedSrOutput {
156 support,
157 resistance,
158 };
159 self.last = Some(out);
160 Some(out)
161 }
162
163 fn reset(&mut self) {
164 self.highs.clear();
165 self.lows.clear();
166 self.volumes.clear();
167 self.sum_hv.reset();
168 self.sum_lv.reset();
169 self.sum_v.reset();
170 self.sum_h.reset();
171 self.sum_l.reset();
172 self.last = None;
173 }
174
175 #[inline]
176 fn warmup_period(&self) -> usize {
177 self.period
178 }
179
180 #[inline]
181 fn is_ready(&self) -> bool {
182 self.last.is_some()
183 }
184
185 #[inline]
186 fn name(&self) -> &'static str {
187 "VolumeWeightedSr"
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::traits::BatchExt;
195 use approx::assert_relative_eq;
196
197 fn c(high: f64, low: f64, volume: f64) -> Candle {
198 Candle::new_unchecked(low, high, low, f64::midpoint(high, low), volume, 0)
199 }
200
201 #[test]
202 fn rejects_zero_period() {
203 assert!(matches!(VolumeWeightedSr::new(0), Err(Error::PeriodZero)));
204 }
205
206 #[test]
207 fn accessors_and_metadata() {
208 let v = VolumeWeightedSr::new(20).unwrap();
209 assert_eq!(v.period(), 20);
210 assert_eq!(v.warmup_period(), 20);
211 assert_eq!(v.name(), "VolumeWeightedSr");
212 assert!(!v.is_ready());
213 assert_eq!(v.value(), None);
214 }
215
216 #[test]
217 fn first_emission_at_warmup_period() {
218 let mut v = VolumeWeightedSr::new(4).unwrap();
219 let candles: Vec<Candle> = (0..6).map(|_| c(102.0, 98.0, 1_000.0)).collect();
220 let out = v.batch(&candles);
221 for o in out.iter().take(3) {
222 assert!(o.is_none());
223 }
224 assert!(out[3].is_some());
225 }
226
227 #[test]
228 fn support_below_resistance() {
229 let mut v = VolumeWeightedSr::new(10).unwrap();
230 let candles: Vec<Candle> = (0..30)
231 .map(|i| {
232 c(
233 110.0 + (f64::from(i) * 0.3).sin() * 5.0,
234 90.0 + (f64::from(i) * 0.3).cos() * 5.0,
235 1_000.0 + f64::from(i),
236 )
237 })
238 .collect();
239 for o in v.batch(&candles).into_iter().flatten() {
240 assert!(o.support <= o.resistance);
241 }
242 }
243
244 #[test]
245 fn weights_toward_high_volume_bars() {
246 let mut v = VolumeWeightedSr::new(4).unwrap();
249 let candles = [
250 c(102.0, 98.0, 100.0),
251 c(102.0, 98.0, 100.0),
252 c(102.0, 98.0, 100.0),
253 c(112.0, 108.0, 9_000.0),
254 ];
255 let out = v.batch(&candles).into_iter().flatten().last().unwrap();
256 assert!(
258 out.resistance > 108.0,
259 "resistance {} should lean to the heavy bar",
260 out.resistance
261 );
262 }
263
264 #[test]
265 fn zero_volume_falls_back_to_equal_weight() {
266 let mut v = VolumeWeightedSr::new(3).unwrap();
267 let candles = [
268 c(102.0, 98.0, 0.0),
269 c(104.0, 96.0, 0.0),
270 c(106.0, 94.0, 0.0),
271 ];
272 let out = v.batch(&candles).into_iter().flatten().last().unwrap();
273 assert_relative_eq!(out.resistance, 104.0, epsilon = 1e-9);
275 assert_relative_eq!(out.support, 96.0, epsilon = 1e-9);
276 }
277
278 #[test]
279 fn reset_clears_state() {
280 let mut v = VolumeWeightedSr::new(4).unwrap();
281 v.batch(&(0..6).map(|_| c(102.0, 98.0, 1_000.0)).collect::<Vec<_>>());
282 assert!(v.is_ready());
283 v.reset();
284 assert!(!v.is_ready());
285 assert_eq!(v.value(), None);
286 assert_eq!(v.update(c(102.0, 98.0, 1_000.0)), None);
287 }
288
289 #[test]
290 fn batch_equals_streaming() {
291 let candles: Vec<Candle> = (0..120)
292 .map(|i| {
293 c(
294 110.0 + (f64::from(i) * 0.25).sin() * 9.0,
295 90.0 + (f64::from(i) * 0.25).cos() * 9.0,
296 1_000.0 + f64::from(i),
297 )
298 })
299 .collect();
300 let batch = VolumeWeightedSr::new(20).unwrap().batch(&candles);
301 let mut b = VolumeWeightedSr::new(20).unwrap();
302 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
303 assert_eq!(batch, streamed);
304 }
305}