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 fn update(&mut self, candle: Candle) -> Option<f64> {
69 self.has_emitted = true;
70 let Some(prev) = self.prev_close else {
71 self.prev_close = Some(candle.close);
73 return Some(self.total);
74 };
75 let roc = if prev == 0.0 {
76 0.0
78 } else {
79 (candle.close - prev) / prev
80 };
81 self.total += candle.volume * roc;
82 self.prev_close = Some(candle.close);
83 Some(self.total)
84 }
85
86 fn reset(&mut self) {
87 self.prev_close = None;
88 self.total = 0.0;
89 self.has_emitted = false;
90 }
91
92 fn warmup_period(&self) -> usize {
93 1
94 }
95
96 fn is_ready(&self) -> bool {
97 self.has_emitted
98 }
99
100 fn name(&self) -> &'static str {
101 "VPT"
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::traits::BatchExt;
109 use approx::assert_relative_eq;
110
111 fn candle(close: f64, volume: f64, ts: i64) -> Candle {
112 Candle::new(close, close, close, close, volume, ts).unwrap()
113 }
114
115 #[test]
116 fn reference_values() {
117 let mut vpt = VolumePriceTrend::new();
122 let out = vpt.batch(&[
123 candle(10.0, 100.0, 0),
124 candle(11.0, 200.0, 1),
125 candle(9.0, 300.0, 2),
126 ]);
127 assert_relative_eq!(out[0].unwrap(), 0.0, epsilon = 1e-12);
128 assert_relative_eq!(out[1].unwrap(), 20.0, epsilon = 1e-12);
129 assert_relative_eq!(out[2].unwrap(), 20.0 - 600.0 / 11.0, epsilon = 1e-12);
130 }
131
132 #[test]
136 fn accessors_and_metadata() {
137 let mut vpt = VolumePriceTrend::new();
138 assert_eq!(vpt.name(), "VPT");
139 assert_eq!(vpt.value(), None);
140 vpt.update(candle(100.0, 50.0, 0));
141 assert_eq!(vpt.value(), Some(0.0));
142 }
143
144 #[test]
148 fn zero_previous_close_contributes_zero() {
149 let mut vpt = VolumePriceTrend::new();
150 vpt.update(candle(0.0, 100.0, 0)); let v = vpt.update(candle(50.0, 200.0, 1)).expect("emits");
152 assert_eq!(v, 0.0);
154 }
155
156 #[test]
157 fn emits_from_first_candle_at_zero() {
158 let mut vpt = VolumePriceTrend::new();
159 assert_eq!(vpt.warmup_period(), 1);
160 assert_eq!(vpt.update(candle(100.0, 50.0, 0)), Some(0.0));
161 }
162
163 #[test]
164 fn constant_close_keeps_line_flat() {
165 let mut vpt = VolumePriceTrend::new();
167 let candles: Vec<Candle> = (0..20).map(|i| candle(100.0, 500.0, i)).collect();
168 for v in vpt.batch(&candles).into_iter().flatten() {
169 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
170 }
171 }
172
173 #[test]
174 fn reset_clears_state() {
175 let mut vpt = VolumePriceTrend::new();
176 vpt.batch(&[
177 candle(10.0, 100.0, 0),
178 candle(11.0, 100.0, 1),
179 candle(12.0, 100.0, 2),
180 ]);
181 assert!(vpt.is_ready());
182 vpt.reset();
183 assert!(!vpt.is_ready());
184 assert_eq!(vpt.value(), None);
185 }
186
187 #[test]
188 fn batch_equals_streaming() {
189 let candles: Vec<Candle> = (0..60)
190 .map(|i| {
191 candle(
192 100.0 + (i as f64 * 0.3).sin() * 8.0,
193 10.0 + (i % 5) as f64,
194 i,
195 )
196 })
197 .collect();
198 let batch = VolumePriceTrend::new().batch(&candles);
199 let mut b = VolumePriceTrend::new();
200 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
201 assert_eq!(batch, streamed);
202 }
203}