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).
5///
6/// Starts at `1000` and moves only on bars whose volume *fell* against the previous bar: there
7/// it changes by the close's relative change, `NVI += NVI * (close - prev_close) / prev_close`
8/// (no change for a non-positive previous close). On every other bar it stays where it was.
9///
10/// First output: with the first bar. [`Indicator::reset`] returns it to `1000`.
11#[derive(Debug, Clone)]
12pub struct NviEngine {
13    nvi: f64,
14    prev_bar: Option<Bar>,
15}
16
17impl NviEngine {
18    pub fn new() -> Self {
19        Self {
20            nvi: 1000.0,
21            prev_bar: None,
22        }
23    }
24}
25
26impl Default for NviEngine {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl Indicator for NviEngine {
33    fn name(&self) -> &str {
34        "nvi"
35    }
36
37    fn warmup_period(&self) -> usize {
38        1
39    }
40
41    fn reset(&mut self) {
42        self.nvi = 1000.0;
43        self.prev_bar = None;
44    }
45
46    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
47        let prev = match &self.prev_bar {
48            Some(p) => p.clone(),
49            None => {
50                self.prev_bar = Some(bar.clone());
51                return Some(IndicatorOutput::new(self.nvi));
52            }
53        };
54        self.prev_bar = Some(bar.clone());
55
56        if bar.volume < prev.volume {
57            let roc = if prev.close > 0.0 {
58                (bar.close - prev.close) / prev.close
59            } else {
60                0.0
61            };
62            self.nvi += self.nvi * roc;
63        }
64
65        Some(IndicatorOutput::new(self.nvi))
66    }
67
68    fn alerts(&self) -> Vec<IndicatorAlert> {
69        Vec::new()
70    }
71}
72
73/// Positive Volume Index (PVI).
74///
75/// Starts at `1000` and moves only on bars whose volume *rose* against the previous bar: there
76/// it changes by the close's relative change, `PVI += PVI * (close - prev_close) / prev_close`
77/// (no change for a non-positive previous close). On every other bar it stays where it was.
78///
79/// First output: with the first bar. [`Indicator::reset`] returns it to `1000`.
80#[derive(Debug, Clone)]
81pub struct PviEngine {
82    pvi: f64,
83    prev_bar: Option<Bar>,
84}
85
86impl PviEngine {
87    pub fn new() -> Self {
88        Self {
89            pvi: 1000.0,
90            prev_bar: None,
91        }
92    }
93}
94
95impl Default for PviEngine {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101impl Indicator for PviEngine {
102    fn name(&self) -> &str {
103        "pvi"
104    }
105
106    fn warmup_period(&self) -> usize {
107        1
108    }
109
110    fn reset(&mut self) {
111        self.pvi = 1000.0;
112        self.prev_bar = None;
113    }
114
115    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
116        let prev = match &self.prev_bar {
117            Some(p) => p.clone(),
118            None => {
119                self.prev_bar = Some(bar.clone());
120                return Some(IndicatorOutput::new(self.pvi));
121            }
122        };
123        self.prev_bar = Some(bar.clone());
124
125        if bar.volume > prev.volume {
126            let roc = if prev.close > 0.0 {
127                (bar.close - prev.close) / prev.close
128            } else {
129                0.0
130            };
131            self.pvi += self.pvi * roc;
132        }
133
134        Some(IndicatorOutput::new(self.pvi))
135    }
136
137    fn alerts(&self) -> Vec<IndicatorAlert> {
138        Vec::new()
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_nvi_pvi() {
148        let mut nvi = NviEngine::new();
149        let mut pvi = PviEngine::new();
150
151        let b1 = Bar::new(1, 100.0, 105.0, 95.0, 100.0, 1000.0);
152        let b2 = Bar::new(2, 100.0, 105.0, 95.0, 105.0, 500.0); // Volume decreased -> NVI updates
153
154        nvi.on_bar(&b1);
155        pvi.on_bar(&b1);
156
157        let out_nvi = nvi.on_bar(&b2).unwrap();
158        let out_pvi = pvi.on_bar(&b2).unwrap();
159
160        assert_eq!(out_nvi.value, 1050.0);
161        assert_eq!(out_pvi.value, 1000.0);
162    }
163}