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