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