1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
48pub struct Kvo {
49 fast_period: usize,
50 slow_period: usize,
51 fast: Ema,
52 slow: Ema,
53 prev_dm: Option<f64>,
54 prev_hlc: f64,
55 trend: i8,
56 cm: f64,
57}
58
59impl Kvo {
60 pub fn new(fast: usize, slow: usize) -> Result<Self> {
66 if fast == 0 || slow == 0 {
67 return Err(Error::PeriodZero);
68 }
69 if fast >= slow {
70 return Err(Error::InvalidPeriod {
71 message: "KVO needs fast < slow",
72 });
73 }
74 Ok(Self {
75 fast_period: fast,
76 slow_period: slow,
77 fast: Ema::new(fast)?,
78 slow: Ema::new(slow)?,
79 prev_dm: None,
80 prev_hlc: 0.0,
81 trend: 0,
82 cm: 0.0,
83 })
84 }
85
86 pub fn classic() -> Self {
88 Self::new(34, 55).expect("classic Klinger periods are valid")
89 }
90
91 pub const fn periods(&self) -> (usize, usize) {
93 (self.fast_period, self.slow_period)
94 }
95}
96
97impl Indicator for Kvo {
98 type Input = Candle;
99 type Output = f64;
100
101 #[inline]
102 fn update(&mut self, candle: Candle) -> Option<f64> {
103 let hlc = candle.high + candle.low + candle.close;
104 let dm = candle.high - candle.low;
105 let Some(prev_dm) = self.prev_dm else {
106 self.prev_dm = Some(dm);
108 self.prev_hlc = hlc;
109 return None;
110 };
111
112 let new_trend: i8 = if hlc > self.prev_hlc {
114 1
115 } else if hlc < self.prev_hlc {
116 -1
117 } else {
118 self.trend
119 };
120
121 if new_trend != self.trend || self.trend == 0 {
125 self.cm = prev_dm + dm;
126 } else {
127 self.cm += dm;
128 }
129 self.trend = new_trend;
130
131 let vf = if self.cm == 0.0 {
132 0.0
134 } else {
135 candle.volume * (2.0 * (dm / self.cm - 1.0)).abs() * f64::from(new_trend) * 100.0
136 };
137
138 self.prev_dm = Some(dm);
139 self.prev_hlc = hlc;
140
141 let fast = self.fast.update(vf);
142 let slow = self.slow.update(vf);
143 Some(fast? - slow?)
144 }
145
146 fn reset(&mut self) {
147 self.fast.reset();
148 self.slow.reset();
149 self.prev_dm = None;
150 self.prev_hlc = 0.0;
151 self.trend = 0;
152 self.cm = 0.0;
153 }
154
155 #[inline]
156 fn warmup_period(&self) -> usize {
157 self.slow_period + 1
159 }
160
161 #[inline]
162 fn is_ready(&self) -> bool {
163 self.fast.is_ready() && self.slow.is_ready()
164 }
165
166 #[inline]
167 fn name(&self) -> &'static str {
168 "KVO"
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::traits::BatchExt;
176 use approx::assert_relative_eq;
177
178 fn c(high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
179 Candle::new(low, high, low, close, volume, ts).unwrap()
180 }
181
182 #[test]
183 fn rejects_zero_period() {
184 assert!(matches!(Kvo::new(0, 10), Err(Error::PeriodZero)));
185 assert!(matches!(Kvo::new(3, 0), Err(Error::PeriodZero)));
186 }
187
188 #[test]
189 fn rejects_fast_geq_slow() {
190 assert!(matches!(Kvo::new(34, 34), Err(Error::InvalidPeriod { .. })));
191 assert!(matches!(Kvo::new(55, 34), Err(Error::InvalidPeriod { .. })));
192 }
193
194 #[test]
195 fn accessors_and_metadata() {
196 let k = Kvo::classic();
197 assert_eq!(k.periods(), (34, 55));
198 assert_eq!(k.name(), "KVO");
199 assert_eq!(k.warmup_period(), 56);
200 }
201
202 #[test]
203 fn zero_ohlc_collapses_vf_to_zero() {
204 let mut k = Kvo::new(3, 6).unwrap();
207 let zero = Candle::new(0.0, 0.0, 0.0, 0.0, 100.0, 0).unwrap();
208 assert_eq!(k.update(zero), None);
209 assert_eq!(k.update(zero), None);
210 assert_eq!(k.update(zero), None);
211 }
212
213 #[test]
214 fn constant_series_yields_zero() {
215 let candles: Vec<Candle> = (0..120).map(|i| c(10.0, 10.0, 10.0, 100.0, i)).collect();
218 let mut k = Kvo::new(3, 6).unwrap();
219 for v in k.batch(&candles).into_iter().flatten() {
220 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
221 }
222 }
223
224 #[test]
225 fn warmup_emits_at_slow_plus_one() {
226 let candles: Vec<Candle> = (0..30i64)
227 .map(|i| {
228 let f = i as f64;
229 c(10.0 + f, 8.0 + f, 9.0 + f, 100.0, i)
230 })
231 .collect();
232 let mut k = Kvo::new(3, 5).unwrap();
233 let out = k.batch(&candles);
234 for (i, v) in out.iter().enumerate().take(5) {
235 assert!(v.is_none(), "index {i} must be None during warmup");
236 }
237 assert!(out[5].is_some(), "first value lands at slow_period");
239 }
240
241 #[test]
242 fn batch_equals_streaming() {
243 let candles: Vec<Candle> = (0..100i64)
244 .map(|i| {
245 let f = i as f64;
246 let mid = 100.0 + (f * 0.2).sin() * 4.0;
247 c(mid + 1.0, mid - 1.0, mid, 10.0 + ((i % 5) as f64), i)
248 })
249 .collect();
250 let mut a = Kvo::classic();
251 let mut b = Kvo::classic();
252 assert_eq!(
253 a.batch(&candles),
254 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
255 );
256 }
257
258 #[test]
259 fn reset_clears_state() {
260 let candles: Vec<Candle> = (0..80i64)
261 .map(|i| {
262 let f = i as f64;
263 c(11.0 + f, 9.0 + f, 10.0 + f, 100.0, i)
264 })
265 .collect();
266 let mut k = Kvo::classic();
267 k.batch(&candles);
268 assert!(k.is_ready());
269 k.reset();
270 assert!(!k.is_ready());
271 assert_eq!(k.update(candles[0]), None);
272 }
273
274 fn wave(len: i64) -> Vec<Candle> {
275 (0..len)
276 .map(|i| {
277 let step = f64::from(i32::try_from(i).unwrap());
278 let mid = 100.0 + (step * 0.31).sin() * 4.0;
279 let half = 0.6 + (step * 0.17).cos().abs();
280 c(
281 mid + half,
282 mid - half,
283 mid + 0.3 * half,
284 50.0 + (step * 0.7).sin() * 20.0,
285 i,
286 )
287 })
288 .collect()
289 }
290
291 #[test]
292 fn rejects_period_above_max() {
293 let too_big = crate::error::MAX_PERIOD + 1;
294 assert!(matches!(
295 Kvo::new(3, too_big),
296 Err(Error::InvalidPeriod { .. })
297 ));
298 }
299
300 #[test]
301 fn hand_computed_reference() {
302 let candles = [
318 c(10.0, 8.0, 9.0, 100.0, 0),
319 c(11.0, 9.0, 10.0, 100.0, 1),
320 c(12.0, 9.0, 11.0, 200.0, 2),
321 c(11.0, 10.0, 10.5, 100.0, 3),
322 c(11.0, 10.0, 10.5, 100.0, 4),
323 c(12.0, 9.5, 11.0, 100.0, 5),
324 ];
325 let mut k = Kvo::new(2, 3).unwrap();
326 assert_eq!(k.warmup_period(), 4);
327 let out = k.batch(&candles);
328 assert!(out[..3].iter().all(Option::is_none));
329 assert_relative_eq!(out[3].unwrap(), -220_000.0 / 21.0, max_relative = 1e-12);
330 assert_relative_eq!(out[4].unwrap(), -450_500.0 / 63.0, max_relative = 1e-12);
331 let f5 = 2.0 / 3.0 * (40_000.0 / 7.0) + (-767_000.0 / 63.0) / 3.0;
332 let s5 = 0.5 * (40_000.0 / 7.0) + 0.5 * (-316_500.0 / 63.0);
333 assert_relative_eq!(out[5].unwrap(), f5 - s5, max_relative = 1e-12);
334 }
335
336 #[test]
337 fn zero_volume_yields_zero() {
338 let candles: Vec<Candle> = wave(30)
340 .into_iter()
341 .map(|x| Candle::new(x.open, x.high, x.low, x.close, 0.0, x.timestamp).unwrap())
342 .collect();
343 let out = Kvo::new(3, 5).unwrap().batch(&candles);
344 assert!(out[5..].iter().all(|v| v.is_some_and(|x| x.abs() < 1e-12)));
345 }
346
347 #[test]
348 fn zero_range_after_a_flip_registers_no_force() {
349 let mut k = Kvo::new(1, 2).unwrap();
352 assert_eq!(k.update(c(10.0, 8.0, 9.0, 100.0, 0)), None);
353 assert_eq!(k.update(c(11.0, 11.0, 11.0, 100.0, 1)), None);
354 let v = k.update(c(10.0, 10.0, 10.0, 100.0, 2)).unwrap();
356 assert_relative_eq!(v, -10_000.0, epsilon = 1e-9);
357 }
358
359 #[test]
360 fn reset_reproduces_a_fresh_run() {
361 let candles = wave(80);
362 let mut k = Kvo::new(5, 13).unwrap();
363 let first = k.batch(&candles);
364 k.reset();
365 let second = k.batch(&candles);
366 assert_eq!(first, second);
367 assert_eq!(second, Kvo::new(5, 13).unwrap().batch(&candles));
368 }
369
370 #[test]
371 fn batch_nan_into_matches_streaming_bits() {
372 let candles = wave(80);
373 let mut streaming = Kvo::new(5, 13).unwrap();
374 let expected: Vec<u64> = candles
375 .iter()
376 .map(|x| streaming.update(*x).unwrap_or(f64::NAN).to_bits())
377 .collect();
378 let mut out = vec![0.0; candles.len()];
379 Kvo::new(5, 13).unwrap().batch_nan_into(&candles, &mut out);
380 let got: Vec<u64> = out.iter().map(|v| v.to_bits()).collect();
381 assert_eq!(got, expected);
382 }
383}