Skip to main content

egui_charts/studies/builtin/
price_oscillator.rs

1use super::smoothing::Smoothing;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::Stepper;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7use std::collections::VecDeque;
8
9/// Average formula of an oscillator's averages and signal line, and of the middle line of
10/// [`KeltnerChannels`](super::KeltnerChannels).
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum OscillatorAverage {
13    /// Alpha 2/(length+1). The Price and Percentage Volume Oscillators and Keltner's middle line
14    /// seed it with the mean of the first `length` values, MACD with the first value.
15    Exponential,
16    /// Arithmetic mean of the last `length` values.
17    Simple,
18}
19
20/// Resumable [`OscillatorAverage`] over the finite values it is fed.
21#[derive(Clone, Default)]
22pub(super) struct Mean {
23    exponential: Smoothing,
24    window: VecDeque<f64>,
25}
26
27impl Mean {
28    pub(super) fn next(&mut self, kind: OscillatorAverage, input: f64, length: usize) -> f64 {
29        match kind {
30            OscillatorAverage::Exponential => {
31                self.exponential
32                    .next(input, length, 2.0 / (length as f64 + 1.0))
33            }
34            OscillatorAverage::Simple => {
35                if !input.is_finite() {
36                    return f64::NAN;
37                }
38                self.window.push_back(input);
39                if self.window.len() > length {
40                    self.window.pop_front();
41                }
42                if self.window.len() < length {
43                    return f64::NAN;
44                }
45                self.window.iter().sum::<f64>() / length as f64
46            }
47        }
48    }
49}
50
51/// Percentage Price Oscillator (PPO): MACD taken as a percentage of the long average.
52///
53/// - PPO = 100 * (MA(close, short) - MA(close, long)) / MA(close, long).
54/// - Signal = MA(PPO, signal length), computed only over the bars where the PPO is defined.
55///   An exponential average is seeded with the mean of its first `signal` values, like the
56///   study's other averages, which are seeded with the mean of their first values; a simple
57///   one is the mean of the last `signal` values.
58/// - Histogram = PPO - signal.
59///
60/// MA is exponential by default; the oscillator's averages and the signal line each choose an
61/// [`OscillatorAverage`] independently. With lengths 12, 26 and 9 a bar has no value before
62/// bar 25, the PPO alone on bars 25 to 32, and from bar 33, the mean of the PPO on bars 25 to
63/// 33 being the first signal, the PPO, the signal and the histogram, under either formula. A
64/// bar with a non-finite close has no value and is skipped by the averages.
65#[derive(Clone)]
66pub struct PriceOscillator {
67    /// Short EMA period (typically 12)
68    short_period: usize,
69    /// Long EMA period (typically 26)
70    long_period: usize,
71    /// Signal line EMA period (typically 9)
72    signal_period: usize,
73    /// Formula of the short and long averages
74    oscillator_average: OscillatorAverage,
75    /// Formula of the signal line
76    signal_average: OscillatorAverage,
77    values: IndicatorValues,
78    stepper: Stepper<[Mean; 3]>,
79    /// Colors: [PPO line, Signal line, Histogram]
80    colors: Vec<Color32>,
81    visible: bool,
82}
83
84impl PriceOscillator {
85    /// Create with `short_period`, `long_period`, `signal_period` in bars, each clamped to at
86    /// least one.
87    pub fn new(short_period: usize, long_period: usize, signal_period: usize) -> Self {
88        Self {
89            short_period: short_period.max(1),
90            long_period: long_period.max(1),
91            signal_period: signal_period.max(1),
92            oscillator_average: OscillatorAverage::Exponential,
93            signal_average: OscillatorAverage::Exponential,
94            values: IndicatorValues::new(),
95            stepper: Stepper::default(),
96            colors: vec![
97                palette::BLUE,   // PPO
98                palette::ORANGE, // Signal
99                palette::HISTOGRAM_TEAL,
100                palette::LIGHT_TEAL,
101                palette::HISTOGRAM_RED,
102                palette::LIGHT_RED, // Histogram
103            ],
104            visible: true,
105        }
106    }
107
108    /// Choose the formula of the short and long averages and of the signal line.
109    pub fn with_averages(
110        mut self,
111        oscillator: OscillatorAverage,
112        signal: OscillatorAverage,
113    ) -> Self {
114        self.oscillator_average = oscillator;
115        self.signal_average = signal;
116        self
117    }
118
119    /// Set plot colors in the order returned by [`Indicator::line_names`].
120    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
121        self.set_colors(colors);
122        self
123    }
124}
125
126/// Resume the percentage difference, its signal over defined values, and histogram.
127pub(super) fn percentage_oscillator(
128    state: &mut [Mean; 3],
129    value: f64,
130    lengths: [usize; 3],
131    averages: [OscillatorAverage; 2],
132) -> IndicatorValue {
133    let [short, long, signal] = lengths;
134    let short_value = state[0].next(averages[0], value, short);
135    let long_value = state[1].next(averages[0], value, long);
136    let line = if long_value == 0.0 {
137        f64::NAN
138    } else {
139        (short_value - long_value) / long_value * 100.0
140    };
141    let signal = state[2].next(averages[1], line, signal);
142    if line.is_finite() {
143        IndicatorValue::Multiple(vec![line, signal, line - signal])
144    } else {
145        IndicatorValue::None
146    }
147}
148
149/// Construct with the conventional default parameters.
150impl Default for PriceOscillator {
151    fn default() -> Self {
152        Self::new(12, 26, 9)
153    }
154}
155
156impl Indicator for PriceOscillator {
157    fn source(&self) -> Option<crate::studies::StudySource> {
158        Some(crate::studies::StudySource::Price(
159            crate::model::PriceSource::Close,
160        ))
161    }
162
163    crate::studies::inputs::study_inputs!(self;
164        short_period: length("Short period", 1, usize::MAX),
165        long_period: length("Long period", 1, usize::MAX),
166        signal_period: length("Signal period", 1, usize::MAX),
167        oscillator_average: choice("Oscillator MA", [OscillatorAverage::Exponential, OscillatorAverage::Simple]),
168        signal_average: choice("Signal MA", [OscillatorAverage::Exponential, OscillatorAverage::Simple])
169    ; {});
170
171    fn plot_color(&self, line: usize, bar: usize) -> Option<Color32> {
172        if line != 2 {
173            return None;
174        }
175        super::macd::histogram_color(&self.values, bar, &self.colors[2..])
176    }
177
178    fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
179        crate::studies::PlotStyle {
180            color: self.colors.get(line).copied().unwrap_or(Color32::WHITE),
181            width: 1.0,
182            pattern: crate::model::LineStyle::Solid,
183            plot_type: if line == 2 {
184                crate::studies::PlotType::Columns
185            } else {
186                crate::studies::PlotType::Line
187            },
188            visible: true,
189        }
190    }
191
192    fn value_format(&self) -> crate::scales::StudyFormat {
193        crate::scales::StudyFormat::Percent
194    }
195
196    fn name(&self) -> &str {
197        "PPO"
198    }
199
200    fn desc(&self) -> &str {
201        "Percentage Price Oscillator - Percentage difference between EMAs"
202    }
203
204    fn calculate(&mut self, data: &[Bar]) {
205        self.update(data, 0);
206    }
207
208    fn update(&mut self, data: &[Bar], from: usize) {
209        self.stepper
210            .run(data, from, &mut self.values, |state, bar| {
211                if !bar.close.is_finite() {
212                    return IndicatorValue::None;
213                }
214                percentage_oscillator(
215                    state,
216                    bar.close,
217                    [self.short_period, self.long_period, self.signal_period],
218                    [self.oscillator_average, self.signal_average],
219                )
220            });
221    }
222
223    fn is_incremental(&self) -> bool {
224        true
225    }
226
227    fn values(&self) -> &IndicatorValues {
228        &self.values
229    }
230
231    fn colors(&self) -> Vec<Color32> {
232        self.colors.clone()
233    }
234
235    fn set_colors(&mut self, colors: Vec<Color32>) {
236        if colors.len() >= 3 {
237            self.colors = colors;
238            if self.colors.len() < 6 {
239                self.colors.truncate(3);
240            }
241            self.colors.resize(6, self.colors[2]);
242        } else {
243            for (stored, color) in self.colors.iter_mut().zip(colors) {
244                *stored = color;
245            }
246        }
247    }
248
249    fn set_color(&mut self, line: usize, color: Color32) {
250        match line {
251            0 | 1 => self.colors[line] = color,
252            2 => self.colors[2..].fill(color),
253            _ => {}
254        }
255    }
256
257    fn is_overlay(&self) -> bool {
258        false
259    }
260
261    fn is_visible(&self) -> bool {
262        self.visible
263    }
264
265    fn set_visible(&mut self, visible: bool) {
266        self.visible = visible;
267    }
268
269    fn clone_box(&self) -> Box<dyn Indicator> {
270        Box::new(self.clone())
271    }
272
273    fn line_count(&self) -> usize {
274        3
275    }
276
277    fn line_names(&self) -> Vec<String> {
278        vec![
279            format!("PPO({},{})", self.short_period, self.long_period),
280            format!("Signal({})", self.signal_period),
281            "Histogram".to_string(),
282        ]
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use crate::studies::test_support::make_bar;
290
291    /// Hand values for lengths 2, 3 and 2 on closes 10, 12, 11, 13, 14, for each pairing of the
292    /// oscillator and signal formulas. The averages of 2 are 11, 11.5, 12, 13.5 (simple) and
293    /// 11, 11, 37/3, 121/9 (exponential, alpha 2/3), those of 3 are 11, 12, 38/3 (simple) and
294    /// 11, 12, 13 (exponential, alpha 1/2). The signal's exponential seed is the mean of its
295    /// first two values, which is also its first simple value.
296    #[test]
297    fn each_average_pairing_matches_hand_values() {
298        use OscillatorAverage::{Exponential as E, Simple as S};
299        let pct = |short: f64, long: f64| 100.0 * (short - long) / long;
300        let exp_line = [
301            pct(11.0, 11.0),
302            pct(37.0 / 3.0, 12.0),
303            pct(121.0 / 9.0, 13.0),
304        ];
305        let sma_line = [pct(11.5, 11.0), pct(12.0, 12.0), pct(13.5, 38.0 / 3.0)];
306        let data: Vec<Bar> = [10.0, 12.0, 11.0, 13.0, 14.0]
307            .into_iter()
308            .map(make_bar)
309            .collect();
310        for (osc, signal, line) in [
311            (E, E, exp_line),
312            (E, S, exp_line),
313            (S, E, sma_line),
314            (S, S, sma_line),
315        ] {
316            let seed = (line[0] + line[1]) / 2.0;
317            let last = match signal {
318                E => seed + (line[2] - seed) * 2.0 / 3.0,
319                S => (line[1] + line[2]) / 2.0,
320            };
321            let mut ppo = PriceOscillator::new(2, 3, 2).with_averages(osc, signal);
322            ppo.calculate(&data);
323            for (bar, ppo_line, signal) in [(3, line[1], seed), (4, line[2], last)] {
324                let got: Vec<f64> = (0..3)
325                    .map(|l| ppo.values().value(l, bar).unwrap())
326                    .collect();
327                for (got, want) in got.iter().zip([ppo_line, signal, ppo_line - signal]) {
328                    assert!((got - want).abs() < 1e-9, "{osc:?}/{signal:?} bar {bar}");
329                }
330            }
331        }
332    }
333}