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 fn update(&mut self, candle: Candle) -> Option<f64> {
92 if let (Some(pc), Some(pv)) = (self.prev_close, self.prev_volume) {
95 if candle.volume < pv && pc != 0.0 {
96 let ret = (candle.close - pc) / pc;
97 self.index += self.index * ret;
98 }
99 }
100 self.prev_close = Some(candle.close);
101 self.prev_volume = Some(candle.volume);
102 self.has_emitted = true;
103 Some(self.index)
104 }
105
106 fn reset(&mut self) {
107 self.prev_close = None;
108 self.prev_volume = None;
109 self.index = STARTING_INDEX;
110 self.has_emitted = false;
111 }
112
113 fn warmup_period(&self) -> usize {
114 1
115 }
116
117 fn is_ready(&self) -> bool {
118 self.has_emitted
119 }
120
121 fn name(&self) -> &'static str {
122 "NVI"
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::traits::BatchExt;
130 use approx::assert_relative_eq;
131
132 fn c(close: f64, volume: f64, ts: i64) -> Candle {
133 Candle::new(close, close, close, close, volume, ts).unwrap()
134 }
135
136 #[test]
137 fn accessors_and_metadata() {
138 let mut n = Nvi::new();
139 assert_eq!(n.warmup_period(), 1);
140 assert_eq!(n.name(), "NVI");
141 assert_eq!(n.value(), None);
142 n.update(c(10.0, 100.0, 0));
143 assert_eq!(n.value(), Some(1000.0));
144 }
145
146 #[test]
147 fn default_matches_new() {
148 let a = Nvi::default();
149 let b = Nvi::new();
150 assert_eq!(a.warmup_period(), b.warmup_period());
151 assert_eq!(a.value(), b.value());
152 assert_eq!(a.is_ready(), b.is_ready());
153 }
154
155 #[test]
156 fn first_bar_seeds_baseline() {
157 let mut n = Nvi::new();
158 assert_relative_eq!(
159 n.update(c(10.0, 100.0, 0)).unwrap(),
160 1000.0,
161 epsilon = 1e-12
162 );
163 }
164
165 #[test]
166 fn volume_rise_leaves_index_unchanged() {
167 let mut n = Nvi::new();
170 n.update(c(10.0, 100.0, 0));
171 let v = n.update(c(11.0, 200.0, 1)).unwrap();
172 assert_relative_eq!(v, 1000.0, epsilon = 1e-12);
173 }
174
175 #[test]
176 fn volume_fall_applies_percent_change() {
177 let mut n = Nvi::new();
180 n.update(c(10.0, 200.0, 0));
181 let v = n.update(c(11.0, 100.0, 1)).unwrap();
182 assert_relative_eq!(v, 1100.0, epsilon = 1e-12);
183 }
184
185 #[test]
186 fn equal_volume_leaves_index_unchanged() {
187 let mut n = Nvi::new();
189 n.update(c(10.0, 100.0, 0));
190 let v = n.update(c(11.0, 100.0, 1)).unwrap();
191 assert_relative_eq!(v, 1000.0, epsilon = 1e-12);
192 }
193
194 #[test]
195 fn zero_previous_close_contributes_no_return() {
196 let mut n = Nvi::new();
198 n.update(c(0.0, 200.0, 0));
199 let v = n.update(c(5.0, 100.0, 1)).unwrap();
200 assert_relative_eq!(v, 1000.0, epsilon = 1e-12);
201 }
202
203 #[test]
204 fn custom_baseline() {
205 let mut n = Nvi::with_baseline(100.0);
206 assert_relative_eq!(n.update(c(10.0, 100.0, 0)).unwrap(), 100.0, epsilon = 1e-12);
207 }
208
209 #[test]
210 fn batch_equals_streaming() {
211 let candles: Vec<Candle> = (0..80i64)
212 .map(|i| {
213 let f = i as f64;
214 c(
215 100.0 + (f * 0.3).sin() * 5.0,
216 50.0 + ((i % 7) as f64) * 10.0,
217 i,
218 )
219 })
220 .collect();
221 let mut a = Nvi::new();
222 let mut b = Nvi::new();
223 assert_eq!(
224 a.batch(&candles),
225 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
226 );
227 }
228
229 #[test]
230 fn reset_clears_state() {
231 let mut n = Nvi::new();
232 n.batch(&[c(10.0, 200.0, 0), c(11.0, 100.0, 1)]);
233 assert!(n.is_ready());
234 n.reset();
235 assert!(!n.is_ready());
236 assert_eq!(n.value(), None);
237 assert_relative_eq!(n.update(c(50.0, 1.0, 2)).unwrap(), 1000.0, epsilon = 1e-12);
239 }
240}