Skip to main content

egui_charts/studies/builtin/
price_momentum_oscillator.rs

1//! Price Momentum Oscillator (PMO), Carl Swenlin (DecisionPoint).
2//!
3//! A doubly smoothed one-bar rate of change, with an exponential signal line.
4//!
5//! # Formula
6//!
7//! ```text
8//! ROC1   = (close / previous close - 1) * 100
9//! csf(x, n): prev + (x - prev) * 2 / n
10//! PMO    = csf(10 * csf(ROC1, length 1), length 2)
11//! Signal = EMA(PMO, signal length)
12//! ```
13//!
14//! The custom smoothing uses the factor `2 / n` instead of the usual `2 / (n + 1)`, so its lengths
15//! are at least 2: at 1 the factor of 2 overshoots and the error grows. Each smoothing,
16//! the signal EMA included, starts at the mean of its first `n` inputs. The rate of change starts on
17//! bar 1, so the PMO starts on bar `length 1 + length 2 - 1` and the signal `signal length - 1` bars
18//! later. A bar following a zero close has no rate of change, and the smoothing carries over it.
19//! Defaults: 35, 20 and 10.
20//!
21//! # Example
22//! ```
23//! # let bars: Vec<egui_charts::model::Bar> = (0..80).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
24//! use egui_charts::studies::{Indicator, PriceMomentumOscillator};
25//!
26//! let mut pmo = PriceMomentumOscillator::default();
27//! pmo.calculate(&bars);
28//! assert!(pmo.values().value(0, 54).is_some());
29//! assert!(pmo.values().value(1, 63).is_some());
30//! ```
31
32use crate::model::Bar;
33use crate::studies::palette;
34use crate::studies::stepper::Stepper;
35use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
36use egui::Color32;
37
38/// Price Momentum Oscillator: PMO and signal lines in a separate pane.
39#[derive(Clone)]
40pub struct PriceMomentumOscillator {
41    first_length: usize,
42    second_length: usize,
43    signal_length: usize,
44    values: IndicatorValues,
45    stepper: Stepper<Option<State>>,
46    /// Colors: [PMO, Signal]
47    colors: Vec<Color32>,
48    visible: bool,
49}
50
51#[derive(Clone)]
52struct State {
53    previous: Option<f64>,
54    first: Smoothing,
55    second: Smoothing,
56    signal: Smoothing,
57}
58
59/// Recursive smoothing `prev + (x - prev) * alpha`, starting at the mean of the first `length` inputs.
60#[derive(Clone)]
61struct Smoothing {
62    length: usize,
63    alpha: f64,
64    seen: usize,
65    value: f64,
66}
67
68impl Smoothing {
69    fn new(length: usize, alpha: f64) -> Self {
70        Self {
71            length,
72            alpha,
73            seen: 0,
74            value: 0.0,
75        }
76    }
77
78    fn push(&mut self, x: f64) -> Option<f64> {
79        if self.seen < self.length {
80            self.seen += 1;
81            self.value += (x - self.value) / self.seen as f64;
82            return (self.seen == self.length).then_some(self.value);
83        }
84        self.value += (x - self.value) * self.alpha;
85        Some(self.value)
86    }
87}
88
89impl PriceMomentumOscillator {
90    /// Create a PMO with the two smoothing lengths and the signal length (35, 20 and 10 by convention).
91    pub fn new(first_length: usize, second_length: usize, signal_length: usize) -> Self {
92        Self {
93            first_length: first_length.max(2),
94            second_length: second_length.max(2),
95            signal_length: signal_length.max(1),
96            values: IndicatorValues::new(),
97            stepper: Stepper::default(),
98            colors: vec![palette::BLUE, palette::ORANGE],
99            visible: true,
100        }
101    }
102}
103
104impl Default for PriceMomentumOscillator {
105    fn default() -> Self {
106        Self::new(35, 20, 10)
107    }
108}
109
110impl Indicator for PriceMomentumOscillator {
111    fn source(&self) -> Option<crate::studies::StudySource> {
112        Some(crate::studies::StudySource::Price(
113            crate::model::PriceSource::Close,
114        ))
115    }
116
117    crate::studies::inputs::study_inputs!(self;
118        first_length: length("Length 1", 2, usize::MAX),
119        second_length: length("Length 2", 2, usize::MAX),
120        signal_length: length("Signal length", 1, usize::MAX)
121    ; {});
122
123    fn name(&self) -> &str {
124        "Price Momentum Oscillator"
125    }
126
127    fn desc(&self) -> &str {
128        "Price Momentum Oscillator - Doubly smoothed rate of change with a signal line"
129    }
130
131    fn calculate(&mut self, data: &[Bar]) {
132        self.update(data, 0);
133    }
134
135    fn update(&mut self, data: &[Bar], from: usize) {
136        self.stepper
137            .run(data, from, &mut self.values, |state, bar| {
138                if !bar.close.is_finite() {
139                    return IndicatorValue::None;
140                }
141                let state = state.get_or_insert_with(|| State {
142                    previous: None,
143                    first: Smoothing::new(self.first_length, 2.0 / self.first_length as f64),
144                    second: Smoothing::new(self.second_length, 2.0 / self.second_length as f64),
145                    signal: Smoothing::new(
146                        self.signal_length,
147                        2.0 / (self.signal_length as f64 + 1.0),
148                    ),
149                });
150                let roc = state
151                    .previous
152                    .map(|previous| (bar.close / previous - 1.0) * 100.0);
153                state.previous = Some(bar.close);
154                let pmo = roc
155                    .filter(|roc| roc.is_finite())
156                    .and_then(|roc| state.first.push(roc))
157                    .and_then(|smoothed| state.second.push(10.0 * smoothed));
158                match pmo {
159                    Some(pmo) => IndicatorValue::Multiple(vec![
160                        pmo,
161                        state.signal.push(pmo).unwrap_or(f64::NAN),
162                    ]),
163                    None => IndicatorValue::None,
164                }
165            });
166    }
167
168    fn is_incremental(&self) -> bool {
169        true
170    }
171
172    fn values(&self) -> &IndicatorValues {
173        &self.values
174    }
175
176    fn colors(&self) -> Vec<Color32> {
177        self.colors.clone()
178    }
179
180    fn set_colors(&mut self, colors: Vec<Color32>) {
181        if colors.len() >= 2 {
182            self.colors = colors;
183        }
184    }
185
186    fn pane(&self) -> crate::studies::StudyPane {
187        crate::studies::StudyPane {
188            height: None,
189            range: None,
190            levels: vec![(0.0, palette::GRAY, "Zero Line")],
191            fill_between_levels: false,
192        }
193    }
194
195    fn is_overlay(&self) -> bool {
196        false
197    }
198
199    fn is_visible(&self) -> bool {
200        self.visible
201    }
202
203    fn set_visible(&mut self, visible: bool) {
204        self.visible = visible;
205    }
206
207    fn clone_box(&self) -> Box<dyn Indicator> {
208        Box::new(self.clone())
209    }
210
211    fn line_count(&self) -> usize {
212        2
213    }
214
215    fn line_names(&self) -> Vec<String> {
216        vec![
217            format!("PMO({},{})", self.first_length, self.second_length),
218            format!("Signal({})", self.signal_length),
219        ]
220    }
221}