wickra_core/indicators/
vzo.rs1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
44pub struct Vzo {
45 period: usize,
46 vp: Ema,
47 tv: Ema,
48 prev_close: Option<f64>,
49}
50
51impl Vzo {
52 pub fn new(period: usize) -> Result<Self> {
57 if period == 0 {
58 return Err(Error::PeriodZero);
59 }
60 if period > crate::error::MAX_PERIOD {
61 return Err(Error::InvalidPeriod {
62 message: crate::error::PERIOD_ABOVE_MAX,
63 });
64 }
65 Ok(Self {
66 period,
67 vp: Ema::new(period)?,
68 tv: Ema::new(period)?,
69 prev_close: None,
70 })
71 }
72
73 pub const fn period(&self) -> usize {
75 self.period
76 }
77}
78
79impl Indicator for Vzo {
80 type Input = Candle;
81 type Output = f64;
82
83 #[inline]
84 fn update(&mut self, candle: Candle) -> Option<f64> {
85 let signed_volume = match self.prev_close {
86 None => {
87 self.prev_close = Some(candle.close);
88 return None;
89 }
90 Some(prev) => {
91 if candle.close > prev {
92 candle.volume
93 } else if candle.close < prev {
94 -candle.volume
95 } else {
96 0.0
97 }
98 }
99 };
100 self.prev_close = Some(candle.close);
101 let vp = self.vp.update(signed_volume);
102 let tv = self.tv.update(candle.volume);
103 let (vp_v, tv_v) = (vp?, tv?);
104 if tv_v == 0.0 {
105 return Some(0.0);
107 }
108 Some(100.0 * vp_v / tv_v)
109 }
110
111 fn reset(&mut self) {
112 self.vp.reset();
113 self.tv.reset();
114 self.prev_close = None;
115 }
116
117 #[inline]
118 fn warmup_period(&self) -> usize {
119 self.period + 1
121 }
122
123 #[inline]
124 fn is_ready(&self) -> bool {
125 self.vp.is_ready() && self.tv.is_ready()
126 }
127
128 #[inline]
129 fn name(&self) -> &'static str {
130 "VZO"
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::traits::BatchExt;
138 use approx::assert_relative_eq;
139
140 fn c(close: f64, volume: f64, ts: i64) -> Candle {
141 Candle::new(close, close, close, close, volume, ts).unwrap()
142 }
143
144 #[test]
145 fn rejects_zero_period() {
146 assert!(matches!(Vzo::new(0), Err(Error::PeriodZero)));
147 }
148
149 #[test]
150 fn accessors_and_metadata() {
151 let v = Vzo::new(14).unwrap();
152 assert_eq!(v.period(), 14);
153 assert_eq!(v.name(), "VZO");
154 assert_eq!(v.warmup_period(), 15);
155 }
156
157 #[test]
158 fn strictly_rising_series_saturates_to_plus_100() {
159 let candles: Vec<Candle> = (0..60i64).map(|i| c(10.0 + i as f64, 100.0, i)).collect();
162 let mut v = Vzo::new(5).unwrap();
163 let out = v.batch(&candles);
164 let last = out.iter().filter_map(|x| *x).next_back().unwrap();
165 assert_relative_eq!(last, 100.0, epsilon = 1e-9);
166 }
167
168 #[test]
169 fn strictly_falling_series_saturates_to_minus_100() {
170 let candles: Vec<Candle> = (0..60i64).map(|i| c(200.0 - i as f64, 100.0, i)).collect();
171 let mut v = Vzo::new(5).unwrap();
172 let out = v.batch(&candles);
173 let last = out.iter().filter_map(|x| *x).next_back().unwrap();
174 assert_relative_eq!(last, -100.0, epsilon = 1e-9);
175 }
176
177 #[test]
178 fn flat_close_yields_zero() {
179 let candles: Vec<Candle> = (0..40).map(|i| c(10.0, 100.0, i)).collect();
181 let mut v = Vzo::new(5).unwrap();
182 for x in v.batch(&candles).into_iter().flatten() {
183 assert_relative_eq!(x, 0.0, epsilon = 1e-9);
184 }
185 }
186
187 #[test]
188 fn zero_volume_window_yields_zero() {
189 let candles: Vec<Candle> = (0..20i64).map(|i| c(10.0 + i as f64, 0.0, i)).collect();
191 let mut v = Vzo::new(3).unwrap();
192 let out = v.batch(&candles);
193 let last = out.iter().filter_map(|x| *x).next_back().unwrap();
194 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
195 }
196
197 #[test]
198 fn batch_equals_streaming() {
199 let candles: Vec<Candle> = (0..100i64)
200 .map(|i| {
201 let f = i as f64;
202 c(
203 100.0 + (f * 0.3).sin() * 5.0,
204 50.0 + (i % 7) as f64 * 10.0,
205 i,
206 )
207 })
208 .collect();
209 let mut a = Vzo::new(14).unwrap();
210 let mut b = Vzo::new(14).unwrap();
211 assert_eq!(
212 a.batch(&candles),
213 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
214 );
215 }
216
217 #[test]
218 fn reset_clears_state() {
219 let candles: Vec<Candle> = (0..40i64).map(|i| c(10.0 + i as f64, 100.0, i)).collect();
220 let mut v = Vzo::new(5).unwrap();
221 v.batch(&candles);
222 assert!(v.is_ready());
223 v.reset();
224 assert!(!v.is_ready());
225 assert_eq!(v.update(candles[0]), None);
226 }
227}