Skip to main content

egui_charts/studies/builtin/
elder_impulse.rs

1use super::smoothing::Smoothing;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::Stepper;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Signal state for Elder Impulse
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum ImpulseSignal {
11    /// EMA rising, MACD histogram rising
12    Bullish,
13    /// EMA falling, MACD histogram falling
14    Bearish,
15    /// Mixed signals or an unchanged EMA or MACD histogram
16    Neutral,
17}
18
19/// Elder Impulse System
20/// Combines EMA slope and MACD histogram for trend signals
21/// Green = bullish, Red = bearish, Blue = neutral
22#[derive(Clone)]
23pub struct ElderImpulseSystem {
24    /// EMA period (typically 13)
25    ema_period: usize,
26    /// MACD fast period
27    macd_fast: usize,
28    /// MACD slow period
29    macd_slow: usize,
30    /// MACD signal period
31    macd_signal: usize,
32    values: IndicatorValues,
33    stepper: Stepper<(usize, [Smoothing; 4], [f64; 2])>,
34    /// Colors: [Bullish, Bearish, Neutral]
35    colors: Vec<Color32>,
36    visible: bool,
37}
38
39impl ElderImpulseSystem {
40    /// Create with `ema_period`, `macd_fast`, `macd_slow`, `macd_signal` in bars, each clamped
41    /// to at least one.
42    pub fn new(ema_period: usize, macd_fast: usize, macd_slow: usize, macd_signal: usize) -> Self {
43        Self {
44            ema_period: ema_period.max(1),
45            macd_fast: macd_fast.max(1),
46            macd_slow: macd_slow.max(1),
47            macd_signal: macd_signal.max(1),
48            values: IndicatorValues::new(),
49            stepper: Stepper::default(),
50            colors: vec![
51                palette::GREEN, // Green - Bullish
52                palette::RED,   // Red - Bearish
53                palette::BLUE,  // Blue - Neutral
54            ],
55            visible: true,
56        }
57    }
58
59    /// Set plot colors in the order returned by [`Indicator::line_names`].
60    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
61        self.colors = colors;
62        self
63    }
64}
65
66impl Default for ElderImpulseSystem {
67    fn default() -> Self {
68        Self::new(13, 12, 26, 9)
69    }
70}
71
72impl Indicator for ElderImpulseSystem {
73    fn source(&self) -> Option<crate::studies::StudySource> {
74        Some(crate::studies::StudySource::Price(
75            crate::model::PriceSource::Close,
76        ))
77    }
78
79    crate::studies::inputs::study_inputs!(self;
80        ema_period: length("EMA period", 1, usize::MAX),
81        macd_fast: length("MACD fast", 1, usize::MAX),
82        macd_slow: length("MACD slow", 1, usize::MAX),
83        macd_signal: length("MACD signal", 1, usize::MAX)
84    ; {});
85
86    fn name(&self) -> &str {
87        "Impulse"
88    }
89
90    fn desc(&self) -> &str {
91        "Elder Impulse System - Trend direction with momentum"
92    }
93
94    fn calculate(&mut self, data: &[Bar]) {
95        self.update(data, 0);
96    }
97
98    fn update(&mut self, data: &[Bar], from: usize) {
99        self.stepper
100            .run(data, from, &mut self.values, |state, bar| {
101                if !bar.close.is_finite() {
102                    return IndicatorValue::None;
103                }
104                state.0 += 1;
105                let ema = state.1[0].next(bar.close, 1, 2.0 / (self.ema_period as f64 + 1.0));
106                let fast = state.1[1].next(bar.close, 1, 2.0 / (self.macd_fast as f64 + 1.0));
107                let slow = state.1[2].next(bar.close, 1, 2.0 / (self.macd_slow as f64 + 1.0));
108                let macd = fast - slow;
109                let signal = state.1[3].next(macd, 1, 2.0 / (self.macd_signal as f64 + 1.0));
110                let histogram = macd - signal;
111                let previous = std::mem::replace(&mut state.2, [ema, histogram]);
112                let required = self
113                    .macd_slow
114                    .max(self.ema_period)
115                    .saturating_add(self.macd_signal);
116                if state.0 < required || state.0 <= 1 {
117                    return IndicatorValue::None;
118                }
119                let rising = ema > previous[0];
120                let hist_rising = histogram > previous[1];
121                IndicatorValue::Single(if rising && hist_rising {
122                    1.0
123                } else if ema < previous[0] && histogram < previous[1] {
124                    -1.0
125                } else {
126                    0.0
127                })
128            });
129    }
130
131    fn is_incremental(&self) -> bool {
132        true
133    }
134
135    fn values(&self) -> &IndicatorValues {
136        &self.values
137    }
138
139    fn colors(&self) -> Vec<Color32> {
140        self.colors.clone()
141    }
142
143    fn set_colors(&mut self, colors: Vec<Color32>) {
144        if colors.len() >= 3 {
145            self.colors = colors;
146        }
147    }
148
149    fn is_overlay(&self) -> bool {
150        false
151    }
152
153    fn is_visible(&self) -> bool {
154        self.visible
155    }
156
157    fn set_visible(&mut self, visible: bool) {
158        self.visible = visible;
159    }
160
161    fn clone_box(&self) -> Box<dyn Indicator> {
162        Box::new(self.clone())
163    }
164
165    fn line_names(&self) -> Vec<String> {
166        vec!["Impulse".to_string()]
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::studies::test_support::make_bar;
174
175    #[test]
176    fn test_elder_impulse() {
177        for direction in [0.0, 1.0, -1.0] {
178            let mut impulse = ElderImpulseSystem::new(5, 6, 12, 5);
179            // A constant has neither slope; accelerating rises/falls give both slopes that sign.
180            let data: Vec<Bar> = (0..40)
181                .map(|i| make_bar(100.0 + direction * (i * i) as f64))
182                .collect();
183            impulse.calculate(&data);
184            assert_eq!(impulse.values.len(), data.len());
185            for bar in 0..data.len() {
186                assert_eq!(
187                    impulse.values().value(0, bar),
188                    (bar >= 16).then_some(direction),
189                    "direction {direction}, bar {bar}"
190                );
191            }
192        }
193    }
194}