wickra_core/indicators/
nvi.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6const STARTING_INDEX: f64 = 1000.0;
9
10#[derive(Debug, Clone)]
43pub struct Nvi {
44 prev_close: Option<f64>,
45 prev_volume: Option<f64>,
46 index: f64,
47 has_emitted: bool,
48}
49
50impl Nvi {
51 pub const fn new() -> Self {
53 Self {
54 prev_close: None,
55 prev_volume: None,
56 index: STARTING_INDEX,
57 has_emitted: false,
58 }
59 }
60
61 pub const fn with_baseline(baseline: f64) -> Self {
63 Self {
64 prev_close: None,
65 prev_volume: None,
66 index: baseline,
67 has_emitted: false,
68 }
69 }
70
71 pub const fn value(&self) -> Option<f64> {
73 if self.has_emitted {
74 Some(self.index)
75 } else {
76 None
77 }
78 }
79}
80
81impl Default for Nvi {
82 fn default() -> Self {
83 Self::new()
84 }
85}
86
87impl Indicator for Nvi {
88 type Input = Candle;
89 type Output = f64;
90
91 #[inline]
92 fn update(&mut self, candle: Candle) -> Option<f64> {
93 if let (Some(pc), Some(pv)) = (self.prev_close, self.prev_volume) {
96 if candle.volume < pv && pc != 0.0 {
97 let ret = (candle.close - pc) / pc;
98 self.index += self.index * ret;
99 }
100 }
101 self.prev_close = Some(candle.close);
102 self.prev_volume = Some(candle.volume);
103 self.has_emitted = true;
104 Some(self.index)
105 }
106
107 fn reset(&mut self) {
108 self.prev_close = None;
109 self.prev_volume = None;
110 self.index = STARTING_INDEX;
111 self.has_emitted = false;
112 }
113
114 #[inline]
115 fn warmup_period(&self) -> usize {
116 1
117 }
118
119 #[inline]
120 fn is_ready(&self) -> bool {
121 self.has_emitted
122 }
123
124 #[inline]
125 fn name(&self) -> &'static str {
126 "NVI"
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::traits::BatchExt;
134 use approx::assert_relative_eq;
135
136 fn c(close: f64, volume: f64, ts: i64) -> Candle {
137 Candle::new(close, close, close, close, volume, ts).unwrap()
138 }
139
140 #[test]
141 fn accessors_and_metadata() {
142 let mut n = Nvi::new();
143 assert_eq!(n.warmup_period(), 1);
144 assert_eq!(n.name(), "NVI");
145 assert_eq!(n.value(), None);
146 n.update(c(10.0, 100.0, 0));
147 assert_eq!(n.value(), Some(1000.0));
148 }
149
150 #[test]
151 fn default_matches_new() {
152 let a = Nvi::default();
153 let b = Nvi::new();
154 assert_eq!(a.warmup_period(), b.warmup_period());
155 assert_eq!(a.value(), b.value());
156 assert_eq!(a.is_ready(), b.is_ready());
157 }
158
159 #[test]
160 fn first_bar_seeds_baseline() {
161 let mut n = Nvi::new();
162 assert_relative_eq!(
163 n.update(c(10.0, 100.0, 0)).unwrap(),
164 1000.0,
165 epsilon = 1e-12
166 );
167 }
168
169 #[test]
170 fn volume_rise_leaves_index_unchanged() {
171 let mut n = Nvi::new();
174 n.update(c(10.0, 100.0, 0));
175 let v = n.update(c(11.0, 200.0, 1)).unwrap();
176 assert_relative_eq!(v, 1000.0, epsilon = 1e-12);
177 }
178
179 #[test]
180 fn volume_fall_applies_percent_change() {
181 let mut n = Nvi::new();
184 n.update(c(10.0, 200.0, 0));
185 let v = n.update(c(11.0, 100.0, 1)).unwrap();
186 assert_relative_eq!(v, 1100.0, epsilon = 1e-12);
187 }
188
189 #[test]
190 fn equal_volume_leaves_index_unchanged() {
191 let mut n = Nvi::new();
193 n.update(c(10.0, 100.0, 0));
194 let v = n.update(c(11.0, 100.0, 1)).unwrap();
195 assert_relative_eq!(v, 1000.0, epsilon = 1e-12);
196 }
197
198 #[test]
199 fn zero_previous_close_contributes_no_return() {
200 let mut n = Nvi::new();
202 n.update(c(0.0, 200.0, 0));
203 let v = n.update(c(5.0, 100.0, 1)).unwrap();
204 assert_relative_eq!(v, 1000.0, epsilon = 1e-12);
205 }
206
207 #[test]
208 fn custom_baseline() {
209 let mut n = Nvi::with_baseline(100.0);
210 assert_relative_eq!(n.update(c(10.0, 100.0, 0)).unwrap(), 100.0, epsilon = 1e-12);
211 }
212
213 #[test]
214 fn batch_equals_streaming() {
215 let candles: Vec<Candle> = (0..80i64)
216 .map(|i| {
217 let f = i as f64;
218 c(
219 100.0 + (f * 0.3).sin() * 5.0,
220 50.0 + ((i % 7) as f64) * 10.0,
221 i,
222 )
223 })
224 .collect();
225 let mut a = Nvi::new();
226 let mut b = Nvi::new();
227 assert_eq!(
228 a.batch(&candles),
229 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
230 );
231 }
232
233 #[test]
234 fn reset_clears_state() {
235 let mut n = Nvi::new();
236 n.batch(&[c(10.0, 200.0, 0), c(11.0, 100.0, 1)]);
237 assert!(n.is_ready());
238 n.reset();
239 assert!(!n.is_ready());
240 assert_eq!(n.value(), None);
241 assert_relative_eq!(n.update(c(50.0, 1.0, 2)).unwrap(), 1000.0, epsilon = 1e-12);
243 }
244}