wickra_core/indicators/
tii.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::sma::Sma;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
46pub struct Tii {
47 sma_period: usize,
48 dev_period: usize,
49 sma: Sma,
50 window: VecDeque<f64>,
52 sum_pos: f64,
53 sum_neg: f64,
54 last: Option<f64>,
55}
56
57impl Tii {
58 pub fn new(sma_period: usize, dev_period: usize) -> Result<Self> {
67 if sma_period == 0 || dev_period == 0 {
68 return Err(Error::PeriodZero);
69 }
70 Ok(Self {
71 sma_period,
72 dev_period,
73 sma: Sma::new(sma_period)?,
74 window: VecDeque::with_capacity(dev_period),
75 sum_pos: 0.0,
76 sum_neg: 0.0,
77 last: None,
78 })
79 }
80
81 pub const fn periods(&self) -> (usize, usize) {
83 (self.sma_period, self.dev_period)
84 }
85
86 pub const fn value(&self) -> Option<f64> {
88 self.last
89 }
90}
91
92impl Indicator for Tii {
93 type Input = f64;
94 type Output = f64;
95
96 fn update(&mut self, input: f64) -> Option<f64> {
97 let sma_value = self.sma.update(input)?;
98 let dev = input - sma_value;
99
100 if self.window.len() == self.dev_period {
101 let old = self.window.pop_front().expect("ring is non-empty");
102 if old > 0.0 {
103 self.sum_pos -= old;
104 } else if old < 0.0 {
105 self.sum_neg -= -old;
106 }
107 }
108 self.window.push_back(dev);
109 if dev > 0.0 {
110 self.sum_pos += dev;
111 } else if dev < 0.0 {
112 self.sum_neg += -dev;
113 }
114
115 if self.window.len() < self.dev_period {
116 return None;
117 }
118
119 let denom = self.sum_pos + self.sum_neg;
120 let tii = if denom <= 0.0 {
121 50.0
129 } else {
130 (100.0 * self.sum_pos / denom).clamp(0.0, 100.0)
135 };
136 self.last = Some(tii);
137 Some(tii)
138 }
139
140 fn reset(&mut self) {
141 self.sma.reset();
142 self.window.clear();
143 self.sum_pos = 0.0;
144 self.sum_neg = 0.0;
145 self.last = None;
146 }
147
148 #[inline]
149 fn warmup_period(&self) -> usize {
150 self.sma_period + self.dev_period - 1
154 }
155
156 #[inline]
157 fn is_ready(&self) -> bool {
158 self.last.is_some()
159 }
160
161 #[inline]
162 fn name(&self) -> &'static str {
163 "TII"
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::traits::BatchExt;
171 use approx::assert_relative_eq;
172
173 #[test]
174 fn rejects_zero_period() {
175 assert!(matches!(Tii::new(0, 10), Err(Error::PeriodZero)));
176 assert!(matches!(Tii::new(10, 0), Err(Error::PeriodZero)));
177 }
178
179 #[test]
180 fn accessors_and_metadata() {
181 let mut t = Tii::new(60, 30).unwrap();
182 assert_eq!(t.periods(), (60, 30));
183 assert_eq!(t.warmup_period(), 89);
184 assert_eq!(t.name(), "TII");
185 assert!(t.value().is_none());
186 let prices: Vec<f64> = (1..=100).map(|i| 100.0 + f64::from(i)).collect();
187 for &p in &prices {
188 t.update(p);
189 }
190 assert!(t.value().is_some());
191 }
192
193 #[test]
194 fn first_emission_at_warmup_period() {
195 let prices: Vec<f64> = (1..=30)
196 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
197 .collect();
198 let mut t = Tii::new(5, 4).unwrap();
199 let out = t.batch(&prices);
200 let warmup = 5 + 4 - 1; for v in out.iter().take(warmup - 1) {
202 assert!(v.is_none());
203 }
204 assert!(out[warmup - 1].is_some());
205 }
206
207 #[test]
208 fn pure_uptrend_saturates_at_100() {
209 let prices: Vec<f64> = (1..=80).map(|i| 100.0 + f64::from(i)).collect();
212 let mut t = Tii::new(10, 5).unwrap();
213 let last = t.batch(&prices).into_iter().flatten().last().unwrap();
214 assert_relative_eq!(last, 100.0, epsilon = 1e-9);
215 }
216
217 #[test]
218 fn pure_downtrend_falls_to_zero() {
219 let prices: Vec<f64> = (1..=80).rev().map(|i| 100.0 + f64::from(i)).collect();
220 let mut t = Tii::new(10, 5).unwrap();
221 let last = t.batch(&prices).into_iter().flatten().last().unwrap();
222 assert_relative_eq!(last, 0.0, epsilon = 1e-9);
223 }
224
225 #[test]
226 fn constant_series_yields_neutral_50() {
227 let mut t = Tii::new(5, 4).unwrap();
230 let last = t
231 .batch(&[10.0_f64; 30])
232 .into_iter()
233 .flatten()
234 .last()
235 .unwrap();
236 assert_eq!(last, 50.0);
237 }
238
239 #[test]
240 fn output_bounded_in_unit_interval() {
241 let prices: Vec<f64> = (0..200)
242 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 6.0 + (f64::from(i) * 0.07).cos() * 3.0)
243 .collect();
244 let mut t = Tii::new(20, 10).unwrap();
245 for v in t.batch(&prices).into_iter().flatten() {
246 assert!((0.0..=100.0).contains(&v));
247 }
248 }
249
250 #[test]
251 fn batch_equals_streaming() {
252 let prices: Vec<f64> = (0..120)
253 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 5.0)
254 .collect();
255 let mut a = Tii::new(20, 10).unwrap();
256 let mut b = Tii::new(20, 10).unwrap();
257 assert_eq!(
258 a.batch(&prices),
259 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
260 );
261 }
262
263 #[test]
264 fn reset_clears_state() {
265 let mut t = Tii::new(5, 4).unwrap();
266 t.batch(&(1..=30).map(f64::from).collect::<Vec<_>>());
267 assert!(t.is_ready());
268 t.reset();
269 assert!(!t.is_ready());
270 assert_eq!(t.update(1.0), None);
271 }
272}