kestrel-chartkit 0.11.3

High-performance Rust technical analysis library for indicator math, market regime classification, composite scoring, and SVG visualization.
Documentation
use super::smoothing::Sma;
use super::{Indicator, IndicatorAlert, IndicatorOutput};
use crate::model::Bar;
use std::collections::HashMap;

/// Vigor index: the close-open span relative to the range, averaged.
///
/// `RVI = SMA(period)((close - open) / (high - low))`, the range floored at `1e-8`;
/// `extra["signal"]` is `SMA(4)` of the RVI and `extra["rvi"]` repeats the value. This is a plain
/// average, not the symmetrically weighted four-bar numerator and denominator of the usual
/// Relative Vigor Index; on bars with `open == close` it is `0`.
///
/// First output: once the signal exists, i.e. with the `period + 3`-th bar. [`Indicator::reset`]
/// clears both averages.
#[derive(Debug, Clone)]
pub struct RviEngine {
    period: usize,
    val_sma: Sma,
    sig_sma: Sma,
}

impl RviEngine {
    pub fn new(period: usize) -> Self {
        Self {
            period: period.max(1),
            val_sma: Sma::new(period),
            sig_sma: Sma::new(4),
        }
    }
}

impl Indicator for RviEngine {
    fn name(&self) -> &str {
        "rvi"
    }

    fn warmup_period(&self) -> usize {
        self.period + 4
    }

    fn reset(&mut self) {
        self.val_sma.reset();
        self.sig_sma.reset();
    }

    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
        let range = (bar.high - bar.low).max(1e-8);
        let raw_val = (bar.close - bar.open) / range;

        let rvi_val = self.val_sma.update(raw_val)?;
        let sig_val = self.sig_sma.update(rvi_val)?;

        let mut extra = HashMap::new();
        extra.insert("rvi".to_string(), rvi_val);
        extra.insert("signal".to_string(), sig_val);

        Some(IndicatorOutput::with_extra(rvi_val, extra))
    }

    fn alerts(&self) -> Vec<IndicatorAlert> {
        Vec::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_rvi_basic() {
        let mut rvi = RviEngine::new(10);
        let mut out = None;
        for i in 0..20 {
            let b = Bar::new(i, 100.0, 105.0, 95.0, 104.0, 1000.0);
            out = rvi.on_bar(&b);
        }
        assert!(out.is_some());
    }
}