wickra_core/indicators/
elder_impulse.rs1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::indicators::macd::MacdIndicator;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
34pub struct ElderImpulse {
35 ema_period: usize,
36 macd_fast: usize,
37 macd_slow: usize,
38 macd_signal: usize,
39 ema: Ema,
40 macd: MacdIndicator,
41 prev_ema: Option<f64>,
42 prev_hist: Option<f64>,
43 current: Option<f64>,
44}
45
46impl ElderImpulse {
47 pub fn new(
50 ema_period: usize,
51 macd_fast: usize,
52 macd_slow: usize,
53 macd_signal: usize,
54 ) -> Result<Self> {
55 if ema_period == 0 {
56 return Err(Error::PeriodZero);
57 }
58 if ema_period > crate::error::MAX_PERIOD {
59 return Err(Error::InvalidPeriod {
60 message: crate::error::PERIOD_ABOVE_MAX,
61 });
62 }
63 Ok(Self {
64 ema_period,
65 macd_fast,
66 macd_slow,
67 macd_signal,
68 ema: Ema::new(ema_period)?,
69 macd: MacdIndicator::new(macd_fast, macd_slow, macd_signal)?,
70 prev_ema: None,
71 prev_hist: None,
72 current: None,
73 })
74 }
75
76 pub fn classic() -> Self {
78 Self::new(13, 12, 26, 9).expect("classic Elder Impulse parameters are valid")
79 }
80
81 pub const fn periods(&self) -> (usize, usize, usize, usize) {
83 (
84 self.ema_period,
85 self.macd_fast,
86 self.macd_slow,
87 self.macd_signal,
88 )
89 }
90}
91
92impl Indicator for ElderImpulse {
93 type Input = f64;
94 type Output = f64;
95
96 #[inline]
97 fn update(&mut self, input: f64) -> Option<f64> {
98 let ema_now = self.ema.update(input);
100 let macd_now = self.macd.update(input);
101 let (ema_now, macd_now) = (ema_now?, macd_now?);
102
103 let prev_ema = self.prev_ema;
106 let prev_hist = self.prev_hist;
107 self.prev_ema = Some(ema_now);
108 self.prev_hist = Some(macd_now.histogram);
109 let prev_ema = prev_ema?;
110 let prev_hist = prev_hist?;
111
112 let ema_rising = ema_now > prev_ema;
113 let ema_falling = ema_now < prev_ema;
114 let hist_rising = macd_now.histogram > prev_hist;
115 let hist_falling = macd_now.histogram < prev_hist;
116
117 let value = if ema_rising && hist_rising {
118 1.0
119 } else if ema_falling && hist_falling {
120 -1.0
121 } else {
122 0.0
123 };
124 self.current = Some(value);
125 Some(value)
126 }
127
128 fn reset(&mut self) {
129 self.ema.reset();
130 self.macd.reset();
131 self.prev_ema = None;
132 self.prev_hist = None;
133 self.current = None;
134 }
135
136 #[inline]
137 fn warmup_period(&self) -> usize {
138 let macd_warmup = self.macd_slow + self.macd_signal - 1;
142 self.ema_period.max(macd_warmup) + 1
143 }
144
145 #[inline]
146 fn is_ready(&self) -> bool {
147 self.current.is_some()
148 }
149
150 #[inline]
151 fn name(&self) -> &'static str {
152 "ElderImpulse"
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::traits::BatchExt;
160
161 #[test]
162 fn rejects_zero_period() {
163 assert!(matches!(
164 ElderImpulse::new(0, 12, 26, 9),
165 Err(Error::PeriodZero)
166 ));
167 assert!(matches!(
168 ElderImpulse::new(13, 0, 26, 9),
169 Err(Error::PeriodZero)
170 ));
171 }
172
173 #[test]
174 fn rejects_invalid_macd_params() {
175 assert!(ElderImpulse::new(13, 26, 12, 9).is_err());
177 }
178
179 #[test]
180 fn accessors_and_metadata() {
181 let elder = ElderImpulse::classic();
182 assert_eq!(elder.periods(), (13, 12, 26, 9));
183 assert_eq!(elder.name(), "ElderImpulse");
184 }
185
186 #[test]
187 fn classic_factory() {
188 assert_eq!(ElderImpulse::classic().periods(), (13, 12, 26, 9));
189 }
190
191 #[test]
192 fn constant_series_yields_neutral() {
193 let mut elder = ElderImpulse::classic();
196 let out = elder.batch(&[42.0_f64; 120]);
197 for v in out.iter().skip(40).flatten() {
199 assert_eq!(*v, 0.0);
200 }
201 }
202
203 #[test]
204 fn pure_uptrend_signals_buy() {
205 let mut elder = ElderImpulse::classic();
208 for i in 1..=300 {
209 elder.update(f64::from(i));
210 }
211 let v = elder.current.unwrap();
214 assert!(v >= 0.0, "uptrend should not signal sell: {v}");
215 }
216
217 #[test]
218 fn warmup_emits_first_value_at_warmup_period() {
219 let mut elder = ElderImpulse::new(3, 2, 4, 3).unwrap();
220 assert_eq!(elder.warmup_period(), 7);
223 let prices: Vec<f64> = (1..=10).map(f64::from).collect();
224 let out = elder.batch(&prices);
225 for v in out.iter().take(6) {
226 assert!(v.is_none());
227 }
228 assert!(out[6].is_some());
229 }
230
231 #[test]
232 fn batch_equals_streaming() {
233 let prices: Vec<f64> = (1..=200)
234 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
235 .collect();
236 let mut a = ElderImpulse::classic();
237 let mut b = ElderImpulse::classic();
238 assert_eq!(
239 a.batch(&prices),
240 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
241 );
242 }
243
244 #[test]
245 fn reset_clears_state() {
246 let mut elder = ElderImpulse::classic();
247 elder.batch(&(1..=200).map(f64::from).collect::<Vec<_>>());
248 assert!(elder.is_ready());
249 elder.reset();
250 assert!(!elder.is_ready());
251 }
252}