wickra_core/indicators/
vpt.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
38pub struct VolumePriceTrend {
39 prev_close: Option<f64>,
40 total: f64,
41 has_emitted: bool,
42}
43
44impl VolumePriceTrend {
45 pub const fn new() -> Self {
47 Self {
48 prev_close: None,
49 total: 0.0,
50 has_emitted: false,
51 }
52 }
53
54 pub const fn value(&self) -> Option<f64> {
56 if self.has_emitted {
57 Some(self.total)
58 } else {
59 None
60 }
61 }
62}
63
64impl Indicator for VolumePriceTrend {
65 type Input = Candle;
66 type Output = f64;
67
68 #[inline]
69 fn update(&mut self, candle: Candle) -> Option<f64> {
70 self.has_emitted = true;
71 let Some(prev) = self.prev_close else {
72 self.prev_close = Some(candle.close);
74 return Some(self.total);
75 };
76 let roc = if prev == 0.0 {
77 0.0
79 } else {
80 (candle.close - prev) / prev
81 };
82 self.total += candle.volume * roc;
83 self.prev_close = Some(candle.close);
84 Some(self.total)
85 }
86
87 fn reset(&mut self) {
88 self.prev_close = None;
89 self.total = 0.0;
90 self.has_emitted = false;
91 }
92
93 #[inline]
94 fn warmup_period(&self) -> usize {
95 1
96 }
97
98 #[inline]
99 fn is_ready(&self) -> bool {
100 self.has_emitted
101 }
102
103 #[inline]
104 fn name(&self) -> &'static str {
105 "VPT"
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::traits::BatchExt;
113 use approx::assert_relative_eq;
114
115 fn candle(close: f64, volume: f64, ts: i64) -> Candle {
116 Candle::new(close, close, close, close, volume, ts).unwrap()
117 }
118
119 #[test]
120 fn reference_values() {
121 let mut vpt = VolumePriceTrend::new();
126 let out = vpt.batch(&[
127 candle(10.0, 100.0, 0),
128 candle(11.0, 200.0, 1),
129 candle(9.0, 300.0, 2),
130 ]);
131 assert_relative_eq!(out[0].unwrap(), 0.0, epsilon = 1e-12);
132 assert_relative_eq!(out[1].unwrap(), 20.0, epsilon = 1e-12);
133 assert_relative_eq!(out[2].unwrap(), 20.0 - 600.0 / 11.0, epsilon = 1e-12);
134 }
135
136 #[test]
140 fn accessors_and_metadata() {
141 let mut vpt = VolumePriceTrend::new();
142 assert_eq!(vpt.name(), "VPT");
143 assert_eq!(vpt.value(), None);
144 vpt.update(candle(100.0, 50.0, 0));
145 assert_eq!(vpt.value(), Some(0.0));
146 }
147
148 #[test]
152 fn zero_previous_close_contributes_zero() {
153 let mut vpt = VolumePriceTrend::new();
154 vpt.update(candle(0.0, 100.0, 0)); let v = vpt.update(candle(50.0, 200.0, 1)).expect("emits");
156 assert_eq!(v, 0.0);
158 }
159
160 #[test]
161 fn emits_from_first_candle_at_zero() {
162 let mut vpt = VolumePriceTrend::new();
163 assert_eq!(vpt.warmup_period(), 1);
164 assert_eq!(vpt.update(candle(100.0, 50.0, 0)), Some(0.0));
165 }
166
167 #[test]
168 fn constant_close_keeps_line_flat() {
169 let mut vpt = VolumePriceTrend::new();
171 let candles: Vec<Candle> = (0..20).map(|i| candle(100.0, 500.0, i)).collect();
172 for v in vpt.batch(&candles).into_iter().flatten() {
173 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
174 }
175 }
176
177 #[test]
178 fn reset_clears_state() {
179 let mut vpt = VolumePriceTrend::new();
180 vpt.batch(&[
181 candle(10.0, 100.0, 0),
182 candle(11.0, 100.0, 1),
183 candle(12.0, 100.0, 2),
184 ]);
185 assert!(vpt.is_ready());
186 vpt.reset();
187 assert!(!vpt.is_ready());
188 assert_eq!(vpt.value(), None);
189 }
190
191 #[test]
192 fn batch_equals_streaming() {
193 let candles: Vec<Candle> = (0..60)
194 .map(|i| {
195 candle(
196 100.0 + (i as f64 * 0.3).sin() * 8.0,
197 10.0 + (i % 5) as f64,
198 i,
199 )
200 })
201 .collect();
202 let batch = VolumePriceTrend::new().batch(&candles);
203 let mut b = VolumePriceTrend::new();
204 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
205 assert_eq!(batch, streamed);
206 }
207}