Skip to main content

egui_charts/studies/builtin/
price_volume_trend_study.rs

1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::stepper::Stepper;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7/// Price Volume Trend (Enhanced) with Signal Line
8/// PVT = prev_PVT + volume * (close - prev_close) / prev_close
9/// Signal = EMA(PVT, signal_period)
10#[derive(Clone)]
11pub struct PriceVolumeTrendStudy {
12    signal_period: usize,
13    values: IndicatorValues,
14    stepper: Stepper<(Option<f64>, f64, f64)>,
15    colors: Vec<Color32>,
16    visible: bool,
17}
18
19impl PriceVolumeTrendStudy {
20    /// Create with `signal_period` in bars, each clamped to at least one.
21    pub fn new(signal_period: usize) -> Self {
22        Self {
23            signal_period: signal_period.max(1),
24            values: IndicatorValues::new(),
25            stepper: Stepper::default(),
26            colors: vec![
27                palette::BLUE,   // PVT line
28                palette::ORANGE, // Signal line
29            ],
30            visible: true,
31        }
32    }
33}
34
35impl Default for PriceVolumeTrendStudy {
36    fn default() -> Self {
37        Self::new(9)
38    }
39}
40
41impl Indicator for PriceVolumeTrendStudy {
42    fn source(&self) -> Option<crate::studies::StudySource> {
43        Some(crate::studies::StudySource::Price(
44            crate::model::PriceSource::Close,
45        ))
46    }
47
48    crate::studies::inputs::study_inputs!(self;
49        signal_period: length("Signal period", 1, usize::MAX)
50    ; {});
51
52    fn value_format(&self) -> crate::scales::StudyFormat {
53        crate::scales::StudyFormat::Volume
54    }
55
56    fn name(&self) -> &str {
57        "PVT Enhanced"
58    }
59
60    fn desc(&self) -> &str {
61        "Price Volume Trend Enhanced - PVT with EMA signal line"
62    }
63
64    fn calculate(&mut self, data: &[Bar]) {
65        self.update(data, 0);
66    }
67
68    fn update(&mut self, data: &[Bar], from: usize) {
69        self.stepper
70            .run(data, from, &mut self.values, |state, bar| {
71                if !bar.close.is_finite() || !bar.volume.is_finite() {
72                    return IndicatorValue::None;
73                }
74                if let Some(previous) = state.0 {
75                    if previous != 0.0 {
76                        state.1 += bar.volume * (bar.close - previous) / previous;
77                    }
78                    state.2 =
79                        (state.1 - state.2) * (2.0 / (self.signal_period as f64 + 1.0)) + state.2;
80                }
81                state.0 = Some(bar.close);
82                IndicatorValue::Multiple(vec![state.1, state.2])
83            });
84    }
85
86    fn is_incremental(&self) -> bool {
87        true
88    }
89
90    fn values(&self) -> &IndicatorValues {
91        &self.values
92    }
93
94    fn colors(&self) -> Vec<Color32> {
95        self.colors.clone()
96    }
97
98    fn set_colors(&mut self, colors: Vec<Color32>) {
99        if colors.len() >= 2 {
100            self.colors = colors;
101        } else if !colors.is_empty() {
102            self.colors = vec![colors[0], colors[0]];
103        }
104    }
105
106    fn is_overlay(&self) -> bool {
107        false
108    }
109
110    fn line_count(&self) -> usize {
111        2
112    }
113
114    fn is_visible(&self) -> bool {
115        self.visible
116    }
117
118    fn set_visible(&mut self, visible: bool) {
119        self.visible = visible;
120    }
121
122    fn clone_box(&self) -> Box<dyn Indicator> {
123        Box::new(self.clone())
124    }
125
126    fn line_names(&self) -> Vec<String> {
127        vec![format!("PVT({})", self.signal_period), "Signal".to_string()]
128    }
129}