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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum OscillatorAverage {
13 Exponential,
16 Simple,
18}
19
20#[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#[derive(Clone)]
66pub struct PriceOscillator {
67 short_period: usize,
69 long_period: usize,
71 signal_period: usize,
73 oscillator_average: OscillatorAverage,
75 signal_average: OscillatorAverage,
77 values: IndicatorValues,
78 stepper: Stepper<[Mean; 3]>,
79 colors: Vec<Color32>,
81 visible: bool,
82}
83
84impl PriceOscillator {
85 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, palette::ORANGE, palette::HISTOGRAM_TEAL,
100 palette::LIGHT_TEAL,
101 palette::HISTOGRAM_RED,
102 palette::LIGHT_RED, ],
104 visible: true,
105 }
106 }
107
108 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 pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
121 self.set_colors(colors);
122 self
123 }
124}
125
126pub(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
149impl 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 #[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}