egui_charts/studies/builtin/
pretty_good_oscillator.rs1use 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#[derive(Clone)]
12pub struct PrettyGoodOscillator {
13 period: usize,
15 values: IndicatorValues,
16 color: Color32,
17 visible: bool,
18}
19
20impl PrettyGoodOscillator {
21 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 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 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 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 for i in 0..data.len() {
68 if i < self.period - 1 {
69 values.push(IndicatorValue::None);
70 } else {
71 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 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 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), ];
188
189 pgo.calculate(&data);
190
191 assert_eq!(pgo.values.len(), 6);
192
193 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}