wickra_core/indicators/
volume_oscillator.rs1use crate::error::{Error, Result};
4use crate::indicators::sma::Sma;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
38pub struct VolumeOscillator {
39 fast_period: usize,
40 slow_period: usize,
41 fast: Sma,
42 slow: Sma,
43}
44
45impl VolumeOscillator {
46 pub fn new(fast: usize, slow: usize) -> Result<Self> {
52 if fast == 0 || slow == 0 {
53 return Err(Error::PeriodZero);
54 }
55 if fast >= slow {
56 return Err(Error::InvalidPeriod {
57 message: "VolumeOscillator needs fast < slow",
58 });
59 }
60 Ok(Self {
61 fast_period: fast,
62 slow_period: slow,
63 fast: Sma::new(fast)?,
64 slow: Sma::new(slow)?,
65 })
66 }
67
68 pub const fn periods(&self) -> (usize, usize) {
70 (self.fast_period, self.slow_period)
71 }
72}
73
74impl Indicator for VolumeOscillator {
75 type Input = Candle;
76 type Output = f64;
77
78 #[inline]
79 fn update(&mut self, candle: Candle) -> Option<f64> {
80 let f = self.fast.update(candle.volume);
81 let s = self.slow.update(candle.volume);
82 let (fast_v, slow_v) = (f?, s?);
83 if slow_v == 0.0 {
84 return Some(0.0);
86 }
87 Some(100.0 * (fast_v - slow_v) / slow_v)
88 }
89
90 fn reset(&mut self) {
91 self.fast.reset();
92 self.slow.reset();
93 }
94
95 #[inline]
96 fn warmup_period(&self) -> usize {
97 self.slow_period
98 }
99
100 #[inline]
101 fn is_ready(&self) -> bool {
102 self.slow.is_ready()
103 }
104
105 #[inline]
106 fn name(&self) -> &'static str {
107 "VolumeOscillator"
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::traits::BatchExt;
115 use approx::assert_relative_eq;
116
117 fn c(volume: f64, ts: i64) -> Candle {
118 Candle::new(10.0, 10.0, 10.0, 10.0, volume, ts).unwrap()
119 }
120
121 #[test]
122 fn rejects_zero_period() {
123 assert!(matches!(
124 VolumeOscillator::new(0, 5),
125 Err(Error::PeriodZero)
126 ));
127 assert!(matches!(
128 VolumeOscillator::new(5, 0),
129 Err(Error::PeriodZero)
130 ));
131 }
132
133 #[test]
134 fn rejects_fast_geq_slow() {
135 assert!(matches!(
136 VolumeOscillator::new(10, 10),
137 Err(Error::InvalidPeriod { .. })
138 ));
139 assert!(matches!(
140 VolumeOscillator::new(28, 14),
141 Err(Error::InvalidPeriod { .. })
142 ));
143 }
144
145 #[test]
146 fn accessors_and_metadata() {
147 let vo = VolumeOscillator::new(14, 28).unwrap();
148 assert_eq!(vo.periods(), (14, 28));
149 assert_eq!(vo.name(), "VolumeOscillator");
150 assert_eq!(vo.warmup_period(), 28);
151 }
152
153 #[test]
154 fn constant_volume_yields_zero() {
155 let mut vo = VolumeOscillator::new(3, 6).unwrap();
157 let candles: Vec<Candle> = (0..30i64).map(|i| c(500.0, i)).collect();
158 for v in vo.batch(&candles).into_iter().flatten() {
159 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
160 }
161 }
162
163 #[test]
164 fn zero_volume_window_yields_zero() {
165 let mut vo = VolumeOscillator::new(2, 4).unwrap();
167 let candles: Vec<Candle> = (0..10i64).map(|i| c(0.0, i)).collect();
168 let out = vo.batch(&candles);
169 assert_relative_eq!(out[3].unwrap(), 0.0, epsilon = 1e-12);
170 }
171
172 #[test]
173 fn reference_value() {
174 let mut vo = VolumeOscillator::new(2, 4).unwrap();
178 let candles = [c(10.0, 0), c(20.0, 1), c(30.0, 2), c(40.0, 3), c(50.0, 4)];
179 let out = vo.batch(&candles);
180 assert!(out[0].is_none() && out[1].is_none() && out[2].is_none());
181 assert_relative_eq!(out[3].unwrap(), 40.0, epsilon = 1e-9);
182 assert_relative_eq!(out[4].unwrap(), 1000.0 / 35.0, epsilon = 1e-9);
185 }
186
187 #[test]
188 fn batch_equals_streaming() {
189 let candles: Vec<Candle> = (0..80i64)
190 .map(|i| c(100.0 + ((i % 11) as f64) * 5.0, i))
191 .collect();
192 let mut a = VolumeOscillator::new(14, 28).unwrap();
193 let mut b = VolumeOscillator::new(14, 28).unwrap();
194 assert_eq!(
195 a.batch(&candles),
196 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
197 );
198 }
199
200 #[test]
201 fn reset_clears_state() {
202 let candles: Vec<Candle> = (0..60i64).map(|i| c(100.0 + (i as f64), i)).collect();
203 let mut vo = VolumeOscillator::new(14, 28).unwrap();
204 vo.batch(&candles);
205 assert!(vo.is_ready());
206 vo.reset();
207 assert!(!vo.is_ready());
208 assert_eq!(vo.update(candles[0]), None);
209 }
210}