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