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 if period > crate::error::MAX_PERIOD {
67 return Err(Error::InvalidPeriod {
68 message: crate::error::PERIOD_ABOVE_MAX,
69 });
70 }
71 Ok(Self {
72 period,
73 candles: VecDeque::with_capacity(LOOKBACK),
74 numerators: VecDeque::with_capacity(period),
75 denominators: VecDeque::with_capacity(period),
76 last_value: None,
77 })
78 }
79
80 pub fn classic() -> Self {
82 Self::new(5).expect("classic TD REI parameters are valid")
83 }
84
85 pub const fn period(&self) -> usize {
87 self.period
88 }
89
90 pub const fn value(&self) -> Option<f64> {
92 self.last_value
93 }
94}
95
96impl Indicator for TdRei {
97 type Input = Candle;
98 type Output = f64;
99
100 fn update(&mut self, candle: Candle) -> Option<f64> {
101 if self.candles.len() == LOOKBACK {
104 self.candles.pop_front();
105 }
106 if self.candles.len() < LOOKBACK - 1 {
107 self.candles.push_back(candle);
110 return None;
111 }
112 let prev2 = self.candles[self.candles.len() - 2];
121 let prev5 = self.candles[1];
122 let prev6 = self.candles[0];
123
124 let cond1 = candle.high >= prev5.low || candle.high >= prev6.low;
125 let cond2 = candle.low <= prev5.high || candle.low <= prev6.high;
126
127 let raw_num = (candle.high - prev2.high) + (candle.low - prev2.low);
128 let denominator = (candle.high - prev2.high).abs() + (candle.low - prev2.low).abs();
129 let numerator = if cond1 && cond2 { raw_num } else { 0.0 };
130
131 if self.numerators.len() == self.period {
132 self.numerators.pop_front();
133 self.denominators.pop_front();
134 }
135 self.numerators.push_back(numerator);
136 self.denominators.push_back(denominator);
137 self.candles.push_back(candle);
138
139 if self.numerators.len() < self.period {
140 return None;
141 }
142 let sum_num: f64 = self.numerators.iter().sum();
143 let sum_den: f64 = self.denominators.iter().sum();
144 let v = if sum_den == 0.0 {
145 0.0
146 } else {
147 100.0 * sum_num / sum_den
148 };
149 self.last_value = Some(v);
150 Some(v)
151 }
152
153 fn reset(&mut self) {
154 self.candles.clear();
155 self.numerators.clear();
156 self.denominators.clear();
157 self.last_value = None;
158 }
159
160 #[inline]
161 fn warmup_period(&self) -> usize {
162 (LOOKBACK - 1) + self.period
165 }
166
167 #[inline]
168 fn is_ready(&self) -> bool {
169 self.last_value.is_some()
170 }
171
172 #[inline]
173 fn name(&self) -> &'static str {
174 "TDREI"
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::traits::BatchExt;
182 use approx::assert_relative_eq;
183
184 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
185 Candle::new_unchecked(close, high, low, close, 0.0, ts)
186 }
187
188 #[test]
189 fn flat_market_yields_neutral_zero() {
190 let candles: Vec<Candle> = (0..40).map(|i| c(11.0, 9.0, 10.0, i)).collect();
193 let mut rei = TdRei::classic();
194 let out = rei.batch(&candles);
195 for v in out.iter().skip(rei.warmup_period()).copied().flatten() {
196 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
197 }
198 }
199
200 #[test]
201 fn pure_uptrend_pegs_indicator_at_100() {
202 let candles: Vec<Candle> = (0..40)
210 .map(|i| {
211 let m = 100.0 + f64::from(i) * 0.1;
212 c(m + 1.0, m - 1.0, m, i64::from(i))
213 })
214 .collect();
215 let mut rei = TdRei::classic();
216 let last = rei.batch(&candles).into_iter().flatten().last().unwrap();
217 assert_relative_eq!(last, 100.0, epsilon = 1e-9);
220 }
221
222 #[test]
223 fn pure_downtrend_pegs_indicator_at_minus_100() {
224 let candles: Vec<Candle> = (0..40)
225 .map(|i| {
226 let m = 100.0 - f64::from(i) * 0.1;
227 c(m + 1.0, m - 1.0, m, i64::from(i))
228 })
229 .collect();
230 let mut rei = TdRei::classic();
231 let last = rei.batch(&candles).into_iter().flatten().last().unwrap();
232 assert_relative_eq!(last, -100.0, epsilon = 1e-9);
233 }
234
235 #[test]
236 fn stays_in_minus_100_to_100() {
237 let candles: Vec<Candle> = (0..200)
238 .map(|i| {
239 let m = 50.0 + (f64::from(i) * 0.2).sin() * 5.0;
240 c(m + 1.0, m - 1.0, m, i64::from(i))
241 })
242 .collect();
243 let mut rei = TdRei::classic();
244 for v in rei.batch(&candles).into_iter().flatten() {
245 assert!((-100.0..=100.0).contains(&v), "out of range: {v}");
246 }
247 }
248
249 #[test]
250 fn batch_equals_streaming() {
251 let candles: Vec<Candle> = (0..80)
252 .map(|i| {
253 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
254 c(m + 1.0, m - 1.0, m, i64::from(i))
255 })
256 .collect();
257 let mut a = TdRei::classic();
258 let mut b = TdRei::classic();
259 assert_eq!(
260 a.batch(&candles),
261 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
262 );
263 }
264
265 #[test]
266 fn rejects_zero_period() {
267 assert!(matches!(TdRei::new(0), Err(Error::PeriodZero)));
268 }
269
270 #[test]
271 fn reset_clears_state() {
272 let candles: Vec<Candle> = (0..40)
273 .map(|i| {
274 let m = 100.0 + f64::from(i) * 0.1;
275 c(m + 1.0, m - 1.0, m, i64::from(i))
276 })
277 .collect();
278 let mut rei = TdRei::classic();
279 rei.batch(&candles);
280 assert!(rei.is_ready());
281 rei.reset();
282 assert!(!rei.is_ready());
283 assert_eq!(rei.update(candles[0]), None);
284 assert_eq!(rei.value(), None);
285 }
286
287 #[test]
288 fn accessors_and_metadata() {
289 let rei = TdRei::classic();
290 assert_eq!(rei.period(), 5);
291 assert_eq!(rei.warmup_period(), 6 + 5);
292 assert_eq!(rei.name(), "TDREI");
293 }
294}