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