wickra_core/indicators/
volume_rsi.rs1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
46pub struct VolumeRsi {
47 period: usize,
48 prev_volume: Option<f64>,
49 seed_gains: f64,
50 seed_losses: f64,
51 seed_count: usize,
52 avg_gain: Option<f64>,
53 avg_loss: Option<f64>,
54 last: Option<f64>,
55}
56
57impl VolumeRsi {
58 pub fn new(period: usize) -> Result<Self> {
64 if period == 0 {
65 return Err(Error::PeriodZero);
66 }
67 if period > crate::error::MAX_PERIOD {
68 return Err(Error::InvalidPeriod {
69 message: crate::error::PERIOD_ABOVE_MAX,
70 });
71 }
72 Ok(Self {
73 period,
74 prev_volume: None,
75 seed_gains: 0.0,
76 seed_losses: 0.0,
77 seed_count: 0,
78 avg_gain: None,
79 avg_loss: None,
80 last: None,
81 })
82 }
83
84 pub const fn period(&self) -> usize {
86 self.period
87 }
88
89 pub const fn value(&self) -> Option<f64> {
91 self.last
92 }
93
94 fn rsi_from_avgs(avg_gain: f64, avg_loss: f64) -> f64 {
95 let denom = avg_gain + avg_loss;
96 if denom == 0.0 {
97 50.0
98 } else {
99 100.0 * (avg_gain / denom)
100 }
101 }
102}
103
104impl Indicator for VolumeRsi {
105 type Input = Candle;
106 type Output = f64;
107
108 #[inline]
109 fn update(&mut self, candle: Candle) -> Option<f64> {
110 let volume = candle.volume;
111 let Some(prev) = self.prev_volume else {
112 self.prev_volume = Some(volume);
113 return None;
114 };
115 let change = volume - prev;
116 self.prev_volume = Some(volume);
117 let gain = if change > 0.0 { change } else { 0.0 };
118 let loss = if change < 0.0 { -change } else { 0.0 };
119
120 if let (Some(ag), Some(al)) = (self.avg_gain, self.avg_loss) {
121 let n = self.period as f64;
122 let new_ag = (ag * (n - 1.0) + gain) / n;
123 let new_al = (al * (n - 1.0) + loss) / n;
124 self.avg_gain = Some(new_ag);
125 self.avg_loss = Some(new_al);
126 let v = Self::rsi_from_avgs(new_ag, new_al);
127 self.last = Some(v);
128 return Some(v);
129 }
130
131 self.seed_gains += gain;
132 self.seed_losses += loss;
133 self.seed_count += 1;
134 if self.seed_count == self.period {
135 let n = self.period as f64;
136 let ag = self.seed_gains / n;
137 let al = self.seed_losses / n;
138 self.avg_gain = Some(ag);
139 self.avg_loss = Some(al);
140 let v = Self::rsi_from_avgs(ag, al);
141 self.last = Some(v);
142 return Some(v);
143 }
144 None
145 }
146
147 fn reset(&mut self) {
148 self.prev_volume = None;
149 self.seed_gains = 0.0;
150 self.seed_losses = 0.0;
151 self.seed_count = 0;
152 self.avg_gain = None;
153 self.avg_loss = None;
154 self.last = None;
155 }
156
157 #[inline]
158 fn warmup_period(&self) -> usize {
159 self.period + 1
160 }
161
162 #[inline]
163 fn is_ready(&self) -> bool {
164 self.last.is_some()
165 }
166
167 #[inline]
168 fn name(&self) -> &'static str {
169 "VolumeRsi"
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::traits::BatchExt;
177 use approx::assert_relative_eq;
178
179 fn vol_candle(volume: f64) -> Candle {
181 Candle::new_unchecked(100.0, 101.0, 99.0, 100.5, volume, 0)
182 }
183
184 #[test]
185 fn rejects_zero_period() {
186 assert!(matches!(VolumeRsi::new(0), Err(Error::PeriodZero)));
187 }
188
189 #[test]
190 fn accessors_and_metadata() {
191 let v = VolumeRsi::new(14).unwrap();
192 assert_eq!(v.period(), 14);
193 assert_eq!(v.warmup_period(), 15);
194 assert_eq!(v.name(), "VolumeRsi");
195 assert!(!v.is_ready());
196 assert_eq!(v.value(), None);
197 }
198
199 #[test]
200 fn first_emission_at_warmup_period() {
201 let mut v = VolumeRsi::new(3).unwrap();
202 let candles: Vec<Candle> = (0..6).map(|i| vol_candle(1_000.0 + f64::from(i))).collect();
203 let out = v.batch(&candles);
204 for o in out.iter().take(3) {
206 assert!(o.is_none());
207 }
208 assert!(out[3].is_some());
209 }
210
211 #[test]
212 fn rising_volume_is_one_hundred() {
213 let mut v = VolumeRsi::new(5).unwrap();
215 let candles: Vec<Candle> = (1..=40).map(|i| vol_candle(f64::from(i) * 100.0)).collect();
216 let last = v.batch(&candles).into_iter().flatten().last().unwrap();
217 assert_relative_eq!(last, 100.0, epsilon = 1e-9);
218 }
219
220 #[test]
221 fn falling_volume_is_zero() {
222 let mut v = VolumeRsi::new(5).unwrap();
223 let candles: Vec<Candle> = (1..=40)
224 .map(|i| vol_candle(5_000.0 - f64::from(i) * 100.0))
225 .collect();
226 let last = v.batch(&candles).into_iter().flatten().last().unwrap();
227 assert_relative_eq!(last, 0.0, epsilon = 1e-9);
228 }
229
230 #[test]
231 fn flat_volume_is_neutral() {
232 let mut v = VolumeRsi::new(3).unwrap();
234 let candles: Vec<Candle> = (0..20).map(|_| vol_candle(2_000.0)).collect();
235 let last = v.batch(&candles).into_iter().flatten().last().unwrap();
236 assert_relative_eq!(last, 50.0, epsilon = 1e-12);
237 }
238
239 #[test]
240 fn output_in_range() {
241 let mut v = VolumeRsi::new(14).unwrap();
242 let candles: Vec<Candle> = (0..200)
243 .map(|i| vol_candle(1_000.0 + (f64::from(i) * 0.3).sin() * 600.0))
244 .collect();
245 for o in v.batch(&candles).into_iter().flatten() {
246 assert!((0.0..=100.0).contains(&o));
247 }
248 }
249
250 #[test]
251 fn reset_clears_state() {
252 let mut v = VolumeRsi::new(3).unwrap();
253 let candles: Vec<Candle> = (0..20)
254 .map(|i| vol_candle(1_000.0 + f64::from(i)))
255 .collect();
256 v.batch(&candles);
257 assert!(v.is_ready());
258 v.reset();
259 assert!(!v.is_ready());
260 assert_eq!(v.value(), None);
261 assert_eq!(v.update(vol_candle(1_000.0)), None);
262 }
263
264 #[test]
265 fn batch_equals_streaming() {
266 let candles: Vec<Candle> = (0..120)
267 .map(|i| vol_candle(1_000.0 + (f64::from(i) * 0.25).sin() * 500.0))
268 .collect();
269 let batch = VolumeRsi::new(14).unwrap().batch(&candles);
270 let mut b = VolumeRsi::new(14).unwrap();
271 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
272 assert_eq!(batch, streamed);
273 }
274}