egui_charts/studies/builtin/
elder_impulse.rs1use 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#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum ImpulseSignal {
11 Bullish,
13 Bearish,
15 Neutral,
17}
18
19#[derive(Clone)]
23pub struct ElderImpulseSystem {
24 ema_period: usize,
26 macd_fast: usize,
28 macd_slow: usize,
30 macd_signal: usize,
32 values: IndicatorValues,
33 stepper: Stepper<(usize, [Smoothing; 4], [f64; 2])>,
34 colors: Vec<Color32>,
36 visible: bool,
37}
38
39impl ElderImpulseSystem {
40 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, palette::RED, palette::BLUE, ],
55 visible: true,
56 }
57 }
58
59 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 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}