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