1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct ElderSafeZoneOutput {
12 pub value: f64,
14 pub direction: f64,
16}
17
18#[derive(Debug, Clone)]
56pub struct ElderSafeZone {
57 period: usize,
58 coeff: f64,
59 prev: Option<Candle>,
60 down_pen: VecDeque<f64>,
61 up_pen: VecDeque<f64>,
62 down_sum: f64,
63 up_sum: f64,
64 down_count: usize,
65 up_count: usize,
66 direction: f64,
67 stop: f64,
68 last: Option<ElderSafeZoneOutput>,
69}
70
71impl ElderSafeZone {
72 pub fn new(period: usize, coeff: f64) -> Result<Self> {
80 if period == 0 {
81 return Err(Error::PeriodZero);
82 }
83 if period > crate::error::MAX_PERIOD {
84 return Err(Error::InvalidPeriod {
85 message: crate::error::PERIOD_ABOVE_MAX,
86 });
87 }
88 if !coeff.is_finite() || coeff <= 0.0 {
89 return Err(Error::NonPositiveMultiplier);
90 }
91 Ok(Self {
92 period,
93 coeff,
94 prev: None,
95 down_pen: VecDeque::with_capacity(period),
96 up_pen: VecDeque::with_capacity(period),
97 down_sum: 0.0,
98 up_sum: 0.0,
99 down_count: 0,
100 up_count: 0,
101 direction: 0.0,
102 stop: 0.0,
103 last: None,
104 })
105 }
106
107 pub const fn params(&self) -> (usize, f64) {
109 (self.period, self.coeff)
110 }
111
112 pub const fn value(&self) -> Option<ElderSafeZoneOutput> {
114 self.last
115 }
116
117 fn push(window: &mut VecDeque<f64>, sum: &mut f64, count: &mut usize, period: usize, pen: f64) {
118 if window.len() == period {
119 let old = window.pop_front().expect("non-empty");
120 *sum -= old;
121 if old > 0.0 {
122 *count -= 1;
123 }
124 }
125 window.push_back(pen);
126 *sum += pen;
127 if pen > 0.0 {
128 *count += 1;
129 }
130 }
131
132 fn avg(sum: f64, count: usize) -> f64 {
133 if count == 0 {
134 0.0
135 } else {
136 sum / count as f64
137 }
138 }
139}
140
141impl Indicator for ElderSafeZone {
142 type Input = Candle;
143 type Output = ElderSafeZoneOutput;
144
145 fn update(&mut self, candle: Candle) -> Option<ElderSafeZoneOutput> {
146 let Some(prev) = self.prev else {
147 self.prev = Some(candle);
148 return None;
149 };
150 let dp = (prev.low - candle.low).max(0.0);
151 let up = (candle.high - prev.high).max(0.0);
152 self.prev = Some(candle);
153
154 Self::push(
155 &mut self.down_pen,
156 &mut self.down_sum,
157 &mut self.down_count,
158 self.period,
159 dp,
160 );
161 Self::push(
162 &mut self.up_pen,
163 &mut self.up_sum,
164 &mut self.up_count,
165 self.period,
166 up,
167 );
168 if self.down_pen.len() < self.period {
169 return None;
170 }
171
172 let avg_down = Self::avg(self.down_sum, self.down_count);
173 let avg_up = Self::avg(self.up_sum, self.up_count);
174
175 if self.direction == 0.0 {
176 self.direction = 1.0;
177 self.stop = candle.low - self.coeff * avg_down;
178 } else if self.direction > 0.0 {
179 let raw = candle.low - self.coeff * avg_down;
180 self.stop = self.stop.max(raw);
181 if candle.close < self.stop {
182 self.direction = -1.0;
183 self.stop = candle.high + self.coeff * avg_up;
184 }
185 } else {
186 let raw = candle.high + self.coeff * avg_up;
187 self.stop = self.stop.min(raw);
188 if candle.close > self.stop {
189 self.direction = 1.0;
190 self.stop = candle.low - self.coeff * avg_down;
191 }
192 }
193
194 let out = ElderSafeZoneOutput {
195 value: self.stop,
196 direction: self.direction,
197 };
198 self.last = Some(out);
199 Some(out)
200 }
201
202 fn reset(&mut self) {
203 self.prev = None;
204 self.down_pen.clear();
205 self.up_pen.clear();
206 self.down_sum = 0.0;
207 self.up_sum = 0.0;
208 self.down_count = 0;
209 self.up_count = 0;
210 self.direction = 0.0;
211 self.stop = 0.0;
212 self.last = None;
213 }
214
215 #[inline]
216 fn warmup_period(&self) -> usize {
217 self.period + 1
218 }
219
220 #[inline]
221 fn is_ready(&self) -> bool {
222 self.last.is_some()
223 }
224
225 #[inline]
226 fn name(&self) -> &'static str {
227 "ElderSafeZone"
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::traits::BatchExt;
235
236 fn c(high: f64, low: f64, close: f64) -> Candle {
237 Candle::new_unchecked(f64::midpoint(high, low), high, low, close, 1_000.0, 0)
238 }
239
240 #[test]
241 fn rejects_invalid_params() {
242 assert!(matches!(ElderSafeZone::new(0, 2.0), Err(Error::PeriodZero)));
243 assert!(matches!(
244 ElderSafeZone::new(14, 0.0),
245 Err(Error::NonPositiveMultiplier)
246 ));
247 assert!(matches!(
248 ElderSafeZone::new(14, -1.0),
249 Err(Error::NonPositiveMultiplier)
250 ));
251 }
252
253 #[test]
254 fn accessors_and_metadata() {
255 let e = ElderSafeZone::new(14, 2.0).unwrap();
256 assert_eq!(e.params(), (14, 2.0));
257 assert_eq!(e.warmup_period(), 15);
258 assert_eq!(e.name(), "ElderSafeZone");
259 assert!(!e.is_ready());
260 assert_eq!(e.value(), None);
261 }
262
263 #[test]
264 fn first_emission_at_warmup_period() {
265 let mut e = ElderSafeZone::new(3, 2.0).unwrap();
266 let candles: Vec<Candle> = (0..8)
267 .map(|i| {
268 let base = 100.0 + f64::from(i);
269 c(base + 1.0, base - 1.0, base)
270 })
271 .collect();
272 let out = e.batch(&candles);
273 let warmup = e.warmup_period(); assert_eq!(warmup, 4);
275 for v in out.iter().take(warmup - 1) {
276 assert!(v.is_none());
277 }
278 assert!(out[warmup - 1].is_some());
279 }
280
281 #[test]
282 fn uptrend_keeps_stop_below_price() {
283 let mut e = ElderSafeZone::new(5, 2.0).unwrap();
284 let candles: Vec<Candle> = (0..60)
285 .map(|i| {
286 let base = 100.0 + 2.0 * f64::from(i);
287 c(base + 1.0, base - 1.0, base + 0.5)
288 })
289 .collect();
290 for (o, candle) in e.batch(&candles).into_iter().zip(candles.iter()) {
291 if let Some(o) = o {
292 assert_eq!(o.direction, 1.0);
293 assert!(o.value <= candle.close);
294 }
295 }
296 }
297
298 #[test]
299 fn noiseless_trend_stop_sits_at_low() {
300 let mut e = ElderSafeZone::new(3, 2.0).unwrap();
303 let candles: Vec<Candle> = (0..10)
304 .map(|i| {
305 let base = 100.0 + f64::from(i);
306 c(base + 1.0, base - 1.0, base + 0.5)
307 })
308 .collect();
309 let out = e.batch(&candles);
310 let last_candle = candles.last().unwrap();
311 let last = out.last().unwrap().unwrap();
312 assert!((last.value - last_candle.low).abs() < 1e-9);
313 }
314
315 #[test]
316 fn flips_on_reversal() {
317 let mut candles: Vec<Candle> = (0..40)
318 .map(|i| {
319 let base = 100.0 + f64::from(i);
320 c(base + 1.0, base - 1.0, base + 0.5)
321 })
322 .collect();
323 candles.extend((0..40).map(|i| {
324 let base = 140.0 - f64::from(i);
325 c(base + 1.0, base - 1.0, base - 0.5)
326 }));
327 let mut e = ElderSafeZone::new(5, 2.0).unwrap();
328 let dirs: Vec<f64> = e
329 .batch(&candles)
330 .into_iter()
331 .flatten()
332 .map(|o| o.direction)
333 .collect();
334 assert!(dirs.iter().any(|&d| d > 0.0));
335 assert!(dirs.iter().any(|&d| d < 0.0));
336 }
337
338 #[test]
339 fn reset_clears_state() {
340 let mut e = ElderSafeZone::new(5, 2.0).unwrap();
341 let candles: Vec<Candle> = (0..40)
342 .map(|i| {
343 let base = 100.0 + f64::from(i);
344 c(base + 1.0, base - 1.0, base + 0.5)
345 })
346 .collect();
347 e.batch(&candles);
348 assert!(e.is_ready());
349 e.reset();
350 assert!(!e.is_ready());
351 assert_eq!(e.value(), None);
352 assert_eq!(e.update(candles[0]), None);
353 }
354
355 #[test]
356 fn batch_equals_streaming() {
357 let candles: Vec<Candle> = (0..120)
358 .map(|i| {
359 let base = 100.0 + (f64::from(i) * 0.25).sin() * 9.0;
360 c(base + 2.0, base - 1.5, base + 0.5)
361 })
362 .collect();
363 let batch = ElderSafeZone::new(14, 2.0).unwrap().batch(&candles);
364 let mut b = ElderSafeZone::new(14, 2.0).unwrap();
365 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
366 assert_eq!(batch, streamed);
367 }
368}