Skip to main content

kestrel_chartkit/indicator/
rvi.rs

1use super::smoothing::Sma;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use std::collections::HashMap;
5
6/// Relative Vigor Index (RVI) Engine.
7/// RVI = SMA((Close - Open) / (High - Low), period)
8#[derive(Debug, Clone)]
9pub struct RviEngine {
10    period: usize,
11    val_sma: Sma,
12    sig_sma: Sma,
13}
14
15impl RviEngine {
16    pub fn new(period: usize) -> Self {
17        Self {
18            period: period.max(1),
19            val_sma: Sma::new(period),
20            sig_sma: Sma::new(4),
21        }
22    }
23}
24
25impl Indicator for RviEngine {
26    fn name(&self) -> &str {
27        "rvi"
28    }
29
30    fn warmup_period(&self) -> usize {
31        self.period + 4
32    }
33
34    fn reset(&mut self) {
35        self.val_sma.reset();
36        self.sig_sma.reset();
37    }
38
39    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
40        let range = (bar.high - bar.low).max(1e-8);
41        let raw_val = (bar.close - bar.open) / range;
42
43        let rvi_val = self.val_sma.update(raw_val)?;
44        let sig_val = self.sig_sma.update(rvi_val)?;
45
46        let mut extra = HashMap::new();
47        extra.insert("rvi".to_string(), rvi_val);
48        extra.insert("signal".to_string(), sig_val);
49
50        Some(IndicatorOutput::with_extra(rvi_val, extra))
51    }
52
53    fn alerts(&self) -> Vec<IndicatorAlert> {
54        Vec::new()
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn test_rvi_basic() {
64        let mut rvi = RviEngine::new(10);
65        let mut out = None;
66        for i in 0..20 {
67            let b = Bar::new(i, 100.0, 105.0, 95.0, 104.0, 1000.0);
68            out = rvi.on_bar(&b);
69        }
70        assert!(out.is_some());
71    }
72}