1#![allow(clippy::doc_markdown)]
2
3use std::collections::VecDeque;
33
34use crate::error::{Error, Result};
35use crate::ohlcv::Candle;
36use crate::traits::Indicator;
37
38#[derive(Debug, Clone)]
40pub struct TdRei {
41 period: usize,
42 candles: VecDeque<Candle>,
46 numerators: VecDeque<f64>,
47 denominators: VecDeque<f64>,
48 last_value: Option<f64>,
49}
50
51const LOOKBACK: usize = 9;
56
57impl TdRei {
58 pub fn new(period: usize) -> Result<Self> {
65 if period == 0 {
66 return Err(Error::PeriodZero);
67 }
68 if period > crate::error::MAX_PERIOD {
69 return Err(Error::InvalidPeriod {
70 message: crate::error::PERIOD_ABOVE_MAX,
71 });
72 }
73 Ok(Self {
74 period,
75 candles: VecDeque::with_capacity(LOOKBACK),
76 numerators: VecDeque::with_capacity(period),
77 denominators: VecDeque::with_capacity(period),
78 last_value: None,
79 })
80 }
81
82 pub fn classic() -> Self {
84 Self::new(5).expect("classic TD REI parameters are valid")
85 }
86
87 pub const fn period(&self) -> usize {
89 self.period
90 }
91
92 pub const fn value(&self) -> Option<f64> {
94 self.last_value
95 }
96}
97
98impl Indicator for TdRei {
99 type Input = Candle;
100 type Output = f64;
101
102 fn update(&mut self, candle: Candle) -> Option<f64> {
103 if self.candles.len() == LOOKBACK {
106 self.candles.pop_front();
107 }
108 if self.candles.len() < LOOKBACK - 1 {
109 self.candles.push_back(candle);
112 return None;
113 }
114 let prev2 = self.candles[6];
117 let prev5 = self.candles[3];
118 let prev6 = self.candles[2];
119 let close7 = self.candles[1].close;
120 let close8 = self.candles[0].close;
121
122 let overlap = (candle.high >= prev5.low || candle.high >= prev6.low)
124 && (candle.low <= prev5.high || candle.low <= prev6.high);
125 let overlap_back = (prev2.high >= close7 || prev2.high >= close8)
127 && (prev2.low <= close7 || prev2.low <= close8);
128
129 let raw_num = (candle.high - prev2.high) + (candle.low - prev2.low);
130 let denominator = (candle.high - prev2.high).abs() + (candle.low - prev2.low).abs();
131 let numerator = if overlap || overlap_back {
132 raw_num
133 } else {
134 0.0
135 };
136
137 if self.numerators.len() == self.period {
138 self.numerators.pop_front();
139 self.denominators.pop_front();
140 }
141 self.numerators.push_back(numerator);
142 self.denominators.push_back(denominator);
143 self.candles.push_back(candle);
144
145 if self.numerators.len() < self.period {
146 return None;
147 }
148 let sum_num: f64 = self.numerators.iter().sum();
149 let sum_den: f64 = self.denominators.iter().sum();
150 let v = if sum_den == 0.0 {
151 0.0
152 } else {
153 (100.0 * sum_num / sum_den).clamp(-100.0, 100.0)
156 };
157 self.last_value = Some(v);
158 Some(v)
159 }
160
161 fn reset(&mut self) {
162 self.candles.clear();
163 self.numerators.clear();
164 self.denominators.clear();
165 self.last_value = None;
166 }
167
168 #[inline]
169 fn warmup_period(&self) -> usize {
170 (LOOKBACK - 1) + self.period
173 }
174
175 #[inline]
176 fn is_ready(&self) -> bool {
177 self.last_value.is_some()
178 }
179
180 #[inline]
181 fn name(&self) -> &'static str {
182 "TDREI"
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::traits::BatchExt;
190 use approx::assert_relative_eq;
191
192 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
193 Candle::new_unchecked(close, high, low, close, 0.0, ts)
194 }
195
196 #[test]
197 fn flat_market_yields_neutral_zero() {
198 let candles: Vec<Candle> = (0..40).map(|i| c(11.0, 9.0, 10.0, i)).collect();
201 let mut rei = TdRei::classic();
202 let out = rei.batch(&candles);
203 for v in out.iter().skip(rei.warmup_period()).copied().flatten() {
204 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
205 }
206 }
207
208 #[test]
209 fn pure_uptrend_pegs_indicator_at_100() {
210 let candles: Vec<Candle> = (0..40)
218 .map(|i| {
219 let m = 100.0 + f64::from(i) * 0.1;
220 c(m + 1.0, m - 1.0, m, i64::from(i))
221 })
222 .collect();
223 let mut rei = TdRei::classic();
224 let last = rei.batch(&candles).into_iter().flatten().last().unwrap();
225 assert_relative_eq!(last, 100.0, epsilon = 1e-9);
228 }
229
230 #[test]
231 fn pure_downtrend_pegs_indicator_at_minus_100() {
232 let candles: Vec<Candle> = (0..40)
233 .map(|i| {
234 let m = 100.0 - f64::from(i) * 0.1;
235 c(m + 1.0, m - 1.0, m, i64::from(i))
236 })
237 .collect();
238 let mut rei = TdRei::classic();
239 let last = rei.batch(&candles).into_iter().flatten().last().unwrap();
240 assert_relative_eq!(last, -100.0, epsilon = 1e-9);
241 }
242
243 #[test]
244 fn stays_in_minus_100_to_100() {
245 let candles: Vec<Candle> = (0..200)
246 .map(|i| {
247 let m = 50.0 + (f64::from(i) * 0.2).sin() * 5.0;
248 c(m + 1.0, m - 1.0, m, i64::from(i))
249 })
250 .collect();
251 let mut rei = TdRei::classic();
252 for v in rei.batch(&candles).into_iter().flatten() {
253 assert!((-100.0..=100.0).contains(&v), "out of range: {v}");
254 }
255 }
256
257 #[test]
258 fn batch_equals_streaming() {
259 let candles: Vec<Candle> = (0..80)
260 .map(|i| {
261 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
262 c(m + 1.0, m - 1.0, m, i64::from(i))
263 })
264 .collect();
265 let mut a = TdRei::classic();
266 let mut b = TdRei::classic();
267 assert_eq!(
268 a.batch(&candles),
269 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
270 );
271 }
272
273 #[test]
274 fn rejects_zero_period() {
275 assert!(matches!(TdRei::new(0), Err(Error::PeriodZero)));
276 }
277
278 #[test]
279 fn reset_clears_state() {
280 let candles: Vec<Candle> = (0..40)
281 .map(|i| {
282 let m = 100.0 + f64::from(i) * 0.1;
283 c(m + 1.0, m - 1.0, m, i64::from(i))
284 })
285 .collect();
286 let mut rei = TdRei::classic();
287 rei.batch(&candles);
288 assert!(rei.is_ready());
289 rei.reset();
290 assert!(!rei.is_ready());
291 assert_eq!(rei.update(candles[0]), None);
292 assert_eq!(rei.value(), None);
293 }
294
295 #[test]
296 fn accessors_and_metadata() {
297 let rei = TdRei::classic();
298 assert_eq!(rei.period(), 5);
299 assert_eq!(rei.warmup_period(), 8 + 5);
300 assert_eq!(rei.name(), "TDREI");
301 }
302
303 fn base(far: f64) -> Vec<Candle> {
306 (0..8)
307 .map(|i| {
308 let m = if i < 2 { far } else { 10.0 };
309 c(m + 1.0, m - 1.0, m, i64::from(i))
310 })
311 .collect()
312 }
313
314 #[test]
315 fn hand_computed_overlap_mixed_sign() {
316 let mut rei = TdRei::new(1).unwrap();
321 let mut candles = base(10.0);
322 candles.push(c(13.0, 8.5, 10.0, 8));
323 let out = rei.batch(&candles);
324 assert_relative_eq!(out[8].unwrap(), 60.0, epsilon = 1e-12);
325 }
326
327 #[test]
328 fn numerator_is_gated_but_denominator_counts() {
329 let mut rei = TdRei::new(1).unwrap();
334 let mut candles = base(50.0);
335 candles.push(c(30.0, 25.0, 27.0, 8));
336 assert_eq!(rei.batch(&candles)[8], Some(0.0));
337 assert_eq!(rei.denominators.back().copied(), Some(35.0));
338 assert_eq!(rei.numerators.back().copied(), Some(0.0));
339 }
340
341 #[test]
342 fn overlap_back_alone_enables_numerator() {
343 let mut rei = TdRei::new(1).unwrap();
347 let mut candles = base(10.0);
348 candles.push(c(30.0, 25.0, 27.0, 8));
349 assert_eq!(rei.batch(&candles)[8], Some(100.0));
350 }
351
352 #[test]
353 fn hand_computed_period_two_window() {
354 let mut rei = TdRei::new(2).unwrap();
361 let mut candles = base(50.0);
362 candles.push(c(30.0, 25.0, 27.0, 8));
363 candles.push(c(12.0, 10.0, 11.0, 9));
364 let out = rei.batch(&candles);
365 assert_eq!(rei.warmup_period(), 10);
366 assert!(out[..9].iter().all(Option::is_none));
367 assert_relative_eq!(out[9].unwrap(), 200.0 / 37.0, epsilon = 1e-12);
368 }
369
370 #[test]
371 fn first_value_lands_at_warmup_minus_one() {
372 let candles: Vec<Candle> = (0..40)
373 .map(|i| {
374 let m = 100.0 + (f64::from(i) * 0.4).sin() * 3.0;
375 c(m + 1.0, m - 1.0, m, i64::from(i))
376 })
377 .collect();
378 for period in [1, 2, 5, 14] {
379 let mut rei = TdRei::new(period).unwrap();
380 let warm = rei.warmup_period();
381 assert_eq!(warm, 8 + period);
382 let out = rei.batch(&candles);
383 assert!(out[..warm - 1].iter().all(Option::is_none));
384 assert!(out[warm - 1..].iter().all(Option::is_some));
385 }
386 }
387
388 #[test]
389 fn rejects_period_above_max() {
390 let err = TdRei::new(crate::error::MAX_PERIOD + 1).unwrap_err();
391 assert!(matches!(err, Error::InvalidPeriod { .. }));
392 }
393
394 #[test]
395 fn reset_reproduces_fresh_run() {
396 let candles: Vec<Candle> = (0..60)
397 .map(|i| {
398 let m = 100.0 + (f64::from(i) * 0.6).sin() * 4.0;
399 c(m + 1.5, m - 0.5, m, i64::from(i))
400 })
401 .collect();
402 let mut fresh = TdRei::classic();
403 let expected = fresh.batch(&candles);
404 let mut rei = TdRei::classic();
405 rei.batch(&candles[..23]);
406 rei.reset();
407 assert_eq!(rei.batch(&candles), expected);
408 }
409
410 #[test]
411 fn batch_nan_into_matches_streaming() {
412 let candles: Vec<Candle> = (0..80)
413 .map(|i| {
414 let m = 100.0 + (f64::from(i) * 0.9).sin() * 6.0;
415 c(m + 1.5, m - 0.5, m, i64::from(i))
416 })
417 .collect();
418 let mut a = TdRei::classic();
419 let mut out = vec![0.0; candles.len()];
420 a.batch_nan_into(&candles, &mut out);
421 let mut b = TdRei::classic();
422 let streamed: Vec<f64> = candles
423 .iter()
424 .map(|x| b.update(*x).unwrap_or(f64::NAN))
425 .collect();
426 assert!(out
427 .iter()
428 .zip(&streamed)
429 .all(|(x, y)| x.to_bits() == y.to_bits()));
430 }
431}