Skip to main content

kestrel_chartkit/indicator/
nvi_pvi.rs

1use super::{Indicator, IndicatorAlert, IndicatorOutput};
2use crate::model::Bar;
3
4/// Negative Volume Index (NVI) Engine.
5/// Updates index value only on days when volume decreases.
6#[derive(Debug, Clone)]
7pub struct NviEngine {
8    nvi: f64,
9    prev_bar: Option<Bar>,
10}
11
12impl NviEngine {
13    pub fn new() -> Self {
14        Self {
15            nvi: 1000.0,
16            prev_bar: None,
17        }
18    }
19}
20
21impl Default for NviEngine {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl Indicator for NviEngine {
28    fn name(&self) -> &str {
29        "nvi"
30    }
31
32    fn warmup_period(&self) -> usize {
33        1
34    }
35
36    fn reset(&mut self) {
37        self.nvi = 1000.0;
38        self.prev_bar = None;
39    }
40
41    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
42        let prev = match &self.prev_bar {
43            Some(p) => p.clone(),
44            None => {
45                self.prev_bar = Some(bar.clone());
46                return Some(IndicatorOutput::new(self.nvi));
47            }
48        };
49        self.prev_bar = Some(bar.clone());
50
51        if bar.volume < prev.volume {
52            let roc = if prev.close > 0.0 {
53                (bar.close - prev.close) / prev.close
54            } else {
55                0.0
56            };
57            self.nvi += self.nvi * roc;
58        }
59
60        Some(IndicatorOutput::new(self.nvi))
61    }
62
63    fn alerts(&self) -> Vec<IndicatorAlert> {
64        Vec::new()
65    }
66}
67
68/// Positive Volume Index (PVI) Engine.
69/// Updates index value only on days when volume increases.
70#[derive(Debug, Clone)]
71pub struct PviEngine {
72    pvi: f64,
73    prev_bar: Option<Bar>,
74}
75
76impl PviEngine {
77    pub fn new() -> Self {
78        Self {
79            pvi: 1000.0,
80            prev_bar: None,
81        }
82    }
83}
84
85impl Default for PviEngine {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl Indicator for PviEngine {
92    fn name(&self) -> &str {
93        "pvi"
94    }
95
96    fn warmup_period(&self) -> usize {
97        1
98    }
99
100    fn reset(&mut self) {
101        self.pvi = 1000.0;
102        self.prev_bar = None;
103    }
104
105    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
106        let prev = match &self.prev_bar {
107            Some(p) => p.clone(),
108            None => {
109                self.prev_bar = Some(bar.clone());
110                return Some(IndicatorOutput::new(self.pvi));
111            }
112        };
113        self.prev_bar = Some(bar.clone());
114
115        if bar.volume > prev.volume {
116            let roc = if prev.close > 0.0 {
117                (bar.close - prev.close) / prev.close
118            } else {
119                0.0
120            };
121            self.pvi += self.pvi * roc;
122        }
123
124        Some(IndicatorOutput::new(self.pvi))
125    }
126
127    fn alerts(&self) -> Vec<IndicatorAlert> {
128        Vec::new()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_nvi_pvi() {
138        let mut nvi = NviEngine::new();
139        let mut pvi = PviEngine::new();
140
141        let b1 = Bar::new(1, 100.0, 105.0, 95.0, 100.0, 1000.0);
142        let b2 = Bar::new(2, 100.0, 105.0, 95.0, 105.0, 500.0); // Volume decreased -> NVI updates
143
144        nvi.on_bar(&b1);
145        pvi.on_bar(&b1);
146
147        let out_nvi = nvi.on_bar(&b2).unwrap();
148        let out_pvi = pvi.on_bar(&b2).unwrap();
149
150        assert_eq!(out_nvi.value, 1050.0);
151        assert_eq!(out_pvi.value, 1000.0);
152    }
153}