Skip to main content

egui_charts/studies/builtin/
ultimate_momentum.rs

1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::stepper::update_window;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7/// Ultimate Momentum Oscillator
8/// Custom composite momentum indicator
9/// Combines multiple timeframes for confirmation
10#[derive(Clone)]
11pub struct UltimateMomentum {
12    /// Short period (typically 5)
13    short_period: usize,
14    /// Medium period (typically 10)
15    medium_period: usize,
16    /// Long period (typically 20)
17    long_period: usize,
18    values: IndicatorValues,
19    color: Color32,
20    visible: bool,
21}
22
23impl UltimateMomentum {
24    /// Create with `short`, `medium`, `long` in bars, each clamped to at least one.
25    pub fn new(short: usize, medium: usize, long: usize) -> Self {
26        Self {
27            short_period: short.max(1),
28            medium_period: medium.max(1),
29            long_period: long.max(1),
30            values: IndicatorValues::new(),
31            color: palette::PURPLE,
32            visible: true,
33        }
34    }
35
36    /// Set the plot color.
37    pub fn with_color(mut self, color: Color32) -> Self {
38        self.color = color;
39        self
40    }
41
42    fn calculate_roc(closes: &[f64], period: usize, index: usize) -> Option<f64> {
43        if index < period || closes[index - period] == 0.0 {
44            None
45        } else {
46            Some((closes[index] - closes[index - period]) / closes[index - period] * 100.0)
47        }
48    }
49}
50
51impl Default for UltimateMomentum {
52    fn default() -> Self {
53        Self::new(5, 10, 20)
54    }
55}
56
57impl UltimateMomentum {
58    fn compute(&self, data: &[Bar]) -> IndicatorValues {
59        let mut values = IndicatorValues::new();
60
61        if data.len() < self.long_period {
62            for _ in 0..data.len() {
63                values.push(IndicatorValue::None);
64            }
65            return values;
66        }
67
68        // Get closes
69        let closes: Vec<f64> = data.iter().map(|b| b.close).collect();
70
71        for i in 0..data.len() {
72            let roc_short = Self::calculate_roc(&closes, self.short_period, i);
73            let roc_medium = Self::calculate_roc(&closes, self.medium_period, i);
74            let roc_long = Self::calculate_roc(&closes, self.long_period, i);
75
76            match (roc_short, roc_medium, roc_long) {
77                (Some(s), Some(m), Some(l)) => {
78                    // Weighted combination: more weight to longer periods
79                    let momentum = (s * 1.0 + m * 2.0 + l * 3.0) / 6.0;
80                    values.push(IndicatorValue::Single(momentum));
81                }
82                _ => {
83                    values.push(IndicatorValue::None);
84                }
85            }
86        }
87        values
88    }
89}
90
91impl Indicator for UltimateMomentum {
92    fn source(&self) -> Option<crate::studies::StudySource> {
93        Some(crate::studies::StudySource::Price(
94            crate::model::PriceSource::Close,
95        ))
96    }
97
98    crate::studies::inputs::study_inputs!(self;
99        short_period: length("Short period", 1, usize::MAX),
100        medium_period: length("Medium period", 1, usize::MAX),
101        long_period: length("Long period", 1, usize::MAX)
102    ; {});
103
104    fn name(&self) -> &str {
105        "UltMom"
106    }
107
108    fn desc(&self) -> &str {
109        "Ultimate Momentum - Multi-timeframe momentum"
110    }
111
112    fn calculate(&mut self, data: &[Bar]) {
113        self.update(data, 0);
114    }
115
116    fn update(&mut self, data: &[Bar], from: usize) {
117        let mut values = std::mem::take(&mut self.values);
118        update_window(
119            &mut values,
120            data,
121            from,
122            |_data, from| {
123                from.saturating_sub(
124                    self.short_period
125                        .max(self.medium_period)
126                        .max(self.long_period),
127                )
128            },
129            |data| self.compute(data),
130        );
131        self.values = values;
132    }
133
134    fn is_incremental(&self) -> bool {
135        true
136    }
137
138    fn values(&self) -> &IndicatorValues {
139        &self.values
140    }
141
142    fn colors(&self) -> Vec<Color32> {
143        vec![self.color]
144    }
145
146    fn set_colors(&mut self, colors: Vec<Color32>) {
147        if !colors.is_empty() {
148            self.color = colors[0];
149        }
150    }
151
152    fn is_overlay(&self) -> bool {
153        false
154    }
155
156    fn is_visible(&self) -> bool {
157        self.visible
158    }
159
160    fn set_visible(&mut self, visible: bool) {
161        self.visible = visible;
162    }
163
164    fn clone_box(&self) -> Box<dyn Indicator> {
165        Box::new(self.clone())
166    }
167
168    fn line_names(&self) -> Vec<String> {
169        vec![format!(
170            "UltMom({},{},{})",
171            self.short_period, self.medium_period, self.long_period
172        )]
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::studies::test_support::make_bar;
180
181    #[test]
182    fn test_ultimate_momentum() {
183        let mut um = UltimateMomentum::new(3, 5, 10);
184
185        let data: Vec<Bar> = (0..30).map(|i| make_bar(100.0 + i as f64)).collect();
186
187        um.calculate(&data);
188
189        assert_eq!(um.values.len(), 30);
190
191        // In uptrend, momentum should be positive
192        let rows = um.values.to_rows();
193        let IndicatorValue::Single(v) = rows.last().unwrap() else {
194            panic!("no value: {:?}", um.values.to_rows().last().unwrap());
195        };
196        assert!(*v > 0.0, "Momentum should be positive in uptrend");
197    }
198}