Skip to main content

egui_charts/studies/builtin/
chande_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/// Chande Momentum Oscillator (CMO)
8/// Measures momentum on both up and down days
9/// Ranges from -100 to +100
10#[derive(Clone)]
11pub struct ChandeMomentumOscillator {
12    /// Period for calculation (typically 14 or 20)
13    period: usize,
14    values: IndicatorValues,
15    color: Color32,
16    visible: bool,
17}
18
19impl ChandeMomentumOscillator {
20    /// Create with a lookback of `period` bars, clamped to at least 1.
21    pub fn new(period: usize) -> Self {
22        Self {
23            period: period.max(1),
24            values: IndicatorValues::new(),
25            color: palette::BLUE,
26            visible: true,
27        }
28    }
29
30    /// Set the plot color.
31    pub fn with_color(mut self, color: Color32) -> Self {
32        self.color = color;
33        self
34    }
35}
36
37impl Default for ChandeMomentumOscillator {
38    fn default() -> Self {
39        Self::new(9)
40    }
41}
42
43impl ChandeMomentumOscillator {
44    fn compute(&self, data: &[Bar]) -> IndicatorValues {
45        let mut values = IndicatorValues::new();
46
47        if data.len() < self.period.saturating_add(1) {
48            for _ in 0..data.len() {
49                values.push(IndicatorValue::None);
50            }
51            return values;
52        }
53
54        // First value is None
55        values.push(IndicatorValue::None);
56
57        // Calculate daily changes
58        let mut gains = Vec::with_capacity(data.len() - 1);
59        let mut losses = Vec::with_capacity(data.len() - 1);
60
61        for i in 1..data.len() {
62            let change = data[i].close - data[i - 1].close;
63            if change > 0.0 {
64                gains.push(change);
65                losses.push(0.0);
66            } else {
67                gains.push(0.0);
68                losses.push((-change).abs());
69            }
70        }
71
72        for i in 0..gains.len() {
73            if i < self.period - 1 {
74                values.push(IndicatorValue::None);
75            } else {
76                let sum_up: f64 = gains[i + 1 - self.period..=i].iter().sum();
77                let sum_down: f64 = losses[i + 1 - self.period..=i].iter().sum();
78
79                let cmo = if sum_up + sum_down != 0.0 {
80                    100.0 * (sum_up - sum_down) / (sum_up + sum_down)
81                } else {
82                    0.0
83                };
84
85                values.push(IndicatorValue::Single(cmo));
86            }
87        }
88        values
89    }
90}
91
92impl Indicator for ChandeMomentumOscillator {
93    fn source(&self) -> Option<crate::studies::StudySource> {
94        Some(crate::studies::StudySource::Price(
95            crate::model::PriceSource::Close,
96        ))
97    }
98
99    crate::studies::inputs::study_inputs!(self;
100        period: length("Period", 1, usize::MAX)
101    ; {});
102
103    fn name(&self) -> &str {
104        "CMO"
105    }
106
107    fn desc(&self) -> &str {
108        "Chande Momentum Oscillator - Measures momentum"
109    }
110
111    fn calculate(&mut self, data: &[Bar]) {
112        self.update(data, 0);
113    }
114
115    fn update(&mut self, data: &[Bar], from: usize) {
116        let mut values = std::mem::take(&mut self.values);
117        update_window(
118            &mut values,
119            data,
120            from,
121            |_data, from| from.saturating_sub(self.period),
122            |data| self.compute(data),
123        );
124        self.values = values;
125    }
126
127    fn is_incremental(&self) -> bool {
128        true
129    }
130
131    fn values(&self) -> &IndicatorValues {
132        &self.values
133    }
134
135    fn colors(&self) -> Vec<Color32> {
136        vec![self.color]
137    }
138
139    fn set_colors(&mut self, colors: Vec<Color32>) {
140        if !colors.is_empty() {
141            self.color = colors[0];
142        }
143    }
144
145    fn is_overlay(&self) -> bool {
146        false
147    }
148
149    fn is_visible(&self) -> bool {
150        self.visible
151    }
152
153    fn set_visible(&mut self, visible: bool) {
154        self.visible = visible;
155    }
156
157    fn clone_box(&self) -> Box<dyn Indicator> {
158        Box::new(self.clone())
159    }
160
161    fn line_names(&self) -> Vec<String> {
162        vec![format!("CMO({})", self.period)]
163    }
164}