Skip to main content

egui_charts/studies/builtin/
momentum.rs

1//! Momentum (MOM) Indicator
2//!
3//! Momentum measures the price change between the current close and the close
4//! N periods ago, in price units. Unlike Rate of Change it is not normalised
5//! to a percentage, so its scale follows the instrument's price.
6//!
7//! # Calculation
8//! MOM = Close - Close[n]
9
10use crate::model::Bar;
11use crate::studies::palette;
12use crate::studies::stepper::update_window;
13use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
14use egui::Color32;
15
16/// Momentum indicator
17#[derive(Clone)]
18pub struct Momentum {
19    period: usize,
20    values: IndicatorValues,
21    color: Color32,
22    visible: bool,
23}
24
25impl Momentum {
26    /// Create a new Momentum indicator
27    ///
28    /// # Arguments
29    /// * `period` - Lookback period (typically 10)
30    pub fn new(period: usize) -> Self {
31        Self {
32            period: period.max(1),
33            values: IndicatorValues::new(),
34            color: palette::BLUE,
35            visible: true,
36        }
37    }
38}
39
40/// Construct with the conventional default parameters.
41impl Default for Momentum {
42    fn default() -> Self {
43        Self::new(10)
44    }
45}
46
47impl Momentum {
48    fn compute(&self, data: &[Bar]) -> IndicatorValues {
49        (0..data.len())
50            .map(|i| match i.checked_sub(self.period) {
51                Some(prev) => IndicatorValue::Single(data[i].close - data[prev].close),
52                None => IndicatorValue::None,
53            })
54            .collect()
55    }
56}
57
58impl Indicator for Momentum {
59    fn source(&self) -> Option<crate::studies::StudySource> {
60        Some(crate::studies::StudySource::Price(
61            crate::model::PriceSource::Close,
62        ))
63    }
64
65    crate::studies::inputs::study_inputs!(self;
66        period: length("Period", 1, usize::MAX)
67    ; {});
68
69    fn value_format(&self) -> crate::scales::StudyFormat {
70        crate::scales::StudyFormat::Price
71    }
72
73    fn name(&self) -> &str {
74        "Momentum"
75    }
76
77    fn desc(&self) -> &str {
78        "Momentum - Price change over the period"
79    }
80
81    fn calculate(&mut self, data: &[Bar]) {
82        self.update(data, 0);
83    }
84
85    fn update(&mut self, data: &[Bar], from: usize) {
86        let mut values = std::mem::take(&mut self.values);
87        update_window(
88            &mut values,
89            data,
90            from,
91            |_data, from| from.saturating_sub(self.period),
92            |data| self.compute(data),
93        );
94        self.values = values;
95    }
96
97    fn is_incremental(&self) -> bool {
98        true
99    }
100
101    fn values(&self) -> &IndicatorValues {
102        &self.values
103    }
104
105    fn colors(&self) -> Vec<Color32> {
106        vec![self.color]
107    }
108
109    fn set_colors(&mut self, colors: Vec<Color32>) {
110        if let Some(&color) = colors.first() {
111            self.color = color;
112        }
113    }
114
115    fn is_overlay(&self) -> bool {
116        false
117    }
118
119    fn is_visible(&self) -> bool {
120        self.visible
121    }
122
123    fn set_visible(&mut self, visible: bool) {
124        self.visible = visible;
125    }
126
127    fn clone_box(&self) -> Box<dyn Indicator> {
128        Box::new(self.clone())
129    }
130
131    fn line_names(&self) -> Vec<String> {
132        vec![format!("MOM({})", self.period)]
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use chrono::{Duration, Utc};
140
141    #[test]
142    fn matches_reference_values() {
143        let closes = [10.0, 10.5, 11.25, 10.75, 12.0, 11.5, 13.25, 12.5];
144        let start = Utc::now();
145        let bars: Vec<Bar> = closes
146            .iter()
147            .enumerate()
148            .map(|(i, &c)| Bar {
149                time: start + Duration::minutes(i as i64),
150                open: c,
151                high: c,
152                low: c,
153                close: c,
154                volume: 1.0,
155                colors: None,
156                payload: None,
157            })
158            .collect();
159
160        let mut momentum = Momentum::new(3);
161        momentum.calculate(&bars);
162
163        // close[i] - close[i - 3], computed independently.
164        let expected = [
165            None,
166            None,
167            None,
168            Some(0.75),
169            Some(1.5),
170            Some(0.25),
171            Some(2.5),
172            Some(0.5),
173        ];
174        let actual: Vec<Option<f64>> = momentum
175            .values()
176            .to_rows()
177            .iter()
178            .map(|v| match v {
179                IndicatorValue::Single(x) => Some(*x),
180                _ => None,
181            })
182            .collect();
183        assert_eq!(actual, expected);
184    }
185}