Skip to main content

kestrel_chartkit/indicator/
vidya.rs

1use crate::model::Bar;
2
3use super::momentum_indicators::CmoEngine;
4use super::{Indicator, IndicatorOutput};
5
6/// Variable Index Dynamic Average: an exponential average whose smoothing constant is scaled by
7/// how directional the recent price movement is.
8///
9/// `alpha_t = 2/(ema_len + 1) * |CMO_t| / 100` and
10/// `VIDYA_t = alpha_t * close_t + (1 - alpha_t) * VIDYA_{t-1}`.
11///
12/// The Chande Momentum Oscillator comes from the existing [`CmoEngine`] over `cmo_len` and is
13/// already scaled to `-100..=100`, which is why the formula divides by 100 exactly once. In a
14/// directionless market `|CMO|` approaches zero, `alpha` with it, and the line holds its level;
15/// at `|CMO| = 100` the average reacts like a plain `Ema(ema_len)`.
16///
17/// This is the CMO-based variant. The variant driven by a ratio of standard deviations carries
18/// the same name elsewhere but is a different formula; it is not offered here.
19///
20/// The two periods are separate on purpose: `cmo_len` sets how far back the directionality is
21/// measured, `ema_len` the base smoothing that directionality scales.
22///
23/// Output: `value` in the price units of the series.
24///
25/// First output: with the first bar the CMO is defined for, i.e. after `cmo_len + 1` bars; that
26/// first value is the close of that bar (the seed). A bar whose CMO is zero leaves the line
27/// exactly where it was — the average then carries no new information, which is the point of the
28/// construction, not a gap. [`Indicator::reset`] clears the CMO state and the line, so the next
29/// series starts deterministically.
30#[derive(Debug, Clone)]
31pub struct Vidya {
32    ema_len: usize,
33    cmo: CmoEngine,
34    state: Option<f64>,
35}
36
37impl Vidya {
38    pub fn new(cmo_len: usize, ema_len: usize) -> Self {
39        Self {
40            ema_len: ema_len.max(1),
41            cmo: CmoEngine::new(cmo_len.max(1)),
42            state: None,
43        }
44    }
45
46    pub fn with_defaults() -> Self {
47        Self::new(9, 12)
48    }
49}
50
51impl Indicator for Vidya {
52    fn name(&self) -> &str {
53        "vidya"
54    }
55
56    fn warmup_period(&self) -> usize {
57        self.cmo.warmup_period()
58    }
59
60    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
61        let cmo = self.cmo.on_bar(bar)?.value;
62
63        let value = match self.state {
64            None => bar.close,
65            Some(prev) => {
66                let alpha = 2.0 / (self.ema_len as f64 + 1.0) * (cmo.abs() / 100.0);
67                alpha * bar.close + (1.0 - alpha) * prev
68            }
69        };
70        self.state = Some(value);
71
72        Some(IndicatorOutput::new(value))
73    }
74
75    fn reset(&mut self) {
76        self.cmo.reset();
77        self.state = None;
78    }
79}