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