wickra_core/indicators/
td_rei.rs1#![allow(clippy::doc_markdown)]
2
3use std::collections::VecDeque;
31
32use crate::error::{Error, Result};
33use crate::ohlcv::Candle;
34use crate::traits::Indicator;
35
36#[derive(Debug, Clone)]
38pub struct TdRei {
39 period: usize,
40 candles: VecDeque<Candle>,
44 numerators: VecDeque<f64>,
45 denominators: VecDeque<f64>,
46 last_value: Option<f64>,
47}
48
49const LOOKBACK: usize = 7;
54
55impl TdRei {
56 pub fn new(period: usize) -> Result<Self> {
63 if period == 0 {
64 return Err(Error::PeriodZero);
65 }
66 Ok(Self {
67 period,
68 candles: VecDeque::with_capacity(LOOKBACK),
69 numerators: VecDeque::with_capacity(period),
70 denominators: VecDeque::with_capacity(period),
71 last_value: None,
72 })
73 }
74
75 pub fn classic() -> Self {
77 Self::new(5).expect("classic TD REI parameters are valid")
78 }
79
80 pub const fn period(&self) -> usize {
82 self.period
83 }
84
85 pub const fn value(&self) -> Option<f64> {
87 self.last_value
88 }
89}
90
91impl Indicator for TdRei {
92 type Input = Candle;
93 type Output = f64;
94
95 fn update(&mut self, candle: Candle) -> Option<f64> {
96 if self.candles.len() == LOOKBACK {
99 self.candles.pop_front();
100 }
101 if self.candles.len() < LOOKBACK - 1 {
102 self.candles.push_back(candle);
105 return None;
106 }
107 let prev2 = self.candles[self.candles.len() - 2];
116 let prev5 = self.candles[1];
117 let prev6 = self.candles[0];
118
119 let cond1 = candle.high >= prev5.low || candle.high >= prev6.low;
120 let cond2 = candle.low <= prev5.high || candle.low <= prev6.high;
121
122 let raw_num = (candle.high - prev2.high) + (candle.low - prev2.low);
123 let denominator = (candle.high - prev2.high).abs() + (candle.low - prev2.low).abs();
124 let numerator = if cond1 && cond2 { raw_num } else { 0.0 };
125
126 if self.numerators.len() == self.period {
127 self.numerators.pop_front();
128 self.denominators.pop_front();
129 }
130 self.numerators.push_back(numerator);
131 self.denominators.push_back(denominator);
132 self.candles.push_back(candle);
133
134 if self.numerators.len() < self.period {
135 return None;
136 }
137 let sum_num: f64 = self.numerators.iter().sum();
138 let sum_den: f64 = self.denominators.iter().sum();
139 let v = if sum_den == 0.0 {
140 0.0
141 } else {
142 100.0 * sum_num / sum_den
143 };
144 self.last_value = Some(v);
145 Some(v)
146 }
147
148 fn reset(&mut self) {
149 self.candles.clear();
150 self.numerators.clear();
151 self.denominators.clear();
152 self.last_value = None;
153 }
154
155 fn warmup_period(&self) -> usize {
156 (LOOKBACK - 1) + self.period
159 }
160
161 fn is_ready(&self) -> bool {
162 self.last_value.is_some()
163 }
164
165 fn name(&self) -> &'static str {
166 "TDREI"
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::traits::BatchExt;
174 use approx::assert_relative_eq;
175
176 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
177 Candle::new_unchecked(close, high, low, close, 0.0, ts)
178 }
179
180 #[test]
181 fn flat_market_yields_neutral_zero() {
182 let candles: Vec<Candle> = (0..40).map(|i| c(11.0, 9.0, 10.0, i)).collect();
185 let mut rei = TdRei::classic();
186 let out = rei.batch(&candles);
187 for v in out.iter().skip(rei.warmup_period()).copied().flatten() {
188 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
189 }
190 }
191
192 #[test]
193 fn pure_uptrend_pegs_indicator_at_100() {
194 let candles: Vec<Candle> = (0..40)
202 .map(|i| {
203 let m = 100.0 + f64::from(i) * 0.1;
204 c(m + 1.0, m - 1.0, m, i64::from(i))
205 })
206 .collect();
207 let mut rei = TdRei::classic();
208 let last = rei.batch(&candles).into_iter().flatten().last().unwrap();
209 assert_relative_eq!(last, 100.0, epsilon = 1e-9);
212 }
213
214 #[test]
215 fn pure_downtrend_pegs_indicator_at_minus_100() {
216 let candles: Vec<Candle> = (0..40)
217 .map(|i| {
218 let m = 100.0 - f64::from(i) * 0.1;
219 c(m + 1.0, m - 1.0, m, i64::from(i))
220 })
221 .collect();
222 let mut rei = TdRei::classic();
223 let last = rei.batch(&candles).into_iter().flatten().last().unwrap();
224 assert_relative_eq!(last, -100.0, epsilon = 1e-9);
225 }
226
227 #[test]
228 fn stays_in_minus_100_to_100() {
229 let candles: Vec<Candle> = (0..200)
230 .map(|i| {
231 let m = 50.0 + (f64::from(i) * 0.2).sin() * 5.0;
232 c(m + 1.0, m - 1.0, m, i64::from(i))
233 })
234 .collect();
235 let mut rei = TdRei::classic();
236 for v in rei.batch(&candles).into_iter().flatten() {
237 assert!((-100.0..=100.0).contains(&v), "out of range: {v}");
238 }
239 }
240
241 #[test]
242 fn batch_equals_streaming() {
243 let candles: Vec<Candle> = (0..80)
244 .map(|i| {
245 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
246 c(m + 1.0, m - 1.0, m, i64::from(i))
247 })
248 .collect();
249 let mut a = TdRei::classic();
250 let mut b = TdRei::classic();
251 assert_eq!(
252 a.batch(&candles),
253 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
254 );
255 }
256
257 #[test]
258 fn rejects_zero_period() {
259 assert!(matches!(TdRei::new(0), Err(Error::PeriodZero)));
260 }
261
262 #[test]
263 fn reset_clears_state() {
264 let candles: Vec<Candle> = (0..40)
265 .map(|i| {
266 let m = 100.0 + f64::from(i) * 0.1;
267 c(m + 1.0, m - 1.0, m, i64::from(i))
268 })
269 .collect();
270 let mut rei = TdRei::classic();
271 rei.batch(&candles);
272 assert!(rei.is_ready());
273 rei.reset();
274 assert!(!rei.is_ready());
275 assert_eq!(rei.update(candles[0]), None);
276 assert_eq!(rei.value(), None);
277 }
278
279 #[test]
280 fn accessors_and_metadata() {
281 let rei = TdRei::classic();
282 assert_eq!(rei.period(), 5);
283 assert_eq!(rei.warmup_period(), 6 + 5);
284 assert_eq!(rei.name(), "TDREI");
285 }
286}