kestrel_chartkit/indicator/pvt.rs
1use crate::model::Bar;
2
3use super::{Indicator, IndicatorOutput};
4
5/// Price Volume Trend: volume weighted by the *relative* price change, accumulated.
6///
7/// `PVT_t = PVT_{t-1} + volume_t * (close_t / close_{t-1} - 1)`
8///
9/// The distinction to its neighbours is the weighting, and it is the whole point:
10/// - **OBV** adds the full volume with the sign of the change — a one-cent move counts as much as
11/// a five-percent one.
12/// - **Elder's Force Index** weights by the *absolute* change, so the same percentage move counts
13/// more at a higher price level.
14/// - PVT weights by the relative change, which makes its steps comparable across price levels
15/// within one series.
16///
17/// Unit: volume units. A cumulative total is only meaningful relative to itself — its level says
18/// nothing without the series it was accumulated over, and comparing the level between two
19/// instruments compares their volume conventions, not their flows.
20///
21/// The running total starts at zero on the first bar that has a predecessor. There is no output
22/// on the very first bar of a series: without a previous close there is no return, and a zero
23/// would claim one. [`Indicator::reset`] returns the total to zero, so a series switch starts a
24/// fresh accumulation rather than carrying a level across a boundary it has no meaning over.
25#[derive(Debug, Clone, Default)]
26pub struct PriceVolumeTrend {
27 prev_close: Option<f64>,
28 total: f64,
29}
30
31impl PriceVolumeTrend {
32 pub fn new() -> Self {
33 Self::default()
34 }
35}
36
37impl Indicator for PriceVolumeTrend {
38 fn name(&self) -> &str {
39 "pvt"
40 }
41
42 fn warmup_period(&self) -> usize {
43 1
44 }
45
46 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
47 let prev_close = match self.prev_close {
48 None => {
49 self.prev_close = Some(bar.close);
50 return None;
51 }
52 Some(prev) => prev,
53 };
54 self.prev_close = Some(bar.close);
55
56 if prev_close != 0.0 {
57 self.total += bar.volume * (bar.close / prev_close - 1.0);
58 }
59 Some(IndicatorOutput::new(self.total))
60 }
61
62 fn reset(&mut self) {
63 self.prev_close = None;
64 self.total = 0.0;
65 }
66}