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 Ok(Self {
59 ema_period,
60 macd_fast,
61 macd_slow,
62 macd_signal,
63 ema: Ema::new(ema_period)?,
64 macd: MacdIndicator::new(macd_fast, macd_slow, macd_signal)?,
65 prev_ema: None,
66 prev_hist: None,
67 current: None,
68 })
69 }
70
71 pub fn classic() -> Self {
73 Self::new(13, 12, 26, 9).expect("classic Elder Impulse parameters are valid")
74 }
75
76 pub const fn periods(&self) -> (usize, usize, usize, usize) {
78 (
79 self.ema_period,
80 self.macd_fast,
81 self.macd_slow,
82 self.macd_signal,
83 )
84 }
85}
86
87impl Indicator for ElderImpulse {
88 type Input = f64;
89 type Output = f64;
90
91 fn update(&mut self, input: f64) -> Option<f64> {
92 let ema_now = self.ema.update(input);
94 let macd_now = self.macd.update(input);
95 let (ema_now, macd_now) = (ema_now?, macd_now?);
96
97 let prev_ema = self.prev_ema;
100 let prev_hist = self.prev_hist;
101 self.prev_ema = Some(ema_now);
102 self.prev_hist = Some(macd_now.histogram);
103 let prev_ema = prev_ema?;
104 let prev_hist = prev_hist?;
105
106 let ema_rising = ema_now > prev_ema;
107 let ema_falling = ema_now < prev_ema;
108 let hist_rising = macd_now.histogram > prev_hist;
109 let hist_falling = macd_now.histogram < prev_hist;
110
111 let value = if ema_rising && hist_rising {
112 1.0
113 } else if ema_falling && hist_falling {
114 -1.0
115 } else {
116 0.0
117 };
118 self.current = Some(value);
119 Some(value)
120 }
121
122 fn reset(&mut self) {
123 self.ema.reset();
124 self.macd.reset();
125 self.prev_ema = None;
126 self.prev_hist = None;
127 self.current = None;
128 }
129
130 fn warmup_period(&self) -> usize {
131 let macd_warmup = self.macd_slow + self.macd_signal - 1;
135 self.ema_period.max(macd_warmup) + 1
136 }
137
138 fn is_ready(&self) -> bool {
139 self.current.is_some()
140 }
141
142 fn name(&self) -> &'static str {
143 "ElderImpulse"
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::traits::BatchExt;
151
152 #[test]
153 fn rejects_zero_period() {
154 assert!(matches!(
155 ElderImpulse::new(0, 12, 26, 9),
156 Err(Error::PeriodZero)
157 ));
158 assert!(matches!(
159 ElderImpulse::new(13, 0, 26, 9),
160 Err(Error::PeriodZero)
161 ));
162 }
163
164 #[test]
165 fn rejects_invalid_macd_params() {
166 assert!(ElderImpulse::new(13, 26, 12, 9).is_err());
168 }
169
170 #[test]
171 fn accessors_and_metadata() {
172 let elder = ElderImpulse::classic();
173 assert_eq!(elder.periods(), (13, 12, 26, 9));
174 assert_eq!(elder.name(), "ElderImpulse");
175 }
176
177 #[test]
178 fn classic_factory() {
179 assert_eq!(ElderImpulse::classic().periods(), (13, 12, 26, 9));
180 }
181
182 #[test]
183 fn constant_series_yields_neutral() {
184 let mut elder = ElderImpulse::classic();
187 let out = elder.batch(&[42.0_f64; 120]);
188 for v in out.iter().skip(40).flatten() {
190 assert_eq!(*v, 0.0);
191 }
192 }
193
194 #[test]
195 fn pure_uptrend_signals_buy() {
196 let mut elder = ElderImpulse::classic();
199 for i in 1..=300 {
200 elder.update(f64::from(i));
201 }
202 let v = elder.current.unwrap();
205 assert!(v >= 0.0, "uptrend should not signal sell: {v}");
206 }
207
208 #[test]
209 fn warmup_emits_first_value_at_warmup_period() {
210 let mut elder = ElderImpulse::new(3, 2, 4, 3).unwrap();
211 assert_eq!(elder.warmup_period(), 7);
214 let prices: Vec<f64> = (1..=10).map(f64::from).collect();
215 let out = elder.batch(&prices);
216 for v in out.iter().take(6) {
217 assert!(v.is_none());
218 }
219 assert!(out[6].is_some());
220 }
221
222 #[test]
223 fn batch_equals_streaming() {
224 let prices: Vec<f64> = (1..=200)
225 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
226 .collect();
227 let mut a = ElderImpulse::classic();
228 let mut b = ElderImpulse::classic();
229 assert_eq!(
230 a.batch(&prices),
231 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
232 );
233 }
234
235 #[test]
236 fn reset_clears_state() {
237 let mut elder = ElderImpulse::classic();
238 elder.batch(&(1..=200).map(f64::from).collect::<Vec<_>>());
239 assert!(elder.is_ready());
240 elder.reset();
241 assert!(!elder.is_ready());
242 }
243}