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 fn update(&mut self, candle: Candle) -> Option<f64> {
79 let f = self.fast.update(candle.volume);
80 let s = self.slow.update(candle.volume);
81 let (fast_v, slow_v) = (f?, s?);
82 if slow_v == 0.0 {
83 return Some(0.0);
85 }
86 Some(100.0 * (fast_v - slow_v) / slow_v)
87 }
88
89 fn reset(&mut self) {
90 self.fast.reset();
91 self.slow.reset();
92 }
93
94 fn warmup_period(&self) -> usize {
95 self.slow_period
96 }
97
98 fn is_ready(&self) -> bool {
99 self.slow.is_ready()
100 }
101
102 fn name(&self) -> &'static str {
103 "VolumeOscillator"
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::traits::BatchExt;
111 use approx::assert_relative_eq;
112
113 fn c(volume: f64, ts: i64) -> Candle {
114 Candle::new(10.0, 10.0, 10.0, 10.0, volume, ts).unwrap()
115 }
116
117 #[test]
118 fn rejects_zero_period() {
119 assert!(matches!(
120 VolumeOscillator::new(0, 5),
121 Err(Error::PeriodZero)
122 ));
123 assert!(matches!(
124 VolumeOscillator::new(5, 0),
125 Err(Error::PeriodZero)
126 ));
127 }
128
129 #[test]
130 fn rejects_fast_geq_slow() {
131 assert!(matches!(
132 VolumeOscillator::new(10, 10),
133 Err(Error::InvalidPeriod { .. })
134 ));
135 assert!(matches!(
136 VolumeOscillator::new(28, 14),
137 Err(Error::InvalidPeriod { .. })
138 ));
139 }
140
141 #[test]
142 fn accessors_and_metadata() {
143 let vo = VolumeOscillator::new(14, 28).unwrap();
144 assert_eq!(vo.periods(), (14, 28));
145 assert_eq!(vo.name(), "VolumeOscillator");
146 assert_eq!(vo.warmup_period(), 28);
147 }
148
149 #[test]
150 fn constant_volume_yields_zero() {
151 let mut vo = VolumeOscillator::new(3, 6).unwrap();
153 let candles: Vec<Candle> = (0..30i64).map(|i| c(500.0, i)).collect();
154 for v in vo.batch(&candles).into_iter().flatten() {
155 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
156 }
157 }
158
159 #[test]
160 fn zero_volume_window_yields_zero() {
161 let mut vo = VolumeOscillator::new(2, 4).unwrap();
163 let candles: Vec<Candle> = (0..10i64).map(|i| c(0.0, i)).collect();
164 let out = vo.batch(&candles);
165 assert_relative_eq!(out[3].unwrap(), 0.0, epsilon = 1e-12);
166 }
167
168 #[test]
169 fn reference_value() {
170 let mut vo = VolumeOscillator::new(2, 4).unwrap();
174 let candles = [c(10.0, 0), c(20.0, 1), c(30.0, 2), c(40.0, 3), c(50.0, 4)];
175 let out = vo.batch(&candles);
176 assert!(out[0].is_none() && out[1].is_none() && out[2].is_none());
177 assert_relative_eq!(out[3].unwrap(), 40.0, epsilon = 1e-9);
178 assert_relative_eq!(out[4].unwrap(), 1000.0 / 35.0, epsilon = 1e-9);
181 }
182
183 #[test]
184 fn batch_equals_streaming() {
185 let candles: Vec<Candle> = (0..80i64)
186 .map(|i| c(100.0 + ((i % 11) as f64) * 5.0, i))
187 .collect();
188 let mut a = VolumeOscillator::new(14, 28).unwrap();
189 let mut b = VolumeOscillator::new(14, 28).unwrap();
190 assert_eq!(
191 a.batch(&candles),
192 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
193 );
194 }
195
196 #[test]
197 fn reset_clears_state() {
198 let candles: Vec<Candle> = (0..60i64).map(|i| c(100.0 + (i as f64), i)).collect();
199 let mut vo = VolumeOscillator::new(14, 28).unwrap();
200 vo.batch(&candles);
201 assert!(vo.is_ready());
202 vo.reset();
203 assert!(!vo.is_ready());
204 assert_eq!(vo.update(candles[0]), None);
205 }
206}