Skip to main content

egui_charts/studies/builtin/
center_of_gravity.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/// Center of Gravity Oscillator
8/// Developed by John Ehlers
9/// Uses a weighted moving avg to identify reversal points
10#[derive(Clone)]
11pub struct CenterOfGravity {
12    /// Lookback period (typically 10)
13    period: usize,
14    values: IndicatorValues,
15    /// Colors: [COG, Signal]
16    colors: Vec<Color32>,
17    visible: bool,
18}
19
20impl CenterOfGravity {
21    /// Create with a lookback of `period` bars, clamped to at least 1.
22    pub fn new(period: usize) -> Self {
23        Self {
24            period: period.max(1),
25            values: IndicatorValues::new(),
26            colors: vec![
27                palette::UP,     // Teal - COG
28                palette::ORANGE, // Signal
29            ],
30            visible: true,
31        }
32    }
33
34    /// Set plot colors in the order returned by [`Indicator::line_names`].
35    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
36        self.colors = colors;
37        self
38    }
39}
40
41impl Default for CenterOfGravity {
42    fn default() -> Self {
43        Self::new(10)
44    }
45}
46
47impl CenterOfGravity {
48    fn compute(&self, data: &[Bar]) -> IndicatorValues {
49        let mut values = IndicatorValues::new();
50
51        if data.len() < self.period {
52            for _ in 0..data.len() {
53                values.push(IndicatorValue::None);
54            }
55            return values;
56        }
57
58        // Get closes
59        let closes: Vec<f64> = data.iter().map(|b| b.close).collect();
60
61        let mut cog_values = Vec::with_capacity(data.len());
62        let mut prev_cog = 0.0;
63
64        for i in 0..data.len() {
65            if i < self.period - 1 {
66                cog_values.push((f64::NAN, f64::NAN));
67                continue;
68            }
69
70            let window = &closes[i + 1 - self.period..=i];
71
72            // Calculate numerator: sum of (price * (period - j + 1))
73            let mut num = 0.0;
74            let mut den = 0.0;
75
76            for (j, &price) in window.iter().enumerate() {
77                let weight = (self.period - j) as f64;
78                num += weight * price;
79                den += price;
80            }
81
82            let cog = if den != 0.0 { -num / den } else { 0.0 };
83            // The first value has no previous one to signal on.
84            let signal = if i + 1 == self.period {
85                f64::NAN
86            } else {
87                prev_cog
88            };
89
90            prev_cog = cog;
91            cog_values.push((cog, signal));
92        }
93
94        // Store results
95        for (cog, signal) in &cog_values {
96            if cog.is_nan() {
97                values.push(IndicatorValue::None);
98            } else {
99                values.push_lines(&[*cog, *signal]);
100            }
101        }
102        values
103    }
104}
105
106impl Indicator for CenterOfGravity {
107    fn source(&self) -> Option<crate::studies::StudySource> {
108        Some(crate::studies::StudySource::Price(
109            crate::model::PriceSource::Close,
110        ))
111    }
112
113    crate::studies::inputs::study_inputs!(self;
114        period: length("Period", 1, usize::MAX)
115    ; {});
116
117    fn name(&self) -> &str {
118        "COG"
119    }
120
121    fn desc(&self) -> &str {
122        "Center of Gravity - Identifies price reversal points"
123    }
124
125    fn calculate(&mut self, data: &[Bar]) {
126        self.update(data, 0);
127    }
128
129    fn update(&mut self, data: &[Bar], from: usize) {
130        let mut values = std::mem::take(&mut self.values);
131        update_window(
132            &mut values,
133            data,
134            from,
135            |_data, from| from.saturating_sub(self.period),
136            |data| self.compute(data),
137        );
138        self.values = values;
139    }
140
141    fn is_incremental(&self) -> bool {
142        true
143    }
144
145    fn values(&self) -> &IndicatorValues {
146        &self.values
147    }
148
149    fn colors(&self) -> Vec<Color32> {
150        self.colors.clone()
151    }
152
153    fn set_colors(&mut self, colors: Vec<Color32>) {
154        if colors.len() >= 2 {
155            self.colors = colors;
156        }
157    }
158
159    fn is_overlay(&self) -> bool {
160        false
161    }
162
163    fn is_visible(&self) -> bool {
164        self.visible
165    }
166
167    fn set_visible(&mut self, visible: bool) {
168        self.visible = visible;
169    }
170
171    fn clone_box(&self) -> Box<dyn Indicator> {
172        Box::new(self.clone())
173    }
174
175    fn line_names(&self) -> Vec<String> {
176        vec![format!("COG({})", self.period), "Signal".to_string()]
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::studies::test_support::make_bar;
184
185    #[test]
186    fn test_cog_calculation() {
187        let mut cog = CenterOfGravity::new(5);
188
189        let data = vec![
190            make_bar(100.0),
191            make_bar(102.0),
192            make_bar(104.0),
193            make_bar(103.0),
194            make_bar(105.0),
195            make_bar(107.0),
196            make_bar(106.0),
197        ];
198
199        cog.calculate(&data);
200
201        assert_eq!(cog.values.len(), 7);
202
203        // Should have valid values after period
204        let valid_cnt = cog
205            .values
206            .to_rows()
207            .iter()
208            .filter(|v| !matches!(v, IndicatorValue::None))
209            .count();
210
211        assert!(valid_cnt > 0);
212    }
213}