Skip to main content

egui_charts/studies/builtin/
pretty_good_oscillator.rs

1use super::over_finite_bars;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::{update_window, usable_behind};
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Pretty Good Oscillator (PGO)
9/// Measures price distance from moving avg in ATR units
10/// Values > 3 or < -3 indicate potential reversal
11#[derive(Clone)]
12pub struct PrettyGoodOscillator {
13    /// Period for calculations (typically 14)
14    period: usize,
15    values: IndicatorValues,
16    color: Color32,
17    visible: bool,
18}
19
20impl PrettyGoodOscillator {
21    /// Create with a lookback of `period` bars, clamped to at least 1.
22    pub fn new(period: usize) -> Self {
23        Self {
24            period: period.max(1),
25            values: IndicatorValues::new(),
26            color: palette::PURPLE,
27            visible: true,
28        }
29    }
30
31    /// Set the plot color.
32    pub fn with_color(mut self, color: Color32) -> Self {
33        self.color = color;
34        self
35    }
36}
37
38impl Default for PrettyGoodOscillator {
39    fn default() -> Self {
40        Self::new(14)
41    }
42}
43
44impl PrettyGoodOscillator {
45    /// The values over `data`, every bar of which has finite inputs.
46    fn compute(&self, data: &[Bar]) -> IndicatorValues {
47        let mut values = IndicatorValues::new();
48        if data.len() < self.period {
49            for _ in 0..data.len() {
50                values.push(IndicatorValue::None);
51            }
52            return values;
53        }
54
55        // Calculate True Range
56        let mut tr = Vec::with_capacity(data.len());
57        tr.push(data[0].high - data[0].low);
58
59        for i in 1..data.len() {
60            let high_low = data[i].high - data[i].low;
61            let high_close = (data[i].high - data[i - 1].close).abs();
62            let low_close = (data[i].low - data[i - 1].close).abs();
63            tr.push(high_low.max(high_close).max(low_close));
64        }
65
66        // Calculate SMA of closes and EMA of TR (ATR)
67        for i in 0..data.len() {
68            if i < self.period - 1 {
69                values.push(IndicatorValue::None);
70            } else {
71                // SMA of closes
72                let sma: f64 = data[i + 1 - self.period..=i]
73                    .iter()
74                    .map(|b| b.close)
75                    .sum::<f64>()
76                    / self.period as f64;
77
78                // SMA of TR (ATR)
79                let atr: f64 = tr[i + 1 - self.period..=i].iter().sum::<f64>() / self.period as f64;
80
81                if atr == 0.0 {
82                    values.push(IndicatorValue::None);
83                } else {
84                    // PGO = (Close - SMA) / ATR
85                    let pgo = (data[i].close - sma) / atr;
86                    values.push(IndicatorValue::Single(pgo));
87                }
88            }
89        }
90
91        values
92    }
93}
94
95impl Indicator for PrettyGoodOscillator {
96    crate::studies::inputs::study_inputs!(self;
97        period: length("Period", 1, usize::MAX)
98    ; {});
99
100    fn name(&self) -> &str {
101        "PGO"
102    }
103
104    fn desc(&self) -> &str {
105        "Pretty Good Oscillator - Price distance in ATR units"
106    }
107
108    fn calculate(&mut self, data: &[Bar]) {
109        self.update(data, 0);
110    }
111
112    fn update(&mut self, data: &[Bar], from: usize) {
113        let mut values = std::mem::take(&mut self.values);
114        update_window(
115            &mut values,
116            data,
117            from,
118            usable_behind(self.period, |b| {
119                b.high.is_finite() && b.low.is_finite() && b.close.is_finite()
120            }),
121            |data| {
122                over_finite_bars(
123                    data,
124                    |b| b.high.is_finite() && b.low.is_finite() && b.close.is_finite(),
125                    |data| self.compute(data),
126                )
127            },
128        );
129        self.values = values;
130    }
131
132    fn is_incremental(&self) -> bool {
133        true
134    }
135
136    fn values(&self) -> &IndicatorValues {
137        &self.values
138    }
139
140    fn colors(&self) -> Vec<Color32> {
141        vec![self.color]
142    }
143
144    fn set_colors(&mut self, colors: Vec<Color32>) {
145        if !colors.is_empty() {
146            self.color = colors[0];
147        }
148    }
149
150    fn is_overlay(&self) -> bool {
151        false
152    }
153
154    fn is_visible(&self) -> bool {
155        self.visible
156    }
157
158    fn set_visible(&mut self, visible: bool) {
159        self.visible = visible;
160    }
161
162    fn clone_box(&self) -> Box<dyn Indicator> {
163        Box::new(self.clone())
164    }
165
166    fn line_names(&self) -> Vec<String> {
167        vec![format!("PGO({})", self.period)]
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::studies::test_support::make_range_bar as make_bar;
175
176    #[test]
177    fn test_pgo_calculation() {
178        let mut pgo = PrettyGoodOscillator::new(5);
179
180        let data = vec![
181            make_bar(102.0, 98.0, 100.0),
182            make_bar(103.0, 99.0, 101.0),
183            make_bar(104.0, 100.0, 102.0),
184            make_bar(105.0, 101.0, 103.0),
185            make_bar(106.0, 102.0, 104.0),
186            make_bar(110.0, 105.0, 109.0), // Big move up
187        ];
188
189        pgo.calculate(&data);
190
191        assert_eq!(pgo.values.len(), 6);
192
193        // Last value should be positive (price above avg)
194        let rows = pgo.values.to_rows();
195        let IndicatorValue::Single(v) = rows.last().unwrap() else {
196            panic!("no value: {:?}", pgo.values.to_rows().last().unwrap());
197        };
198        assert!(*v > 0.0, "PGO should be positive after upward move");
199    }
200}