Skip to main content

egui_charts/studies/builtin/
coppock.rs

1use crate::model::Bar;
2use crate::studies::helpers;
3use crate::studies::palette;
4use crate::studies::stepper::update_window;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Coppock Curve
9/// A momentum indicator originally designed to identify buying opportunities in stock indices
10/// Uses a weighted moving avg of the sum of two ROC periods
11#[derive(Clone)]
12pub struct CoppockCurve {
13    /// Long ROC period (typically 14)
14    long_roc: usize,
15    /// Short ROC period (typically 11)
16    short_roc: usize,
17    /// WMA period (typically 10)
18    wma_period: usize,
19    values: IndicatorValues,
20    color: Color32,
21    visible: bool,
22}
23
24impl CoppockCurve {
25    /// Create with rate-of-change periods 14 and 11 and weighted-average period 10.
26    pub fn new() -> Self {
27        Self {
28            long_roc: 14,
29            short_roc: 11,
30            wma_period: 10,
31            values: IndicatorValues::new(),
32            color: palette::LIGHT_BLUE,
33            visible: true,
34        }
35    }
36
37    /// Set the long and short rate-of-change and weighted-average periods, each at least one.
38    pub fn with_periods(mut self, long_roc: usize, short_roc: usize, wma: usize) -> Self {
39        self.long_roc = long_roc.max(1);
40        self.short_roc = short_roc.max(1);
41        self.wma_period = wma.max(1);
42        self
43    }
44
45    /// Set the plot color.
46    pub fn with_color(mut self, color: Color32) -> Self {
47        self.color = color;
48        self
49    }
50}
51
52impl Default for CoppockCurve {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl CoppockCurve {
59    fn compute(&self, data: &[Bar]) -> IndicatorValues {
60        let mut values = IndicatorValues::new();
61
62        let required = self
63            .long_roc
64            .max(self.short_roc)
65            .saturating_add(self.wma_period);
66        if data.len() < required {
67            for _ in 0..data.len() {
68                values.push(IndicatorValue::None);
69            }
70            return values;
71        }
72
73        // Get close prices
74        let closes: Vec<f64> = data.iter().map(|b| b.close).collect();
75
76        // Calculate both ROCs
77        let long_roc = helpers::calculate_roc(&closes, self.long_roc);
78        let short_roc = helpers::calculate_roc(&closes, self.short_roc);
79
80        // Sum of ROCs
81        let roc_sum: Vec<f64> = long_roc
82            .iter()
83            .zip(short_roc.iter())
84            .map(|(l, s)| {
85                if l.is_nan() || s.is_nan() {
86                    f64::NAN
87                } else {
88                    l + s
89                }
90            })
91            .collect();
92
93        // Apply WMA to the sum
94        let coppock = helpers::wma(&roc_sum, self.wma_period);
95
96        // Store results
97        for value in coppock {
98            if value.is_nan() {
99                values.push(IndicatorValue::None);
100            } else {
101                values.push(IndicatorValue::Single(value));
102            }
103        }
104        values
105    }
106}
107
108impl Indicator for CoppockCurve {
109    fn source(&self) -> Option<crate::studies::StudySource> {
110        Some(crate::studies::StudySource::Price(
111            crate::model::PriceSource::Close,
112        ))
113    }
114
115    crate::studies::inputs::study_inputs!(self;
116        long_roc: length("Long roc", 1, usize::MAX),
117        short_roc: length("Short roc", 1, usize::MAX),
118        wma_period: length("Wma period", 1, usize::MAX)
119    ; {});
120
121    fn name(&self) -> &str {
122        "Coppock"
123    }
124
125    fn desc(&self) -> &str {
126        "Coppock Curve - Long-term momentum indicator"
127    }
128
129    fn calculate(&mut self, data: &[Bar]) {
130        self.update(data, 0);
131    }
132
133    fn update(&mut self, data: &[Bar], from: usize) {
134        let mut values = std::mem::take(&mut self.values);
135        update_window(
136            &mut values,
137            data,
138            from,
139            |_data, from| {
140                from.saturating_sub(
141                    self.long_roc
142                        .max(self.short_roc)
143                        .saturating_add(self.wma_period - 1),
144                )
145            },
146            |data| self.compute(data),
147        );
148        self.values = values;
149    }
150
151    fn is_incremental(&self) -> bool {
152        true
153    }
154
155    fn values(&self) -> &IndicatorValues {
156        &self.values
157    }
158
159    fn colors(&self) -> Vec<Color32> {
160        vec![self.color]
161    }
162
163    fn set_colors(&mut self, colors: Vec<Color32>) {
164        if !colors.is_empty() {
165            self.color = colors[0];
166        }
167    }
168
169    fn is_overlay(&self) -> bool {
170        false // Separate pane
171    }
172
173    fn is_visible(&self) -> bool {
174        self.visible
175    }
176
177    fn set_visible(&mut self, visible: bool) {
178        self.visible = visible;
179    }
180
181    fn clone_box(&self) -> Box<dyn Indicator> {
182        Box::new(self.clone())
183    }
184
185    fn line_names(&self) -> Vec<String> {
186        vec![format!(
187            "Coppock({},{},{})",
188            self.long_roc, self.short_roc, self.wma_period
189        )]
190    }
191}