Skip to main content

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}