wickra_core/indicators/
kvo.rs1use 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 trend: i8,
55 cm: f64,
56}
57
58impl Kvo {
59 pub fn new(fast: usize, slow: usize) -> Result<Self> {
65 if fast == 0 || slow == 0 {
66 return Err(Error::PeriodZero);
67 }
68 if fast >= slow {
69 return Err(Error::InvalidPeriod {
70 message: "KVO needs fast < slow",
71 });
72 }
73 Ok(Self {
74 fast_period: fast,
75 slow_period: slow,
76 fast: Ema::new(fast)?,
77 slow: Ema::new(slow)?,
78 prev_dm: None,
79 trend: 0,
80 cm: 0.0,
81 })
82 }
83
84 pub fn classic() -> Self {
86 Self::new(34, 55).expect("classic Klinger periods are valid")
87 }
88
89 pub const fn periods(&self) -> (usize, usize) {
91 (self.fast_period, self.slow_period)
92 }
93}
94
95impl Indicator for Kvo {
96 type Input = Candle;
97 type Output = f64;
98
99 fn update(&mut self, candle: Candle) -> Option<f64> {
100 let dm = candle.high + candle.low + candle.close;
101 let Some(prev_dm) = self.prev_dm else {
102 self.prev_dm = Some(dm);
104 return None;
105 };
106
107 let new_trend: i8 = if dm > prev_dm {
109 1
110 } else if dm < prev_dm {
111 -1
112 } else {
113 self.trend
114 };
115
116 if new_trend != self.trend || self.trend == 0 {
120 self.cm = prev_dm + dm;
121 } else {
122 self.cm += dm;
123 }
124 self.trend = new_trend;
125
126 let vf = if self.cm == 0.0 {
127 0.0
129 } else {
130 candle.volume * (2.0 * (dm / self.cm - 1.0)).abs() * f64::from(new_trend) * 100.0
131 };
132
133 self.prev_dm = Some(dm);
134
135 let fast = self.fast.update(vf);
136 let slow = self.slow.update(vf);
137 Some(fast? - slow?)
138 }
139
140 fn reset(&mut self) {
141 self.fast.reset();
142 self.slow.reset();
143 self.prev_dm = None;
144 self.trend = 0;
145 self.cm = 0.0;
146 }
147
148 fn warmup_period(&self) -> usize {
149 self.slow_period + 1
151 }
152
153 fn is_ready(&self) -> bool {
154 self.fast.is_ready() && self.slow.is_ready()
155 }
156
157 fn name(&self) -> &'static str {
158 "KVO"
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::traits::BatchExt;
166 use approx::assert_relative_eq;
167
168 fn c(high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
169 Candle::new(low, high, low, close, volume, ts).unwrap()
170 }
171
172 #[test]
173 fn rejects_zero_period() {
174 assert!(matches!(Kvo::new(0, 10), Err(Error::PeriodZero)));
175 assert!(matches!(Kvo::new(3, 0), Err(Error::PeriodZero)));
176 }
177
178 #[test]
179 fn rejects_fast_geq_slow() {
180 assert!(matches!(Kvo::new(34, 34), Err(Error::InvalidPeriod { .. })));
181 assert!(matches!(Kvo::new(55, 34), Err(Error::InvalidPeriod { .. })));
182 }
183
184 #[test]
185 fn accessors_and_metadata() {
186 let k = Kvo::classic();
187 assert_eq!(k.periods(), (34, 55));
188 assert_eq!(k.name(), "KVO");
189 assert_eq!(k.warmup_period(), 56);
190 }
191
192 #[test]
193 fn zero_ohlc_collapses_vf_to_zero() {
194 let mut k = Kvo::new(3, 6).unwrap();
197 let zero = Candle::new(0.0, 0.0, 0.0, 0.0, 100.0, 0).unwrap();
198 assert_eq!(k.update(zero), None);
199 assert_eq!(k.update(zero), None);
200 assert_eq!(k.update(zero), None);
201 }
202
203 #[test]
204 fn constant_series_yields_zero() {
205 let candles: Vec<Candle> = (0..120).map(|i| c(10.0, 10.0, 10.0, 100.0, i)).collect();
208 let mut k = Kvo::new(3, 6).unwrap();
209 for v in k.batch(&candles).into_iter().flatten() {
210 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
211 }
212 }
213
214 #[test]
215 fn warmup_emits_at_slow_plus_one() {
216 let candles: Vec<Candle> = (0..30i64)
217 .map(|i| {
218 let f = i as f64;
219 c(10.0 + f, 8.0 + f, 9.0 + f, 100.0, i)
220 })
221 .collect();
222 let mut k = Kvo::new(3, 5).unwrap();
223 let out = k.batch(&candles);
224 for (i, v) in out.iter().enumerate().take(5) {
225 assert!(v.is_none(), "index {i} must be None during warmup");
226 }
227 assert!(out[5].is_some(), "first value lands at slow_period");
229 }
230
231 #[test]
232 fn batch_equals_streaming() {
233 let candles: Vec<Candle> = (0..100i64)
234 .map(|i| {
235 let f = i as f64;
236 let mid = 100.0 + (f * 0.2).sin() * 4.0;
237 c(mid + 1.0, mid - 1.0, mid, 10.0 + ((i % 5) as f64), i)
238 })
239 .collect();
240 let mut a = Kvo::classic();
241 let mut b = Kvo::classic();
242 assert_eq!(
243 a.batch(&candles),
244 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
245 );
246 }
247
248 #[test]
249 fn reset_clears_state() {
250 let candles: Vec<Candle> = (0..80i64)
251 .map(|i| {
252 let f = i as f64;
253 c(11.0 + f, 9.0 + f, 10.0 + f, 100.0, i)
254 })
255 .collect();
256 let mut k = Kvo::classic();
257 k.batch(&candles);
258 assert!(k.is_ready());
259 k.reset();
260 assert!(!k.is_ready());
261 assert_eq!(k.update(candles[0]), None);
262 }
263}